1
0

improve command and completions

This commit is contained in:
2026-04-08 20:54:51 +02:00
parent 8a7f922b52
commit b27b46471a
4 changed files with 503 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

361
a.patch Normal file
View File

@@ -0,0 +1,361 @@
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..dc0bd83
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+root = true
+
+[*]
+charset = utf-8
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
\ No newline at end of file
diff --git a/completion.bash b/completion.bash
index a96fc8c..68a370b 100644
--- a/completion.bash
+++ b/completion.bash
@@ -4,41 +4,85 @@ _comp_cmd_goto() {
local cur prev words cword comp_args
_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
- 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
+ if [[ "${word:0:1}" == '-' ]]; then # reading an option
+
+ # any option is invalid if...
+ if \
+ (( positional_args > 0 )) || # a positional parameter has been specified \
+ [[ -n "${expected_optarg}" ]] || # a previous option is expecting an argument \
+ ! [[ -v valid_opts["${word}"] ]] # the option is invalid given the previously provided ones \
+ then
+ return
+ fi
+
+ local arg_required=${valid_opts["${word}"]}
+
+ 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
- if [[ $cur == -* ]]; then
- _comp_compgen -- -W '"${!opts[@]}"'
+ if [[ "${cur:0:1}" == '-' ]]; then
+ if [[ -z "${expected_optarg}" ]] && (( positional_args == 0 )); then
+ _comp_compgen -- -W '"${!valid_opts[@]}"'
+ fi
return
fi
- case $prev in
- -e)
- [[ -v 'opts[-e]' ]] && __goto_comp_keys
- return
- ;;
- esac
+ if [[ "${prev:0:1}" == '-' ]]; then
+ case "${prev:1}" in
+ r|g)
+ __goto_comp_keys
+ ;;
+ esac
+ return
+ fi
- # do goto keys only if we did not have -[elt]
- [[ ${words[*]} == *\ -* ]] || __goto_comp_keys
+ if (( opt_args == 0 && positional_args == 0 )); then
+ __goto_comp_keys
+ fi
} &&
complete -F _comp_cmd_goto goto
-# ex: filetype=sh
\ No newline at end of file
+# ex: filetype=sh
diff --git a/goto.bash b/goto.bash
index 9a32e11..5076aa2 100644
--- a/goto.bash
+++ b/goto.bash
@@ -6,14 +6,14 @@
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
+# path to yq executable used by goto
readonly __GOTO_YQ=/usr/local/bin/yq
__goto_exists() {
[[ -f ${__GOTO_FILE} ]]
}
-__goto_load() {
+__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
@@ -24,7 +24,8 @@ __goto_load() {
return
fi
- local entries entry key value
+ local -a entries
+ local entry key value
mapfile entries < <(${__GOTO_YQ} 'explode(.) | to_entries[] | "\(.key) \(.value)"' ${__GOTO_FILE})
@@ -43,13 +44,27 @@ __goto_load() {
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 data
+ local -a data
+ __goto_load_keys data
- _comp_compgen -- -W '"${!data[@]}"'
+ _comp_compgen -- -W '"${data[@]}"'
}
__goto_resolve_path() {
@@ -63,7 +78,7 @@ __goto_prompt() {
# prompt string for the terminal
local -A data
- __goto_load data
+ __goto_load_entries data
local entry_key entry_value best_match="" best_key=""
@@ -71,7 +86,7 @@ __goto_prompt() {
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
+ if [[ "${PWD}" == "${entry_value}"?(/*) ]] && [[ ${#entry_value} -gt ${#best_match} ]]; then
best_match="${entry_value}"
best_key="${entry_key}"
fi
@@ -91,7 +106,7 @@ __goto_test() {
# command: goto -t
local -A data
- __goto_load data
+ __goto_load_entries data
local entry_key entry_value
local -i ret_code=0
@@ -119,7 +134,7 @@ __goto_edit() {
# edits the goto configuration file
# command: goto -e
- nano ${__GOTO_FILE}
+ ${EDITOR:-nano} ${__GOTO_FILE}
}
__goto_list() {
@@ -127,7 +142,7 @@ __goto_list() {
# command: goto -l
local -A data
- __goto_load data
+ __goto_load_entries data
local -a sorted_keys
mapfile -d '' sorted_keys < <(printf '%s\0' "${!data[@]}" | sort -z)
@@ -146,16 +161,17 @@ __goto_add() {
# $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}")
+ 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}
+ 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
@@ -179,32 +195,35 @@ __goto_get() {
# $1: directory alias
if ! __goto_exists; then
- return 11
+ echo "goto: $1: configuration file not found" >&2
+ return 10
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
- return 10
+ echo "goto: $1: no such entry" >&2
+ return 11
fi
-
+
__goto_resolve_path "${dir_path}"
}
goto() {
# 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
a)
__goto_add "${OPTARG}"
- return
+ can_run_main=false
;;
r)
__goto_remove "${OPTARG}"
- return
+ can_run_main=false
;;
g)
__goto_get "${OPTARG}"
@@ -222,8 +241,12 @@ goto() {
__goto_test
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
;;
esac
@@ -236,25 +259,30 @@ goto() {
return 1
fi
- if (( $# == 0 )); then
- cd
+ if ! ${can_run_main}; then
+ if (( $# > 0 )); then
+ echo 'goto: too many arguments' >&2
+ return 1
+ fi
+
return
fi
- if ! __goto_exists; then
- echo "goto: $1: configuration file not found" >&2
- return 3
+ if (( $# == 0 )); then
+ echo 'goto: not enough arguments' >&2
+ return
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
- echo "goto: $1: no such entry" >&2
- return 2
+ if (( ret_code != 0 )); then
+ return ${ret_code}
fi
- cd $(__goto_resolve_path "${dir_path}")
+ cd ${dir_path}
}
# complete -F __goto_completions goto
-source ${__GOTO_ROOT_DIR}/completion.bash
\ No newline at end of file
+source ${__GOTO_ROOT_DIR}/completion.bash

View File

@@ -4,40 +4,84 @@ _comp_cmd_goto() {
local cur prev words cword comp_args
_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
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
if [[ "${word:0:1}" == '-' ]]; then # reading an option
# any option is invalid if...
if \
(( positional_args > 0 )) || # a positional parameter has been specified \
[[ -n "${expected_optarg}" ]] || # a previous option is expecting an argument \
! [[ -v valid_opts["${word}"] ]] # the option is invalid given the previously provided ones \
then
return
fi
local arg_required=${valid_opts["${word}"]}
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
if [[ $cur == -* ]]; then
_comp_compgen -- -W '"${!opts[@]}"'
if [[ "${cur:0:1}" == '-' ]]; then
if [[ -z "${expected_optarg}" ]] && (( positional_args == 0 )); then
_comp_compgen -- -W '"${!valid_opts[@]}"'
fi
return
fi
case $prev in
-e)
[[ -v 'opts[-e]' ]] && __goto_comp_keys
return
;;
esac
if [[ "${prev:0:1}" == '-' ]]; then
case "${prev:1}" in
r|g)
__goto_comp_keys
;;
esac
return
fi
# do goto keys only if we did not have -[elt]
[[ ${words[*]} == *\ -* ]] || __goto_comp_keys
if (( opt_args == 0 && positional_args == 0 )); then
__goto_comp_keys
fi
} &&
complete -F _comp_cmd_goto goto

View File

@@ -6,14 +6,14 @@
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
# path to yq executable used by goto
readonly __GOTO_YQ=/usr/local/bin/yq
__goto_exists() {
[[ -f ${__GOTO_FILE} ]]
}
__goto_load() {
__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
@@ -24,7 +24,8 @@ __goto_load() {
return
fi
local entries entry key value
local -a entries
local entry key value
mapfile entries < <(${__GOTO_YQ} 'explode(.) | to_entries[] | "\(.key) \(.value)"' ${__GOTO_FILE})
@@ -43,13 +44,27 @@ __goto_load() {
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 data
local -a data
__goto_load_keys data
_comp_compgen -- -W '"${!data[@]}"'
_comp_compgen -- -W '"${data[@]}"'
}
__goto_resolve_path() {
@@ -63,7 +78,7 @@ __goto_prompt() {
# prompt string for the terminal
local -A data
__goto_load data
__goto_load_entries data
local entry_key entry_value best_match="" best_key=""
@@ -71,7 +86,7 @@ __goto_prompt() {
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
if [[ "${PWD}" == "${entry_value}"?(/*) ]] && [[ ${#entry_value} -gt ${#best_match} ]]; then
best_match="${entry_value}"
best_key="${entry_key}"
fi
@@ -91,7 +106,7 @@ __goto_test() {
# command: goto -t
local -A data
__goto_load data
__goto_load_entries data
local entry_key entry_value
local -i ret_code=0
@@ -119,7 +134,7 @@ __goto_edit() {
# edits the goto configuration file
# command: goto -e
nano ${__GOTO_FILE}
${EDITOR:-nano} ${__GOTO_FILE}
}
__goto_list() {
@@ -127,7 +142,7 @@ __goto_list() {
# command: goto -l
local -A data
__goto_load data
__goto_load_entries data
local -a sorted_keys
mapfile -d '' sorted_keys < <(printf '%s\0' "${!data[@]}" | sort -z)
@@ -146,16 +161,17 @@ __goto_add() {
# $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}")
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}
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
@@ -179,13 +195,15 @@ __goto_get() {
# $1: directory alias
if ! __goto_exists; then
return 11
echo "goto: $1: configuration file not found" >&2
return 10
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
return 10
echo "goto: $1: no such entry" >&2
return 11
fi
__goto_resolve_path "${dir_path}"
@@ -194,17 +212,18 @@ __goto_get() {
goto() {
# 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
a)
__goto_add "${OPTARG}"
return
can_run_main=false
;;
r)
__goto_remove "${OPTARG}"
return
can_run_main=false
;;
g)
__goto_get "${OPTARG}"
@@ -222,8 +241,12 @@ goto() {
__goto_test
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
;;
esac
@@ -236,24 +259,29 @@ goto() {
return 1
fi
if (( $# == 0 )); then
cd
if ! ${can_run_main}; then
if (( $# > 0 )); then
echo 'goto: too many arguments' >&2
return 1
fi
return
fi
if ! __goto_exists; then
echo "goto: $1: configuration file not found" >&2
return 3
if (( $# == 0 )); then
echo 'goto: not enough arguments' >&2
return
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
echo "goto: $1: no such entry" >&2
return 2
if (( ret_code != 0 )); then
return ${ret_code}
fi
cd $(__goto_resolve_path "${dir_path}")
cd ${dir_path}
}
# complete -F __goto_completions goto