1
0
Files
goto/goto.bash

289 lines
6.0 KiB
Bash

#!/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 <directory alias>[:<directory path>]
# $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 <directory alias>
# $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 <directory alias>
# $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}
}
# complete -F __goto_completions goto
source ${__GOTO_ROOT_DIR}/completion.bash