# shellcheck shell=bash # shellcheck disable=SC2039 # SOURCE: https://github.com/iridakos/goto # MIT License # # Copyright (c) 2018 Lazarus Lazaridis # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Changes to the given alias directory # or executes a command based on the arguments. goto() { local target _goto_resolve_db if [ -z "$1" ]; then # display usage and exit when no args _goto_usage return fi subcommand="$1" shift case "$subcommand" in -c|--cleanup) _goto_cleanup "$@" ;; -r|--register) # Register an alias _goto_register_alias "$@" ;; -u|--unregister) # Unregister an alias _goto_unregister_alias "$@" ;; -p|--push) # Push the current directory onto the pushd stack, then goto _goto_directory_push "$@" ;; -o|--pop) # Pop the top directory off of the pushd stack, then change that directory _goto_directory_pop ;; -l|--list) _goto_list_aliases ;; -x|--expand) # Expand an alias _goto_expand_alias "$@" ;; -h|--help) _goto_usage ;; -v|--version) _goto_version ;; *) _goto_directory "$subcommand" ;; esac return $? } _goto_resolve_db() { local CONFIG_DEFAULT="${XDG_CONFIG_HOME:-$HOME/.config}/goto" GOTO_DB="${GOTO_DB:-$CONFIG_DEFAULT}" GOTO_DB_CONFIG_DIRNAME=$(dirname "$GOTO_DB") if [[ ! -d "$GOTO_DB_CONFIG_DIRNAME" ]]; then mkdir "$GOTO_DB_CONFIG_DIRNAME" fi touch -a "$GOTO_DB" } _goto_usage() { cat <<\USAGE usage: goto [<option>] <alias> [<directory>] default usage: goto <alias> - changes to the directory registered for the given alias OPTIONS: -r, --register: registers an alias goto -r|--register <alias> <directory> -u, --unregister: unregisters an alias goto -u|--unregister <alias> -p, --push: pushes the current directory onto the stack, then performs goto goto -p|--push <alias> -o, --pop: pops the top directory from the stack, then changes to that directory goto -o|--pop -l, --list: lists aliases goto -l|--list -x, --expand: expands an alias goto -x|--expand <alias> -c, --cleanup: cleans up non existent directory aliases goto -c|--cleanup -h, --help: prints this help goto -h|--help -v, --version: displays the version of the goto script goto -v|--version USAGE } # Displays version _goto_version() { echo "goto version 2.1.0" } # Expands directory. # Helpful for ~, ., .. paths _goto_expand_directory() { builtin cd "$1" 2>/dev/null && pwd } # Lists registered aliases. _goto_list_aliases() { local IFS=$' ' if [ -f "$GOTO_DB" ]; then local maxlength=0 while read -r name directory; do local length=${#name} if [[ $length -gt $maxlength ]]; then local maxlength=$length fi done < "$GOTO_DB" while read -r name directory; do printf "\e[1;36m%${maxlength}s \e[0m%s\n" "$name" "$directory" done < "$GOTO_DB" else echo "You haven't configured any directory aliases yet." fi } # Expands a registered alias. _goto_expand_alias() { if [ "$#" -ne "1" ]; then _goto_error "usage: goto -x|--expand <alias>" return fi local resolved resolved=$(_goto_find_alias_directory "$1") if [ -z "$resolved" ]; then _goto_error "alias '$1' does not exist" return fi echo "$resolved" } # Lists duplicate directory aliases _goto_find_duplicate() { local duplicates= duplicates=$(sed -n 's:[^ ]* '"$1"'$:&:p' "$GOTO_DB" 2>/dev/null) echo "$duplicates" } # Registers and alias. _goto_register_alias() { if [ "$#" -ne "2" ]; then _goto_error "usage: goto -r|--register <alias> <directory>" return 1 fi if ! [[ $1 =~ ^[[:alnum:]]+[a-zA-Z0-9_-]*$ ]]; then _goto_error "invalid alias - can start with letters or digits followed by letters, digits, hyphens or underscores" return 1 fi local resolved resolved=$(_goto_find_alias_directory "$1") if [ -n "$resolved" ]; then _goto_error "alias '$1' exists" return 1 fi local directory directory=$(_goto_expand_directory "$2") if [ -z "$directory" ]; then _goto_error "failed to register '$1' to '$2' - can't cd to directory" return 1 fi local duplicate duplicate=$(_goto_find_duplicate "$directory") if [ -n "$duplicate" ]; then _goto_warning "duplicate alias(es) found: \\n$duplicate" fi # Append entry to file. echo "$1 $directory" >> "$GOTO_DB" echo "Alias '$1' registered successfully." } # Unregisters the given alias. _goto_unregister_alias() { if [ "$#" -ne "1" ]; then _goto_error "usage: goto -u|--unregister <alias>" return 1 fi local resolved resolved=$(_goto_find_alias_directory "$1") if [ -z "$resolved" ]; then _goto_error "alias '$1' does not exist" return 1 fi # shellcheck disable=SC2034 local readonly GOTO_DB_TMP="$HOME/.goto_" # Delete entry from file. sed "/^$1 /d" "$GOTO_DB" > "$GOTO_DB_TMP" && mv "$GOTO_DB_TMP" "$GOTO_DB" echo "Alias '$1' unregistered successfully." } # Pushes the current directory onto the stack, then goto _goto_directory_push() { if [ "$#" -ne "1" ]; then _goto_error "usage: goto -p|--push <alias>" return fi { pushd . || return; } 1>/dev/null 2>&1 _goto_directory "$@" } # Pops the top directory from the stack, then goto _goto_directory_pop() { { popd || return; } 1>/dev/null 2>&1 } # Unregisters aliases whose directories no longer exist. _goto_cleanup() { if ! [ -f "$GOTO_DB" ]; then return fi while IFS= read -r i && [ -n "$i" ]; do echo "Cleaning up: $i" _goto_unregister_alias "$i" done <<< "$(awk '{al=$1; $1=""; dir=substr($0,2); system("[ ! -d \"" dir "\" ] && echo " al)}' "$GOTO_DB")" } # Changes to the given alias' directory _goto_directory() { # directly goto the special name that is unable to be registered due to invalid alias, eg: ~ if ! [[ $1 =~ ^[[:alnum:]]+[a-zA-Z0-9_-]*$ ]]; then { builtin cd "$1" 2> /dev/null && return 0; } || \ { _goto_error "Failed to goto '$1'" && return 1; } fi local target target=$(_goto_resolve_alias "$1") || return 1 builtin cd "$target" 2> /dev/null || \ { _goto_error "Failed to goto '$target'" && return 1; } } # Fetches the alias directory. _goto_find_alias_directory() { local resolved resolved=$(sed -n "s/^$1 \\(.*\\)/\\1/p" "$GOTO_DB" 2>/dev/null) echo "$resolved" } # Displays the given error. # Used for common error output. _goto_error() { (>&2 echo -e "goto error: $1") } # Displays the given warning. # Used for common warning output. _goto_warning() { (>&2 echo -e "goto warning: $1") } # Displays entries with aliases starting as the given one. _goto_print_similar() { local similar similar=$(sed -n "/^$1[^ ]* .*/p" "$GOTO_DB" 2>/dev/null) if [ -n "$similar" ]; then (>&2 echo "Did you mean:") (>&2 column -t <<< "$similar") fi } # Fetches alias directory, errors if it doesn't exist. _goto_resolve_alias() { local resolved resolved=$(_goto_find_alias_directory "$1") if [ -z "$resolved" ]; then _goto_error "unregistered alias $1" _goto_print_similar "$1" return 1 else echo "${resolved}" fi } # Completes the goto function with the available commands _complete_goto_commands() { local IFS=$' \t\n' # shellcheck disable=SC2207 COMPREPLY=($(compgen -W "-r --register -u --unregister -p --push -o --pop -l --list -x --expand -c --cleanup -v --version" -- "$1")) } # Completes the goto function with the available aliases _complete_goto_aliases() { local IFS=$'\n' matches _goto_resolve_db # shellcheck disable=SC2207 matches=($(sed -n "/^$1/p" "$GOTO_DB" 2>/dev/null)) if [ "${#matches[@]}" -eq "1" ]; then # remove the filenames attribute from the completion method compopt +o filenames 2>/dev/null # if you find only one alias don't append the directory COMPREPLY=("${matches[0]// *}") else for i in "${!matches[@]}"; do # remove the filenames attribute from the completion method compopt +o filenames 2>/dev/null if ! [[ $(uname -s) =~ Darwin* ]]; then matches[$i]=$(printf '%*s' "-$COLUMNS" "${matches[$i]}") COMPREPLY+=("$(compgen -W "${matches[$i]}")") else COMPREPLY+=("${matches[$i]// */}") fi done fi } # Bash programmable completion for the goto function _complete_goto_bash() { local cur="${COMP_WORDS[$COMP_CWORD]}" prev if [ "$COMP_CWORD" -eq "1" ]; then # if we are on the first argument if [[ $cur == -* ]]; then # and starts like a command, prompt commands _complete_goto_commands "$cur" else # and doesn't start as a command, prompt aliases _complete_goto_aliases "$cur" fi elif [ "$COMP_CWORD" -eq "2" ]; then # if we are on the second argument prev="${COMP_WORDS[1]}" if [[ $prev = "-u" ]] || [[ $prev = "--unregister" ]]; then # prompt with aliases if user tries to unregister one _complete_goto_aliases "$cur" elif [[ $prev = "-x" ]] || [[ $prev = "--expand" ]]; then # prompt with aliases if user tries to expand one _complete_goto_aliases "$cur" elif [[ $prev = "-p" ]] || [[ $prev = "--push" ]]; then # prompt with aliases only if user tries to push _complete_goto_aliases "$cur" fi elif [ "$COMP_CWORD" -eq "3" ]; then # if we are on the third argument prev="${COMP_WORDS[1]}" if [[ $prev = "-r" ]] || [[ $prev = "--register" ]]; then # prompt with directories only if user tries to register an alias local IFS=$' \t\n' # shellcheck disable=SC2207 COMPREPLY=($(compgen -d -- "$cur")) fi fi } # Zsh programmable completion for the goto function _complete_goto_zsh() { local all_aliases=() _goto_resolve_db while IFS= read -r line; do all_aliases+=("$line") done <<< "$(sed -e 's/ /:/g' $GOTO_DB 2>/dev/null)" local state local -a options=( '(1)'{-r,--register}'[registers an alias]:register:->register' '(- 1 2)'{-u,--unregister}'[unregisters an alias]:unregister:->unregister' '(: -)'{-l,--list}'[lists aliases]' '(*)'{-c,--cleanup}'[cleans up non existent directory aliases]' '(1 2)'{-x,--expand}'[expands an alias]:expand:->aliases' '(1 2)'{-p,--push}'[pushes the current directory onto the stack, then performs goto]:push:->aliases' '(*)'{-o,--pop}'[pops the top directory from stack, then changes to that directory]' '(: -)'{-h,--help}'[prints this help]' '(* -)'{-v,--version}'[displays the version of the goto script]' ) _arguments -C \ "${options[@]}" \ '1:alias:->aliases' \ '2:dir:_files' \ && ret=0 case ${state} in (aliases) _describe -t aliases 'goto aliases:' all_aliases && ret=0 ;; (unregister) _describe -t aliases 'unregister alias:' all_aliases && ret=0 ;; esac return $ret } goto_aliases=($(alias | sed -n "s/.*\s\(.*\)='goto'/\1/p")) goto_aliases+=("goto") for i in "${goto_aliases[@]}" do # Register the goto completions. if [ -n "${BASH_VERSION}" ]; then if ! [[ $(uname -s) =~ Darwin* ]]; then complete -o filenames -F _complete_goto_bash $i else complete -F _complete_goto_bash $i fi elif [ -n "${ZSH_VERSION}" ]; then compdef _complete_goto_zsh $i else echo "Unsupported shell." exit 1 fi done