#!/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 used by goto readonly __GOTO_YQ=/usr/local/bin/yq __goto_exists() { [[ -f ${__GOTO_FILE} ]] } __goto_load_entries() { # 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 -a entries local 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_load_keys() { # load the directory keys from the configuration file into the variable reference passed as parameter # $1: name of the result array to create local -n data_ref=$1 data_ref=() if ! __goto_exists; then return fi mapfile data_ref < <(${__GOTO_YQ} 'keys[]' ${__GOTO_FILE}) } __goto_comp_keys() { # loads and initializes directory aliases completions for the goto command local -a data __goto_load_keys 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_entries 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_entries 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 ${EDITOR:-nano} ${__GOTO_FILE} } __goto_list() { # displays the goto configuration file # command: goto -l local -A data __goto_load_entries 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 echo "goto: $1: configuration file not found" >&2 return 10 fi local dir_path=$(dir_alias="$1" ${__GOTO_YQ} 'explode(.)[env(dir_alias)] // ""' ${__GOTO_FILE}) if [[ -z "${dir_path}" ]]; then echo "goto: $1: no such entry" >&2 return 11 fi __goto_resolve_path "${dir_path}" } goto() { # the goto command local can_run_main=true local OPTIND OPT OPTARG while getopts ':a:r:g:elt' OPT; do case "${OPT}" in a) __goto_add "${OPTARG}" can_run_main=false ;; r) __goto_remove "${OPTARG}" can_run_main=false ;; g) __goto_get "${OPTARG}" return ;; e) __goto_edit return ;; l) __goto_list return ;; t) __goto_test return ;; :) echo "goto: missing argument for option '${OPTARG}'" >&2 return 1 ;; *) 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 ! ${can_run_main}; then if (( $# > 0 )); then echo 'goto: too many arguments' >&2 return 1 fi return fi if (( $# == 0 )); then echo 'goto: not enough arguments' >&2 return fi local dir_path dir_path=$(__goto_get "$1") local -i ret_code=$? if (( ret_code != 0 )); then return ${ret_code} fi cd ${dir_path} } . ${__GOTO_ROOT_DIR}/completion.bash