The goto shell utility allows users to navigate to aliased directories and also supports autocompletion.
How it works
Before you can use goto, you need to register your directory aliases. For example:
goto -r dev /home/iridakos/developmentthen change to that directory, e.g.:
goto dev
Autocompletion in goto
goto comes with a nice autocompletion script—whenever you press the Tab key after the goto command, Bash or Zsh will prompt you with suggestions of the aliases that are available:
$ goto <tab>
bc /etc/bash_completion.d                     
dev /home/iridakos/development
rubies /home/iridakos/.rvm/rubiesInstalling goto
There are several ways to install goto.
Via script
Clone the repository and run the install script as a superuser or root:
git clone https://github.com/iridakos/goto.git
cd goto
sudo ./installManually
Copy the file goto.sh somewhere in your filesystem and add a line in your .zshrc or .bashrc to source it.
For example, if you placed the file in your home folder, all you have to do is add the following line to your .zshrc or .bashrc file:
source ~/goto.shMacOS Homebrew
A formula named goto is available for the Bash shell in MacOS:
brew install gotoAdd colored output
echo -e "\$include /etc/inputrc\nset colored-completion-prefix on" >> ~/.inputrcNotes:
- You need to restart your shell after installation.
 - You need to have the Bash completion feature enabled for Bash in MacOS (see this issue).
- You can install it with brew install bash-completion if you don't have it enabled.
 
 
Ways to use goto
Change to an aliased directory
To change to an aliased directory, type:
goto <alias>For example:
goto devRegister an alias
To register a directory alias, type:
goto -r <alias> <directory>or
goto --register <alias> <directory>For example:
goto -r blog /mnt/external/projects/html/blogor
goto --register blog /mnt/external/projects/html/blogNotes:
- goto expands the directories, so you can easily alias your current directory with the following command and it will automatically be aliased to the whole path:
goto -r last_release . - Pressing the Tab key after the alias name provides the shell's default directory suggestions.
 
Unregister an alias
To unregister an alias, use:
goto -u <alias>or
goto --unregister <alias>For example:
goto -u last_releaseor
goto --unregister last_releaseNote: By pressing the Tab key after the command (-u or --unregister), the completion script will prompt you with the list of registered aliases.
List aliases
To get a list of your currently registered aliases, use:
goto -lor
goto --listExpand an alias
To expand an alias to its value, use:
goto -x <alias>or
goto --expand <alias>For example:
goto -x last_releaseor
goto --expand last_releaseClean up aliases
To clean up the aliases from directories that are no longer accessible in your filesystem, use:
goto -cor
goto --cleanupGet help
To view the tool's help information, use:
goto -hor
goto --helpCheck the version
To view the tool's version, use:
goto -vor
goto --versionPush before changing directories
To push the current directory onto the directory stack before changing directories, type:
goto -p <alias>or
goto --push <alias>Revert to a pushed directory
To return to a pushed directory, type:
goto -oor
goto --popNote: This command is equivalent to popd but within the goto command.
Troubleshooting
If you see the error command not found: compdef in Zsh, it means you need to load bashcompinit. To do so, append this to your .zshrc file:
autoload bashcompinit
bashcompinitGet involved
The goto tool is open source under the MIT License terms, and contributions are welcomed. To learn more, visit the Contributing section in goto's GitHub repository.
The goto script
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()
{
  GOTO_DB="${GOTO_DB:-$HOME/.goto}"
  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 1.2.4.1"
}
# 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
    while read -r name directory; do
      printf '\e[1;36m%20s  \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()
{
  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=()
  while IFS= read -r line; do
    all_aliases+=("$line")
  done <<< "$(sed -e 's/ /:/g' ~/.goto 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
This was originally published as the README in goto's GitHub repository and is reused with permission.

Comments are closed.