#!/bin/bash # the goto command allows changing directory using directory aliases # directory where goto files live readonly __GOTO_ROOT_DIR=~/.goto # goto configuration file that contains the directory mappings readonly __GOTO_FILE=${__GOTO_ROOT_DIR}/goto.yaml # path to yq executable readonly __GOTO_YQ=/usr/local/bin/yq __goto_exists() { [[ -f ${__GOTO_FILE} ]] } __goto_load() { # load the directory mappings from the configuration file into the variable reference passed as parameter # $1: name of the result associative array to create local -n data_ref=$1 data_ref=() if ! __goto_exists; then return fi local entries entry key value mapfile entries < <(${__GOTO_YQ} 'explode(.) | to_entries[] | "\(.key) \(.value)"' ${__GOTO_FILE}) for entry in "${entries[@]}"; do if [[ -z "${entry}" ]]; then continue fi IFS=' ' read key value <<< "${entry}" if [[ -z "${key}" ]] || [[ -z "${value}" ]]; then continue fi data_ref["${key}"]="${value}" done } __goto_comp_keys() { # loads and initializes directory aliases completions for the goto command local -A data __goto_load data _comp_compgen -- -W '"${!data[@]}"' } __goto_resolve_path() { # resolves a path to its canonical form # $1: path to resolve readlink -m "${1/#\~/${HOME}}" } __goto_prompt() { # prompt string for the terminal local -A data __goto_load data local entry_key entry_value best_match="" best_key="" for entry_key in "${!data[@]}"; do entry_value=$(__goto_resolve_path "${data[${entry_key}]}") # select the closest ancestor of the current working directory if [[ "${PWD}" == "${entry_value}"* ]] && [[ ${#entry_value} -gt ${#best_match} ]]; then best_match="${entry_value}" best_key="${entry_key}" fi done if [[ -n "${best_key}" ]]; then local remaining="${PWD#${best_match}}" printf '%s' "#${best_key}${remaining}" else local w='\w' printf '%s' "${w@P}" fi } __goto_test() { # validates the goto configuration file # command: goto -t local -A data __goto_load data local entry_key entry_value local -i ret_code=0 for entry_key in "${!data[@]}"; do entry_value=$(__goto_resolve_path "${data[${entry_key}]}") if ! [[ -e "${entry_value}" ]]; then echo "Not found: ${entry_key} → ${entry_value}" ret_code=1 elif ! [[ -d "${entry_value}" ]]; then echo "Not a directory: ${entry_key} → ${entry_value}" ret_code=1 fi done if (( ret_code == 0 )); then echo 'Configuration ok' fi return ${ret_code} } __goto_edit() { # edits the goto configuration file # command: goto -e nano ${__GOTO_FILE} } __goto_list() { # displays the goto configuration file # command: goto -l local -A data __goto_load data local -a sorted_keys mapfile -d '' sorted_keys < <(printf '%s\0' "${!data[@]}" | sort -z) local entry_key entry_value for entry_key in "${sorted_keys[@]}"; do entry_value=$(__goto_resolve_path "${data[${entry_key}]}") echo "${entry_key};${entry_value}" done | column -t -s ';' } __goto_add() { # adds or replaces an entry to the goto configuration file # command: goto -a [:] # $1: directory alias and optional path local dir_alias dir_path IFS=':' read dir_alias dir_path <<< "$1" if [[ -n "${dir_path}" ]]; then dir_path=$(__goto_resolve_path "${dir_path}") else dir_path="${PWD}" fi if __goto_exists; then dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} -i '. | (.[env(dir_alias)] = env(dir_path))' ${__GOTO_FILE} else dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} '.[env(dir_alias)] = env(dir_path)' ${__GOTO_FILE} fi } __goto_remove() { # removes an entry from the goto configuration file # command: goto -r # $1: directory alias if ! __goto_exists; then return fi dir_alias="$1" ${__GOTO_YQ} -i 'del(.[env(dir_alias)])' ${__GOTO_FILE} } __goto_get() { # returns the path denoted by a directory alias # command: goto -g # $1: directory alias if ! __goto_exists; then return 11 fi local dir_path="$(dir_alias="$1" ${__GOTO_YQ} '.[env(dir_alias)] // ""' ${__GOTO_FILE})" if [[ -z "${dir_path}" ]]; then return 10 fi __goto_resolve_path "${dir_path}" } goto() { # the goto command local OPTIND OPT while getopts 'a:r:g:elt' OPT; do case "${OPT}" in a) __goto_add "${OPTARG}" return ;; r) __goto_remove "${OPTARG}" return ;; g) __goto_get "${OPTARG}" return ;; e) __goto_edit return ;; l) __goto_list return ;; t) __goto_test return ;; *) echo "goto: invalid option ${OPTARG}" >&2 return 1 ;; esac done shift $((OPTIND-1)) if (( $# > 1 )); then echo 'goto: too many arguments' >&2 return 1 fi if (( $# == 0 )); then cd return fi if ! __goto_exists; then echo "goto: $1: configuration file not found" >&2 return 3 fi local dir_path="$(dir_alias="$1" ${__GOTO_YQ} '.[env(dir_alias)] // ""' ${__GOTO_FILE})" if [[ -z "${dir_path}" ]]; then echo "goto: $1: no such entry" >&2 return 2 fi cd $(__goto_resolve_path "${dir_path}") } # complete -F __goto_completions goto source ${__GOTO_ROOT_DIR}/completion.bash