commit 28096308269aca3a61b8d11fc738bd804eaf7451 Author: Hervé BECHER Date: Mon Apr 6 18:54:09 2026 +0200 first commit diff --git a/completion.bash b/completion.bash new file mode 100644 index 0000000..a96fc8c --- /dev/null +++ b/completion.bash @@ -0,0 +1,44 @@ +# goto completion -*- shell-script -*- + +_comp_cmd_goto() { + local cur prev words cword comp_args + _comp_initialize -- "$@" || return + + # echo "[$cur] [$prev] [${words[@]}] [$cword] [$comp_args]" + + local -A opts=([-e]="" [-l]="" [-t]="") + + local i + for i in "${!words[@]}"; do + [[ ${words[i]} && $i -ne $cword ]] && unset -v "opts[${words[i]}]" + case "${words[i]}" in + -e) + unset -v 'opts[-l]' 'opts[-t]' + ;; + -l) + unset -v 'opts[-e]' 'opts[-t]' + ;; + -t) + unset -v 'opts[-e]' 'opts[-l]' + ;; + esac + done + + if [[ $cur == -* ]]; then + _comp_compgen -- -W '"${!opts[@]}"' + return + fi + + case $prev in + -e) + [[ -v 'opts[-e]' ]] && __goto_comp_keys + return + ;; + esac + + # do goto keys only if we did not have -[elt] + [[ ${words[*]} == *\ -* ]] || __goto_comp_keys +} && + complete -F _comp_cmd_goto goto + +# ex: filetype=sh \ No newline at end of file diff --git a/goto.bash b/goto.bash new file mode 100644 index 0000000..f841680 --- /dev/null +++ b/goto.bash @@ -0,0 +1,235 @@ +#!/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_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 + local entries entry key value + + mapfile entries < <(${__GOTO_YQ} 'explode(.) | to_entries[] | "\(.key) \(.value)"' ${__GOTO_FILE}) + + data_ref=() + + 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 + + dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} -i '. | (.[env(dir_alias)] = env(dir_path))' ${__GOTO_FILE} +} + +__goto_remove() { + # removes an entry from the goto configuration file + # command: goto -r + # $1: directory alias + + 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 + + local dir_path="$(dir_alias="$1" ${__GOTO_YQ} '.[env(dir_alias)] // ""' ${__GOTO_FILE})" + + if [[ -n "${dir_path}" ]]; then + __goto_resolve_path "${dir_path}" + else + return 10; + fi +} + +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 + + 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 \ No newline at end of file