Skip to content

Instantly share code, notes, and snippets.

@iwan-uschka
Last active May 29, 2021 14:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iwan-uschka/09445b4d94b45458cd43e266983a6f1f to your computer and use it in GitHub Desktop.
Save iwan-uschka/09445b4d94b45458cd43e266983a6f1f to your computer and use it in GitHub Desktop.
dev command with auto-completion
#compdef dev
function _dev() {
# set auto completion result
_describe 'command' __DEV_AUTOCOMPLETION_LIST
}
# Originally created by Stefan Judis. See
# https://www.stefanjudis.com/snippets/a-custom-dev-command-with-auto-completion/
#
# Creates a `dev` command for the shell (zsh) including autocompletion to
# provide a UI for selecting a dev project in (aka git repository cloned into)
# `$__DEV_ROOT_DIR_PATH`.
#
# Unlike to the solution of Stefan Judis, here the folder structure can be
# arbitrarily complex. A project directory just needs to be a valid git
# repository (containing a `.git` folder) to be identified by this script.
#
# Directories can be excluded explicitely which means they won't be scanned at
# all.
#
# Subdirectories of an identified project directory won't be scanned by default
# unless the project directory has been added to `$__DEV_FORCE_DIR_PATHS`. This
# can be helpful when using git submodules or workspaces. Example:
# `test-project-workspace` contains two submodules `test-sub-1` and
# `test-sub-2`. `test-project-workspace` needs to be added to
# `$__DEV_FORCE_DIR_PATHS` to enable searching for `.git` in its subdirectories.
#
# The final list containing all projects is stored into an index file
# physically. If the file doesn't exists it will be created automatically on
# shell startup. If the list needs to be created manually (after a new project
# has been added/created) `dev_create_index` needs to be called in the shell.
#
# Creating the index can take a while depending on the project structure.
#
# After selecting a project batch actions are executed for this project:
# - change directory in the shell
# - set node version via `nvm
# - run npm script `dev` if present
#
# Optional batch actions can be triggered by setting specific arguments:
# - `--code` / `-c` => open project in VS code
# - `--finder` / `-f` => open project in Finder
# - `--git` / `-g` => open project in Tower (git GUI)
#
# ZSH autocompletion needs to be installed and activated (see
# https://github.com/zsh-users/zsh-completions#using-zsh-frameworks).
# Final list to create.
__DEV_AUTOCOMPLETION_LIST=()
# Directory to scan for projects recursively.
__DEV_ROOT_DIR_PATH=~/projects
# Path to index cache file.
__DEV_INDEX_FILE_PATH="$ZSH/cache/dev-index"
# List of directory paths to prevent scanning for.
__DEV_EXCLUDE_DIR_PATHS=("/_Ressources" "/_github" "/_services" "*/platform-backups" "*/node_modules" "*/bower_components" "*/.*" "*/_*")
# List of directory paths to ignore when creating the autocompletion list.
__DEV_IGNORE_DIR_PATHS=("*/*-workspace")
# List of directory paths where subdirectory scanning should be forced even if
# the directory itself is a valid git repository.
__DEV_FORCE_DIR_PATHS=("*/*-workspace")
# Remove the following string at the end of each autocompletion list item.
# Example: `path/to/project/sub-dir-containing-project` should be displayed
# in autocompletion as `path/to/project`. In this case
# `__DEV_RTRIM_AUTOCOMPLETION="/sub-dir-containing-project"` does the trick.
__DEV_RTRIM_AUTOCOMPLETION="/production"
# Main shell command `dev`.
function dev() {
echo -e "Starting up \033[1m$1\033[0m ..."
PROJECT_DIR=$__DEV_ROOT_DIR_PATH/$1
if [ -n "$__DEV_RTRIM_AUTOCOMPLETION" ]; then
if [ -d "$PROJECT_DIR$__DEV_RTRIM_AUTOCOMPLETION" ]; then
PROJECT_DIR+=$__DEV_RTRIM_AUTOCOMPLETION
fi
fi
while [[ "$#" -gt 0 ]]; do
case $1 in
-g | --git) OPEN_GIT_GUI=true ;;
-f | --finder) OPEN_FINDER=true ;;
-c | --code) OPEN_CODE_EDITOR=true ;;
*)
# echo "Unknown parameter passed: $1"
# exit 1
;;
esac
shift
done
cd $PROJECT_DIR || return
if [ "$OPEN_GIT_GUI" = true ]; then
gittower .
fi
if [ "$OPEN_FINDER" = true ]; then
open .
fi
if [ "$OPEN_CODE_EDITOR" = true ]; then
code .
fi
if [ -f "$PROJECT_DIR/package.json" ]; then
NODE_VERSION=$(jq ".engines.node" <"$PROJECT_DIR/package.json")
DEV_COMMAND=$(jq ".scripts.dev" <"$PROJECT_DIR/package.json")
if [ "$NODE_VERSION" != "null" ]; then
# `nvm` doesn't provide an option to use a node version advised in
# package.json because it's advisory only and not proscriptive. See
# https://github.com/nvm-sh/nvm/issues/651
#
# But it's always safe to choose the oldest version extractable from a
# semver range. The following examples work if `v12.20.0` has been
# installed via `nvm`:
# - `>=12.20.0` => `nvm use 12.20.0` => `v12.20.0`
# - `>=12` => `nvm use 12` => `v12.20.0`
#
# What doesn't work here are negative basic comparisons like
# - `!=`
# - '<'
nvm use $(sed -nre 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' <<<$NODE_VERSION)
fi
if [ "$DEV_COMMAND" != "null" ]; then
npm run dev
fi
fi
}
# Create index cache.
function dev_create_index() {
echo "Creating $__DEV_INDEX_FILE_PATH ..."
FIND_EXCLUDE_ARGS=()
for __DEV_EXCLUDE_DIR_PATH in "${__DEV_EXCLUDE_DIR_PATHS[@]}"; do
FIND_EXCLUDE_ARGS+=('-o' '-path' "$__DEV_ROOT_DIR_PATH$__DEV_EXCLUDE_DIR_PATH")
done
FIND_IGNORE_ARGS=()
for __DEV_IGNORE_DIR_PATH in "${__DEV_IGNORE_DIR_PATHS[@]}"; do
FIND_IGNORE_ARGS+=('-not' '-path' "$__DEV_ROOT_DIR_PATH$__DEV_IGNORE_DIR_PATH")
done
# `-name "$__DEV_ROOT_DIR_PATH"` will never be true so it's safe to add it
# here. This way `$_FIND_FORCE_ARGS` has at least one element even if
# `$__DEV_IGNORE_DIR_PATHS` is empty. This way an error like `empty
# expression` can be prevented later while subtituting
# `"${FIND_FORCE_ARGS[@]}"` in `find` statement.
FIND_FORCE_ARGS=('-name' "$__DEV_ROOT_DIR_PATH")
# first `-o` is removed later on because the array acts as an isolated expression
for __DEV_FORCE_DIR_PATH in "${__DEV_FORCE_DIR_PATHS[@]}"; do
FIND_FORCE_ARGS+=('-o' '-name' "$__DEV_ROOT_DIR_PATH$__DEV_FORCE_DIR_PATH")
done
find $__DEV_ROOT_DIR_PATH \
-type d \
"${FIND_IGNORE_ARGS[@]}" \
\( \
\( -exec sh -c 'test -d "$1"/.git' -- {} \; -print ! \( "${FIND_FORCE_ARGS[@]}" \) \) \
"${FIND_EXCLUDE_ARGS[@]}" \
\) -prune |
sort -u -f >$__DEV_INDEX_FILE_PATH
dev_set_autocompletion
}
function dev_set_autocompletion() {
while IFS= read -r line; do
__DEV_AUTOCOMPLETION_LIST+=$(sed -r "s|^$__DEV_ROOT_DIR_PATH/||;s|$__DEV_RTRIM_AUTOCOMPLETION$||" <<<$line)
done <$__DEV_INDEX_FILE_PATH
}
# Create index cache file if it doesn't exist on shell start up.
if [ ! -f "$__DEV_INDEX_FILE_PATH" ]; then
echo "$__DEV_INDEX_FILE_PATH does not exist."
dev_create_index
else
dev_set_autocompletion
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment