1
0

improve command and completions

This commit is contained in:
2026-04-08 20:54:51 +02:00
parent 8a7f922b52
commit 730d65bf1d
3 changed files with 142 additions and 62 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -4,40 +4,84 @@ _comp_cmd_goto() {
local cur prev words cword comp_args local cur prev words cword comp_args
_comp_initialize -- "$@" || return _comp_initialize -- "$@" || return
# echo "[$cur] [$prev] [${words[@]}] [$cword] [$comp_args]" # the list of valid options with their argument requirement
# it is updated as the command is parsed
local -A valid_opts=([-a]=true [-r]=true [-g]=true [-e]=false [-l]=false [-t]=false)
# how many positional arguments are expected
local -i expected_positional_args=1
# optionals and positionals read up to, not including ${cword} (current word index)
local -i opt_args=0 positional_args=0
# holds an option that requires its argument to be read next
local expected_optarg
local -A opts=([-e]="" [-l]="" [-t]="") local i word
for (( i = 1; i < cword; i++ )); do
word="${words[${i}]}"
local i if [[ "${word:0:1}" == '-' ]]; then # reading an option
for i in "${!words[@]}"; do
[[ ${words[i]} && $i -ne $cword ]] && unset -v "opts[${words[i]}]" # any option is invalid if...
case "${words[i]}" in if \
-e) (( positional_args > 0 )) || # a positional parameter has been specified \
unset -v 'opts[-l]' 'opts[-t]' [[ -n "${expected_optarg}" ]] || # a previous option is expecting an argument \
;; ! [[ -v valid_opts["${word}"] ]] # the option is invalid given the previously provided ones \
-l) then
unset -v 'opts[-e]' 'opts[-t]' return
;; fi
-t)
unset -v 'opts[-e]' 'opts[-l]' local arg_required=${valid_opts["${word}"]}
;;
esac case "${word:1}" in
a|r)
unset -v 'valid_opts[-g]' 'valid_opts[-e]' 'valid_opts[-l]' 'valid_opts[-t]'
;;
g|e|l|t)
valid_opts=()
;;
*)
return
;;
esac
if ${arg_required}; then
expected_optarg="${word}"
else
expected_optarg=
fi
expected_positional_args=0
(( opt_args++ ))
elif [[ -n "${expected_optarg}" ]]; then
expected_optarg=
else
(( positional_args++ ))
if (( positional_args > expected_positional_args )); then
return
fi
fi
done done
if [[ $cur == -* ]]; then if [[ "${cur:0:1}" == '-' ]]; then
_comp_compgen -- -W '"${!opts[@]}"' if [[ -z "${expected_optarg}" ]] && (( positional_args == 0 )); then
_comp_compgen -- -W '"${!valid_opts[@]}"'
fi
return return
fi fi
case $prev in if [[ "${prev:0:1}" == '-' ]]; then
-e) case "${prev:1}" in
[[ -v 'opts[-e]' ]] && __goto_comp_keys r|g)
return __goto_comp_keys
;; ;;
esac esac
return
fi
# do goto keys only if we did not have -[elt] if (( opt_args == 0 && positional_args == 0 )); then
[[ ${words[*]} == *\ -* ]] || __goto_comp_keys __goto_comp_keys
fi
} && } &&
complete -F _comp_cmd_goto goto complete -F _comp_cmd_goto goto

View File

@@ -6,14 +6,14 @@
readonly __GOTO_ROOT_DIR=~/.goto readonly __GOTO_ROOT_DIR=~/.goto
# goto configuration file that contains the directory mappings # goto configuration file that contains the directory mappings
readonly __GOTO_FILE=${__GOTO_ROOT_DIR}/goto.yaml readonly __GOTO_FILE=${__GOTO_ROOT_DIR}/goto.yaml
# path to yq executable # path to yq executable used by goto
readonly __GOTO_YQ=/usr/local/bin/yq readonly __GOTO_YQ=/usr/local/bin/yq
__goto_exists() { __goto_exists() {
[[ -f ${__GOTO_FILE} ]] [[ -f ${__GOTO_FILE} ]]
} }
__goto_load() { __goto_load_entries() {
# load the directory mappings from the configuration file into the variable reference passed as parameter # load the directory mappings from the configuration file into the variable reference passed as parameter
# $1: name of the result associative array to create # $1: name of the result associative array to create
@@ -24,7 +24,8 @@ __goto_load() {
return return
fi fi
local entries entry key value local -a entries
local entry key value
mapfile entries < <(${__GOTO_YQ} 'explode(.) | to_entries[] | "\(.key) \(.value)"' ${__GOTO_FILE}) mapfile entries < <(${__GOTO_YQ} 'explode(.) | to_entries[] | "\(.key) \(.value)"' ${__GOTO_FILE})
@@ -43,13 +44,27 @@ __goto_load() {
done 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() { __goto_comp_keys() {
# loads and initializes directory aliases completions for the goto command # loads and initializes directory aliases completions for the goto command
local -A data local -a data
__goto_load data __goto_load_keys data
_comp_compgen -- -W '"${!data[@]}"' _comp_compgen -- -W '"${data[@]}"'
} }
__goto_resolve_path() { __goto_resolve_path() {
@@ -63,7 +78,7 @@ __goto_prompt() {
# prompt string for the terminal # prompt string for the terminal
local -A data local -A data
__goto_load data __goto_load_entries data
local entry_key entry_value best_match="" best_key="" local entry_key entry_value best_match="" best_key=""
@@ -71,7 +86,7 @@ __goto_prompt() {
entry_value=$(__goto_resolve_path "${data[${entry_key}]}") entry_value=$(__goto_resolve_path "${data[${entry_key}]}")
# select the closest ancestor of the current working directory # select the closest ancestor of the current working directory
if [[ "${PWD}" == "${entry_value}"* ]] && [[ ${#entry_value} -gt ${#best_match} ]]; then if [[ "${PWD}" == "${entry_value}"?(/*) ]] && [[ ${#entry_value} -gt ${#best_match} ]]; then
best_match="${entry_value}" best_match="${entry_value}"
best_key="${entry_key}" best_key="${entry_key}"
fi fi
@@ -91,7 +106,7 @@ __goto_test() {
# command: goto -t # command: goto -t
local -A data local -A data
__goto_load data __goto_load_entries data
local entry_key entry_value local entry_key entry_value
local -i ret_code=0 local -i ret_code=0
@@ -119,7 +134,7 @@ __goto_edit() {
# edits the goto configuration file # edits the goto configuration file
# command: goto -e # command: goto -e
nano ${__GOTO_FILE} ${EDITOR:-nano} ${__GOTO_FILE}
} }
__goto_list() { __goto_list() {
@@ -127,7 +142,7 @@ __goto_list() {
# command: goto -l # command: goto -l
local -A data local -A data
__goto_load data __goto_load_entries data
local -a sorted_keys local -a sorted_keys
mapfile -d '' sorted_keys < <(printf '%s\0' "${!data[@]}" | sort -z) mapfile -d '' sorted_keys < <(printf '%s\0' "${!data[@]}" | sort -z)
@@ -146,16 +161,17 @@ __goto_add() {
# $1: directory alias and optional path # $1: directory alias and optional path
local dir_alias dir_path local dir_alias dir_path
IFS=':' read dir_alias dir_path <<< "$1" IFS=':' read dir_alias dir_path <<< "$1"
if [[ -n "${dir_path}" ]]; then if [[ -n "${dir_path}" ]]; then
dir_path=$(__goto_resolve_path "${dir_path}") dir_path="$(__goto_resolve_path "${dir_path}")"
else else
dir_path="${PWD}" dir_path="${PWD}"
fi fi
if __goto_exists; then if __goto_exists; then
dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} -i '. | (.[env(dir_alias)] = env(dir_path))' ${__GOTO_FILE} dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} -i '.[env(dir_alias)] |= env(dir_path)' ${__GOTO_FILE}
else else
dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} '.[env(dir_alias)] = env(dir_path)' ${__GOTO_FILE} dir_alias="${dir_alias}" dir_path="${dir_path}" ${__GOTO_YQ} '.[env(dir_alias)] = env(dir_path)' ${__GOTO_FILE}
fi fi
@@ -179,13 +195,15 @@ __goto_get() {
# $1: directory alias # $1: directory alias
if ! __goto_exists; then if ! __goto_exists; then
return 11 echo "goto: $1: configuration file not found" >&2
return 10
fi fi
local dir_path="$(dir_alias="$1" ${__GOTO_YQ} '.[env(dir_alias)] // ""' ${__GOTO_FILE})" local dir_path=$(dir_alias="$1" ${__GOTO_YQ} 'explode(.)[env(dir_alias)] // ""' ${__GOTO_FILE})
if [[ -z "${dir_path}" ]]; then if [[ -z "${dir_path}" ]]; then
return 10 echo "goto: $1: no such entry" >&2
return 11
fi fi
__goto_resolve_path "${dir_path}" __goto_resolve_path "${dir_path}"
@@ -194,17 +212,18 @@ __goto_get() {
goto() { goto() {
# the goto command # the goto command
local OPTIND OPT local can_run_main=true
local OPTIND OPT OPTARG
while getopts 'a:r:g:elt' OPT; do while getopts ':a:r:g:elt' OPT; do
case "${OPT}" in case "${OPT}" in
a) a)
__goto_add "${OPTARG}" __goto_add "${OPTARG}"
return can_run_main=false
;; ;;
r) r)
__goto_remove "${OPTARG}" __goto_remove "${OPTARG}"
return can_run_main=false
;; ;;
g) g)
__goto_get "${OPTARG}" __goto_get "${OPTARG}"
@@ -222,8 +241,12 @@ goto() {
__goto_test __goto_test
return return
;; ;;
:)
echo "goto: missing argument for option '${OPTARG}'" >&2
return 1
;;
*) *)
echo "goto: invalid option ${OPTARG}" >&2 echo "goto: invalid option '${OPTARG}'" >&2
return 1 return 1
;; ;;
esac esac
@@ -236,24 +259,29 @@ goto() {
return 1 return 1
fi fi
if (( $# == 0 )); then if ! ${can_run_main}; then
cd if (( $# > 0 )); then
echo 'goto: too many arguments' >&2
return 1
fi
return return
fi fi
if ! __goto_exists; then if (( $# == 0 )); then
echo "goto: $1: configuration file not found" >&2 echo 'goto: not enough arguments' >&2
return 3 return
fi fi
local dir_path="$(dir_alias="$1" ${__GOTO_YQ} '.[env(dir_alias)] // ""' ${__GOTO_FILE})" local dir_path
dir_path=$(__goto_get "$1")
local -i ret_code=$?
if [[ -z "${dir_path}" ]]; then if (( ret_code != 0 )); then
echo "goto: $1: no such entry" >&2 return ${ret_code}
return 2
fi fi
cd $(__goto_resolve_path "${dir_path}") cd ${dir_path}
} }
# complete -F __goto_completions goto # complete -F __goto_completions goto