From eb40f566cc9e3c5a845a12caf0f4344dab2a0b2c Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 18:26:22 +0900 Subject: [PATCH 01/14] fix(_get_first_arg): remove invalid doccomment --- bash_completion | 2 -- 1 file changed, 2 deletions(-) diff --git a/bash_completion b/bash_completion index 00e3b6bf7b1..4ffb8b1a0cf 100644 --- a/bash_completion +++ b/bash_completion @@ -2157,8 +2157,6 @@ _comp_realcommand() } # This function returns the first argument, excluding options -# @param $1 chars Characters out of $COMP_WORDBREAKS which should -# NOT be considered word breaks. See _comp__reassemble_words. # TODO:API: rename per conventions _get_first_arg() { From f187baf67e6492aedf6328a9f0080fb3cd40665b Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 18:45:02 +0900 Subject: [PATCH 02/14] test(_count_args): quote argument in test command --- test/t/unit/test_unit_count_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 50fdcdd277a..665dc832dbe 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -8,7 +8,7 @@ ) class TestUnitCountArgs(TestUnitBase): def _test(self, *args, **kwargs): - return self._test_unit("_count_args %s; echo $args", *args, **kwargs) + return self._test_unit('_count_args %s; echo "$args"', *args, **kwargs) def test_1(self, bash): assert_bash_exec(bash, "COMP_CWORD= _count_args >/dev/null") From e8b63687c2f85b3b6d206c7b31e743deed49c483 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 18:48:52 +0900 Subject: [PATCH 03/14] test(_count_args): check unexpected stdout --- test/t/unit/test_unit_count_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 665dc832dbe..10d3ce17258 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -11,7 +11,7 @@ def _test(self, *args, **kwargs): return self._test_unit('_count_args %s; echo "$args"', *args, **kwargs) def test_1(self, bash): - assert_bash_exec(bash, "COMP_CWORD= _count_args >/dev/null") + assert_bash_exec(bash, "COMP_CWORD= _count_args") def test_2(self, bash): """a b| should set args to 1""" From 5e68c1c31f551edfed90c734108a983741c514c7 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 18:30:26 +0900 Subject: [PATCH 04/14] refactor: rename `{ => _comp}_get_first_word` --- bash_completion | 11 ++++++----- .../000_bash_completion_compat.bash | 17 +++++++++++++++++ completions/cryptsetup | 2 +- completions/hcitool | 10 +++++----- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/bash_completion b/bash_completion index 4ffb8b1a0cf..a37a42e039f 100644 --- a/bash_completion +++ b/bash_completion @@ -2157,15 +2157,16 @@ _comp_realcommand() } # This function returns the first argument, excluding options -# TODO:API: rename per conventions -_get_first_arg() +# @var[out] arg First argument if any, or otherwise an empty string +# @since 2.12 +_comp_get_first_arg() { local i arg= - for ((i = 1; i < COMP_CWORD; i++)); do - if [[ ${COMP_WORDS[i]} != -* ]]; then - arg=${COMP_WORDS[i]} + for ((i = 1; i < cword; i++)); do + if [[ ${words[i]} != -* ]]; then + arg=${words[i]} break fi done diff --git a/bash_completion.d/000_bash_completion_compat.bash b/bash_completion.d/000_bash_completion_compat.bash index e85f296db74..6b013752ba1 100644 --- a/bash_completion.d/000_bash_completion_compat.bash +++ b/bash_completion.d/000_bash_completion_compat.bash @@ -398,4 +398,21 @@ _fstypes() _comp_compgen -a fstypes } +# This function returns the first argument, excluding options +# @deprecated 2.12 Use `_comp_get_first_arg`. Note that the new function +# `_comp_get_first_arg` operates on `words` and `cword` instead of `COMP_WORDS` +# and `COMP_CWORD`. +_get_first_arg() +{ + local i + + arg= + for ((i = 1; i < COMP_CWORD; i++)); do + if [[ ${COMP_WORDS[i]} != -* ]]; then + arg=${COMP_WORDS[i]} + break + fi + done +} + # ex: filetype=sh diff --git a/completions/cryptsetup b/completions/cryptsetup index 9604b08790d..84748dfe151 100644 --- a/completions/cryptsetup +++ b/completions/cryptsetup @@ -35,7 +35,7 @@ _comp_cmd_cryptsetup() [[ $was_split ]] && return local arg - _get_first_arg + _comp_get_first_arg if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help diff --git a/completions/hcitool b/completions/hcitool index 94b23e10fdd..f4c5c0a37ce 100644 --- a/completions/hcitool +++ b/completions/hcitool @@ -46,7 +46,7 @@ _comp_cmd_hcitool() [[ $was_split ]] && return local arg - _get_first_arg + _comp_get_first_arg if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help @@ -117,7 +117,7 @@ _comp_cmd_sdptool() [[ $was_split ]] && return local arg - _get_first_arg + _comp_get_first_arg if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help @@ -199,7 +199,7 @@ _comp_cmd_rfcomm() esac local arg - _get_first_arg + _comp_get_first_arg if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help @@ -238,7 +238,7 @@ _comp_cmd_ciptool() esac local arg - _get_first_arg + _comp_get_first_arg if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help @@ -294,7 +294,7 @@ _comp_cmd_hciconfig() _comp_initialize -- "$@" || return local arg - _get_first_arg + _comp_get_first_arg if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen -- -W '--help --all' From 4830fdde6dcfb62b4381828430805db99e4d7fd6 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 18:44:04 +0900 Subject: [PATCH 05/14] refactor: rename `{ => _comp}_count_args` --- bash_completion | 5 ++-- .../000_bash_completion_compat.bash | 1 + completions/7z | 2 +- completions/_cal | 2 +- completions/_xm | 18 +++++++------- completions/arp | 2 +- completions/avctrl | 2 +- completions/chmod | 2 +- completions/chown | 2 +- completions/cryptsetup | 2 +- completions/gpgv | 2 +- completions/hcitool | 24 +++++++++---------- completions/ifup | 2 +- completions/jq | 2 +- completions/jsonschema | 2 +- completions/lz4 | 2 +- completions/mkinitrd | 2 +- completions/nc | 2 +- completions/nslookup | 4 ++-- completions/patch | 2 +- completions/quota | 2 +- completions/sh | 2 +- completions/ssh | 2 +- completions/xdg-mime | 2 +- completions/xdg-settings | 2 +- completions/zopflipng | 2 +- test/t/unit/test_unit_count_args.py | 6 +++-- 27 files changed, 52 insertions(+), 48 deletions(-) diff --git a/bash_completion b/bash_completion index a37a42e039f..3334ae60ac8 100644 --- a/bash_completion +++ b/bash_completion @@ -2177,8 +2177,9 @@ _comp_get_first_arg() # NOT be considered word breaks. See _comp__reassemble_words. # @param $2 glob Options whose following argument should not be counted # @param $3 glob Options that should be counted as args -# TODO:API: rename per conventions -_count_args() +# @var[out] args Return the number of arguments +# @since 2.12 +_comp_count_args() { local i cword words _comp__reassemble_words "${1-}" words cword diff --git a/bash_completion.d/000_bash_completion_compat.bash b/bash_completion.d/000_bash_completion_compat.bash index 6b013752ba1..1bef50ce828 100644 --- a/bash_completion.d/000_bash_completion_compat.bash +++ b/bash_completion.d/000_bash_completion_compat.bash @@ -29,6 +29,7 @@ _comp_deprecate_func 2.12 _modules _comp_compgen_kernel_modules _comp_deprecate_func 2.12 _installed_modules _comp_compgen_inserted_kernel_modules _comp_deprecate_func 2.12 _usergroup _comp_compgen_usergroup _comp_deprecate_func 2.12 _complete_as_root _comp_as_root +_comp_deprecate_func 2.12 _count_args _comp_count_args # Backwards compatibility for compat completions that use have(). # @deprecated 1.90 should no longer be used; generally not needed with diff --git a/completions/7z b/completions/7z index 368641085cb..379de6e2587 100644 --- a/completions/7z +++ b/completions/7z @@ -85,7 +85,7 @@ _comp_cmd_7z() fi local args - _count_args "=" + _comp_count_args "=" if ((args == 2)); then _filedir_xspec unzip "${@:2}" # TODO: parsing 7z i output? diff --git a/completions/_cal b/completions/_cal index bf6845fb0c5..95b44707609 100644 --- a/completions/_cal +++ b/completions/_cal @@ -29,7 +29,7 @@ _comp_cmd_cal() fi local args - _count_args + _comp_count_args ((args == 1)) && _comp_compgen -- -W '{1..12}' } && complete -F _comp_cmd_cal cal ncal diff --git a/completions/_xm b/completions/_xm index 84f29894029..5dcfcbbfb68 100644 --- a/completions/_xm +++ b/completions/_xm @@ -79,7 +79,7 @@ _comp_cmd_xm() mem-max | pause | reboot | rename | shutdown | unpause | \ vcpu-list | vcpu-pin | vcpu-set | block-list | \ network-list | vtpm-list) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -87,7 +87,7 @@ _comp_cmd_xm() esac ;; migrate) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -101,7 +101,7 @@ _comp_cmd_xm() _comp_compgen_filedir ;; save) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -112,7 +112,7 @@ _comp_cmd_xm() esac ;; sysrq) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -123,7 +123,7 @@ _comp_cmd_xm() esac ;; block-attach) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -140,7 +140,7 @@ _comp_cmd_xm() esac ;; block-detach) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -152,7 +152,7 @@ _comp_cmd_xm() esac ;; network-attach) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -164,7 +164,7 @@ _comp_cmd_xm() esac ;; network-detach) - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names @@ -201,7 +201,7 @@ _comp_cmd_xm() ;; esac - _count_args + _comp_count_args case $args in 2) _comp_cmd_xm__domain_names diff --git a/completions/arp b/completions/arp index 7ae609cbb1d..9ab7b74b14d 100644 --- a/completions/arp +++ b/completions/arp @@ -34,7 +34,7 @@ _comp_cmd_arp() fi local args - _count_args "" "@(--device|--protocol|--file|--hw-type|-${noargopts}[iApfHt])" + _comp_count_args "" "@(--device|--protocol|--file|--hw-type|-${noargopts}[iApfHt])" case $args in 1) local ips=$("$1" -an | command sed -ne \ diff --git a/completions/avctrl b/completions/avctrl index f503bd67f22..33dcaef6245 100644 --- a/completions/avctrl +++ b/completions/avctrl @@ -9,7 +9,7 @@ _comp_cmd_avctrl() _comp_compgen -- -W '--help --quiet' else local args - _count_args + _comp_count_args if ((args == 1)); then _comp_compgen -- -W 'discover switch' fi diff --git a/completions/chmod b/completions/chmod index cf9a5b169c9..98be2d2ca06 100644 --- a/completions/chmod +++ b/completions/chmod @@ -28,7 +28,7 @@ _comp_cmd_chmod() fi local args - _count_args "" "" "$modearg" + _comp_count_args "" "" "$modearg" case $args in 1) ;; # mode diff --git a/completions/chown b/completions/chown index 22fd4bf94ba..d0685e39c39 100644 --- a/completions/chown +++ b/completions/chown @@ -32,7 +32,7 @@ _comp_cmd_chown() local args # The first argument is a usergroup; the rest are filedir. - _count_args : + _comp_count_args : if ((args == 1)); then _comp_compgen_usergroup -u diff --git a/completions/cryptsetup b/completions/cryptsetup index 84748dfe151..a572bb19e7b 100644 --- a/completions/cryptsetup +++ b/completions/cryptsetup @@ -48,7 +48,7 @@ _comp_cmd_cryptsetup() fi else local args - _count_args "" "-${noargopts}[chslSbopitTdM]" + _comp_count_args "" "-${noargopts}[chslSbopitTdM]" case $arg in open | create | luksOpen | loopaesOpen | tcryptOpen) case $args in diff --git a/completions/gpgv b/completions/gpgv index e83f5f8433c..dba7ad1cd5f 100644 --- a/completions/gpgv +++ b/completions/gpgv @@ -20,7 +20,7 @@ _comp_cmd_gpgv() esac local args - _count_args "" "--@(weak-digest|*-fd|keyring|homedir)" + _comp_count_args "" "--@(weak-digest|*-fd|keyring|homedir)" if [[ $cur == -* && $args -eq 1 ]]; then _comp_compgen_help diff --git a/completions/hcitool b/completions/hcitool index f4c5c0a37ce..69ec3382bdb 100644 --- a/completions/hcitool +++ b/completions/hcitool @@ -58,7 +58,7 @@ _comp_cmd_hcitool() local args case $arg in name | info | dc | rssi | lq | afh | auth | key | clkoff | lst) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi @@ -67,14 +67,14 @@ _comp_cmd_hcitool() if [[ $cur == -* ]]; then _comp_compgen -- -W '--role --pkt-type' else - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi fi ;; sr) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses else @@ -82,7 +82,7 @@ _comp_cmd_hcitool() fi ;; cpt) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses else @@ -90,7 +90,7 @@ _comp_cmd_hcitool() fi ;; tpl | enc | clock) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses else @@ -208,7 +208,7 @@ _comp_cmd_rfcomm() fi else local args - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_devices else @@ -249,7 +249,7 @@ _comp_cmd_ciptool() local args case $arg in connect | release | loopback) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi @@ -275,7 +275,7 @@ _comp_cmd_dfutool() _comp_compgen_help else local args - _count_args + _comp_count_args case $args in 1) _comp_compgen -- -W 'verify modify upgrade archive' @@ -309,19 +309,19 @@ _comp_cmd_hciconfig() local args case $arg in putkey | delkey) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi ;; lm) - _count_args + _comp_count_args if ((args == 2)); then _comp_compgen -- -W 'MASTER SLAVE NONE ACCEPT' fi ;; ptype) - _count_args + _comp_count_args if ((args == 2)); then _comp_cmd_hcitool__bluetooth_packet_types fi @@ -340,7 +340,7 @@ _comp_cmd_hciattach() _comp_compgen -- -W '-n -p -t -b -s -l' else local args - _count_args + _comp_count_args case $args in 1) _comp_expand_glob COMPREPLY '/dev/tty*' diff --git a/completions/ifup b/completions/ifup index e9ead9674c3..959760840b5 100644 --- a/completions/ifup +++ b/completions/ifup @@ -29,7 +29,7 @@ _comp_cmd_ifupdown() fi local args - _count_args "" "@(--allow|-i|--interfaces|--state-dir|-X|--exclude|-o)" + _comp_count_args "" "@(--allow|-i|--interfaces|--state-dir|-X|--exclude|-o)" if ((args == 1)); then _comp_compgen_configured_interfaces diff --git a/completions/jq b/completions/jq index f89cde17302..3acf389ac78 100644 --- a/completions/jq +++ b/completions/jq @@ -66,7 +66,7 @@ _comp_cmd_jq() local args # TODO: DTRT with args taking 2 options # -f|--from-file are not counted here because they supply the filter - _count_args "" "@(--arg|--arg?(json|file)|--slurpfile|--indent|--run-tests|-${noargopts}L)" + _comp_count_args "" "@(--arg|--arg?(json|file)|--slurpfile|--indent|--run-tests|-${noargopts}L)" # 1st arg is filter ((args == 1)) && return diff --git a/completions/jsonschema b/completions/jsonschema index cd3a8ff7fe5..759b1a84c63 100644 --- a/completions/jsonschema +++ b/completions/jsonschema @@ -21,7 +21,7 @@ _comp_cmd_jsonschema() fi local args - _count_args "" "-*" + _comp_count_args "" "-*" ((args == 1)) || return _comp_compgen_filedir '@(json|schema)' } && diff --git a/completions/lz4 b/completions/lz4 index b64700472df..39bf6d6f40b 100644 --- a/completions/lz4 +++ b/completions/lz4 @@ -20,7 +20,7 @@ _comp_cmd_lz4() fi local args word xspec="*.?(t)lz4" - _count_args + _comp_count_args ((args > 2)) && return for word in "${words[@]}"; do diff --git a/completions/mkinitrd b/completions/mkinitrd index 5b695559bb5..44f73d049b4 100644 --- a/completions/mkinitrd +++ b/completions/mkinitrd @@ -31,7 +31,7 @@ _comp_cmd_mkinitrd() [[ ${COMPREPLY-} == *= ]] && compopt -o nospace else local args - _count_args + _comp_count_args case $args in 1) diff --git a/completions/nc b/completions/nc index 104000e8e1b..9298abcbdb8 100644 --- a/completions/nc +++ b/completions/nc @@ -39,7 +39,7 @@ _comp_cmd_nc() # Complete 1st non-option arg only local args - _count_args "" "-*[IiMmOPpqsTVWwXx]" + _comp_count_args "" "-*[IiMmOPpqsTVWwXx]" ((args == 1)) || return _known_hosts_real -- "$cur" diff --git a/completions/nslookup b/completions/nslookup index 76408a254c8..a73a2ca7dd0 100644 --- a/completions/nslookup +++ b/completions/nslookup @@ -53,7 +53,7 @@ _comp_cmd_nslookup() fi local args - _count_args "=" + _comp_count_args "=" if ((args <= 2)); then _known_hosts_real -- "$cur" [[ $args -eq 1 && $cur == @(|-) ]] && COMPREPLY+=(-) @@ -90,7 +90,7 @@ _comp_cmd_host() fi local args - _count_args "" "-*[ctmNRW]" + _comp_count_args "" "-*[ctmNRW]" if ((args == 1)); then _known_hosts_real -- "$cur" elif ((args == 2)); then diff --git a/completions/patch b/completions/patch index 9f43148bede..f48cf340ad7 100644 --- a/completions/patch +++ b/completions/patch @@ -56,7 +56,7 @@ _comp_cmd_patch() fi local args - _count_args + _comp_count_args case $args in 1) _comp_compgen_filedir diff --git a/completions/quota b/completions/quota index 5772d47fa9b..48e6e9a5a35 100644 --- a/completions/quota +++ b/completions/quota @@ -84,7 +84,7 @@ _comp_cmd_setquota() _comp_cmd_quota__parse_help "$1" else local args - _count_args + _comp_count_args case $args in 1) diff --git a/completions/sh b/completions/sh index 75e8350f033..017af6b9f9a 100644 --- a/completions/sh +++ b/completions/sh @@ -26,7 +26,7 @@ _comp_cmd_sh() fi local args ext= - _count_args "" "@(-c|[-+]o)" + _comp_count_args "" "@(-c|[-+]o)" ((args == 1)) && ext="sh" _comp_compgen_filedir $ext } && diff --git a/completions/ssh b/completions/ssh index c83bf26b12c..648dec48a68 100644 --- a/completions/ssh +++ b/completions/ssh @@ -375,7 +375,7 @@ _comp_cmd_ssh() else local args # Keep glob sort in sync with cases above - _count_args "=" "-*[BbcDeLpRWEFSIiJlmOoQw]" + _comp_count_args "=" "-*[BbcDeLpRWEFSIiJlmOoQw]" if ((args > 1)); then compopt -o filenames _comp_compgen_commands diff --git a/completions/xdg-mime b/completions/xdg-mime index b779454468d..f1dda12135c 100644 --- a/completions/xdg-mime +++ b/completions/xdg-mime @@ -26,7 +26,7 @@ _comp_cmd_xdg_mime() _comp_initialize -- "$@" || return local args - _count_args + _comp_count_args if ((args == 1)); then if [[ $cur == -* ]]; then diff --git a/completions/xdg-settings b/completions/xdg-settings index 2e8f41b2292..d91aa7ec3e3 100644 --- a/completions/xdg-settings +++ b/completions/xdg-settings @@ -17,7 +17,7 @@ _comp_cmd_xdg_settings() fi local args - _count_args + _comp_count_args if ((args == 1)); then _comp_compgen -- -W "get check set" elif ((args == 2)); then diff --git a/completions/zopflipng b/completions/zopflipng index 71bd1180957..22df490791e 100644 --- a/completions/zopflipng +++ b/completions/zopflipng @@ -28,7 +28,7 @@ _comp_cmd_zopflipng() if [[ ${words[*]} != *\ --prefix=* ]]; then # 2 png args only if --prefix not given local args - _count_args + _comp_count_args ((args < 3)) && _comp_compgen_filedir png else # otherwise arbitrary number of png args diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 10d3ce17258..c343ef843f6 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -8,10 +8,12 @@ ) class TestUnitCountArgs(TestUnitBase): def _test(self, *args, **kwargs): - return self._test_unit('_count_args %s; echo "$args"', *args, **kwargs) + return self._test_unit( + '_comp_count_args %s; echo "$ret"', *args, **kwargs + ) def test_1(self, bash): - assert_bash_exec(bash, "COMP_CWORD= _count_args") + assert_bash_exec(bash, "COMP_CWORD= _comp_count_args") def test_2(self, bash): """a b| should set args to 1""" From ecfa34716df8e84e991afa2c3c93be46bd1d2bf1 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 19:12:56 +0900 Subject: [PATCH 06/14] refactor(_comp_count_args): return result via `ret` --- bash_completion | 6 ++-- .../000_bash_completion_compat.bash | 17 ++++++++- completions/7z | 4 +-- completions/_cal | 4 +-- completions/_xm | 20 +++++------ completions/arp | 4 +-- completions/avctrl | 4 +-- completions/chmod | 4 +-- completions/chown | 4 +-- completions/cryptsetup | 3 +- completions/gpgv | 3 +- completions/hcitool | 35 ++++++++++--------- completions/ifup | 4 +-- completions/jq | 4 +-- completions/jsonschema | 4 +-- completions/lz4 | 3 +- completions/mkinitrd | 4 +-- completions/nc | 4 +-- completions/nslookup | 12 +++---- completions/patch | 4 +-- completions/quota | 4 +-- completions/sh | 4 +-- completions/ssh | 4 +-- completions/xdg-mime | 4 +-- completions/xdg-settings | 6 ++-- completions/zopflipng | 4 +-- test/t/unit/test_unit_count_args.py | 2 +- 27 files changed, 97 insertions(+), 78 deletions(-) diff --git a/bash_completion b/bash_completion index 3334ae60ac8..9433541584c 100644 --- a/bash_completion +++ b/bash_completion @@ -2177,19 +2177,19 @@ _comp_get_first_arg() # NOT be considered word breaks. See _comp__reassemble_words. # @param $2 glob Options whose following argument should not be counted # @param $3 glob Options that should be counted as args -# @var[out] args Return the number of arguments +# @var[out] ret Return the number of arguments # @since 2.12 _comp_count_args() { local i cword words _comp__reassemble_words "${1-}" words cword - args=1 + ret=1 for ((i = 1; i < cword; i++)); do # shellcheck disable=SC2053 if [[ ${words[i]} != -* && ${words[i - 1]} != ${2-} || ${words[i]} == ${3-} ]]; then - ((args++)) + ((ret++)) fi done } diff --git a/bash_completion.d/000_bash_completion_compat.bash b/bash_completion.d/000_bash_completion_compat.bash index 1bef50ce828..3e4318e7af1 100644 --- a/bash_completion.d/000_bash_completion_compat.bash +++ b/bash_completion.d/000_bash_completion_compat.bash @@ -29,7 +29,6 @@ _comp_deprecate_func 2.12 _modules _comp_compgen_kernel_modules _comp_deprecate_func 2.12 _installed_modules _comp_compgen_inserted_kernel_modules _comp_deprecate_func 2.12 _usergroup _comp_compgen_usergroup _comp_deprecate_func 2.12 _complete_as_root _comp_as_root -_comp_deprecate_func 2.12 _count_args _comp_count_args # Backwards compatibility for compat completions that use have(). # @deprecated 1.90 should no longer be used; generally not needed with @@ -416,4 +415,20 @@ _get_first_arg() done } +# This function counts the number of args, excluding options +# @param $1 chars Characters out of $COMP_WORDBREAKS which should +# NOT be considered word breaks. See _comp__reassemble_words. +# @param $2 glob Options whose following argument should not be counted +# @param $3 glob Options that should be counted as args +# @var[out] args Return the number of arguments +# @deprecated 2.12 Use `_comp_count_args`. Note that the new function +# `_comp_count_args` returns the result in variable `ret` instead of `args`. +_count_args() +{ + local ret + _comp_count_args "$@" + # shellcheck disable=SC2178 + args=$ret +} + # ex: filetype=sh diff --git a/completions/7z b/completions/7z index 379de6e2587..87eca50a4aa 100644 --- a/completions/7z +++ b/completions/7z @@ -84,9 +84,9 @@ _comp_cmd_7z() return fi - local args + local ret _comp_count_args "=" - if ((args == 2)); then + if ((ret == 2)); then _filedir_xspec unzip "${@:2}" # TODO: parsing 7z i output? # - how to figure out if the format is input or output? diff --git a/completions/_cal b/completions/_cal index 95b44707609..6ca6520784d 100644 --- a/completions/_cal +++ b/completions/_cal @@ -28,9 +28,9 @@ _comp_cmd_cal() return fi - local args + local ret _comp_count_args - ((args == 1)) && _comp_compgen -- -W '{1..12}' + ((ret == 1)) && _comp_compgen -- -W '{1..12}' } && complete -F _comp_cmd_cal cal ncal diff --git a/completions/_xm b/completions/_xm index 5dcfcbbfb68..a79c74c1003 100644 --- a/completions/_xm +++ b/completions/_xm @@ -17,7 +17,7 @@ _comp_cmd_xm() # TODO: split longopt - local args command commands options + local ret command commands options commands='console vncviewer create new delete destroy domid domname dump-core list mem-max mem-set migrate pause reboot rename reset @@ -80,7 +80,7 @@ _comp_cmd_xm() vcpu-list | vcpu-pin | vcpu-set | block-list | \ network-list | vtpm-list) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -88,7 +88,7 @@ _comp_cmd_xm() ;; migrate) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -102,7 +102,7 @@ _comp_cmd_xm() ;; save) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -113,7 +113,7 @@ _comp_cmd_xm() ;; sysrq) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -124,7 +124,7 @@ _comp_cmd_xm() ;; block-attach) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -141,7 +141,7 @@ _comp_cmd_xm() ;; block-detach) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -153,7 +153,7 @@ _comp_cmd_xm() ;; network-attach) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -165,7 +165,7 @@ _comp_cmd_xm() ;; network-detach) _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; @@ -202,7 +202,7 @@ _comp_cmd_xm() esac _comp_count_args - case $args in + case $ret in 2) _comp_cmd_xm__domain_names ;; diff --git a/completions/arp b/completions/arp index 9ab7b74b14d..3e653516fd0 100644 --- a/completions/arp +++ b/completions/arp @@ -33,9 +33,9 @@ _comp_cmd_arp() return fi - local args + local ret _comp_count_args "" "@(--device|--protocol|--file|--hw-type|-${noargopts}[iApfHt])" - case $args in + case $ret in 1) local ips=$("$1" -an | command sed -ne \ 's/.*(\([0-9]\{1,3\}\(\.[0-9]\{1,3\}\)\{3\}\)).*/\1/p') diff --git a/completions/avctrl b/completions/avctrl index 33dcaef6245..139bf5961f6 100644 --- a/completions/avctrl +++ b/completions/avctrl @@ -8,9 +8,9 @@ _comp_cmd_avctrl() if [[ $cur == -* ]]; then _comp_compgen -- -W '--help --quiet' else - local args + local ret _comp_count_args - if ((args == 1)); then + if ((ret == 1)); then _comp_compgen -- -W 'discover switch' fi fi diff --git a/completions/chmod b/completions/chmod index 98be2d2ca06..440750f89f5 100644 --- a/completions/chmod +++ b/completions/chmod @@ -27,10 +27,10 @@ _comp_cmd_chmod() return fi - local args + local ret _comp_count_args "" "" "$modearg" - case $args in + case $ret in 1) ;; # mode *) _comp_compgen_filedir ;; esac diff --git a/completions/chown b/completions/chown index d0685e39c39..986dec79cc4 100644 --- a/completions/chown +++ b/completions/chown @@ -29,12 +29,12 @@ _comp_cmd_chown() --no-dereference --from --silent --quiet --reference --recursive --verbose --help --version $opts' else - local args + local ret # The first argument is a usergroup; the rest are filedir. _comp_count_args : - if ((args == 1)); then + if ((ret == 1)); then _comp_compgen_usergroup -u else _comp_compgen_filedir diff --git a/completions/cryptsetup b/completions/cryptsetup index a572bb19e7b..1c6fb090d38 100644 --- a/completions/cryptsetup +++ b/completions/cryptsetup @@ -47,8 +47,9 @@ _comp_cmd_cryptsetup() luksResume luksHeaderBackup luksHeaderRestore' fi else - local args + local ret _comp_count_args "" "-${noargopts}[chslSbopitTdM]" + local args=$ret case $arg in open | create | luksOpen | loopaesOpen | tcryptOpen) case $args in diff --git a/completions/gpgv b/completions/gpgv index dba7ad1cd5f..2d4aba65215 100644 --- a/completions/gpgv +++ b/completions/gpgv @@ -19,8 +19,9 @@ _comp_cmd_gpgv() ;; esac - local args + local ret _comp_count_args "" "--@(weak-digest|*-fd|keyring|homedir)" + local args=$ret if [[ $cur == -* && $args -eq 1 ]]; then _comp_compgen_help diff --git a/completions/hcitool b/completions/hcitool index 69ec3382bdb..828114fb40b 100644 --- a/completions/hcitool +++ b/completions/hcitool @@ -55,11 +55,11 @@ _comp_cmd_hcitool() dc sr cpt rssi lq tpl afh lst auth enc key clkoff clock' fi else - local args + local ret case $arg in name | info | dc | rssi | lq | afh | auth | key | clkoff | lst) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi ;; @@ -68,14 +68,14 @@ _comp_cmd_hcitool() _comp_compgen -- -W '--role --pkt-type' else _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi fi ;; sr) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses else _comp_compgen -- -W 'master slave' @@ -83,7 +83,7 @@ _comp_cmd_hcitool() ;; cpt) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses else _comp_cmd_hcitool__bluetooth_packet_types @@ -91,7 +91,7 @@ _comp_cmd_hcitool() ;; tpl | enc | clock) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses else _comp_compgen -- -W '0 1' @@ -207,8 +207,9 @@ _comp_cmd_rfcomm() _comp_compgen -- -W 'show connect listen watch bind release' fi else - local args + local ret _comp_count_args + local args=$ret if ((args == 2)); then _comp_cmd_hcitool__bluetooth_devices else @@ -246,11 +247,11 @@ _comp_cmd_ciptool() _comp_compgen -- -W 'show search connect release loopback' fi else - local args case $arg in connect | release | loopback) + local ret _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi ;; @@ -274,9 +275,9 @@ _comp_cmd_dfutool() if [[ $cur == -* ]]; then _comp_compgen_help else - local args + local ret _comp_count_args - case $args in + case $ret in 1) _comp_compgen -- -W 'verify modify upgrade archive' ;; @@ -306,23 +307,23 @@ _comp_cmd_hciconfig() version revision lm' fi else - local args + local ret case $arg in putkey | delkey) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_addresses fi ;; lm) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_compgen -- -W 'MASTER SLAVE NONE ACCEPT' fi ;; ptype) _comp_count_args - if ((args == 2)); then + if ((ret == 2)); then _comp_cmd_hcitool__bluetooth_packet_types fi ;; @@ -339,9 +340,9 @@ _comp_cmd_hciattach() if [[ $cur == -* ]]; then _comp_compgen -- -W '-n -p -t -b -s -l' else - local args + local ret _comp_count_args - case $args in + case $ret in 1) _comp_expand_glob COMPREPLY '/dev/tty*' ((${#COMPREPLY[@]})) && diff --git a/completions/ifup b/completions/ifup index 959760840b5..58c642f0916 100644 --- a/completions/ifup +++ b/completions/ifup @@ -28,10 +28,10 @@ _comp_cmd_ifupdown() return fi - local args + local ret _comp_count_args "" "@(--allow|-i|--interfaces|--state-dir|-X|--exclude|-o)" - if ((args == 1)); then + if ((ret == 1)); then _comp_compgen_configured_interfaces fi } && diff --git a/completions/jq b/completions/jq index 3acf389ac78..537b8f5a572 100644 --- a/completions/jq +++ b/completions/jq @@ -63,13 +63,13 @@ _comp_cmd_jq() [[ $word != --?(json)args ]] || return done - local args + local ret # TODO: DTRT with args taking 2 options # -f|--from-file are not counted here because they supply the filter _comp_count_args "" "@(--arg|--arg?(json|file)|--slurpfile|--indent|--run-tests|-${noargopts}L)" # 1st arg is filter - ((args == 1)) && return + ((ret == 1)) && return # 2... are input files _comp_compgen_filedir 'json?(l)' diff --git a/completions/jsonschema b/completions/jsonschema index 759b1a84c63..2b7c49d0e94 100644 --- a/completions/jsonschema +++ b/completions/jsonschema @@ -20,9 +20,9 @@ _comp_cmd_jsonschema() return fi - local args + local ret _comp_count_args "" "-*" - ((args == 1)) || return + ((ret == 1)) || return _comp_compgen_filedir '@(json|schema)' } && complete -F _comp_cmd_jsonschema jsonschema diff --git a/completions/lz4 b/completions/lz4 index 39bf6d6f40b..65b4a3b370b 100644 --- a/completions/lz4 +++ b/completions/lz4 @@ -19,8 +19,9 @@ _comp_cmd_lz4() return fi - local args word xspec="*.?(t)lz4" + local ret word xspec="*.?(t)lz4" _comp_count_args + local args=$ret ((args > 2)) && return for word in "${words[@]}"; do diff --git a/completions/mkinitrd b/completions/mkinitrd index 44f73d049b4..a81cfbb7299 100644 --- a/completions/mkinitrd +++ b/completions/mkinitrd @@ -30,10 +30,10 @@ _comp_cmd_mkinitrd() --net-dev --fstab --nocompress --dsdt --bootchart' [[ ${COMPREPLY-} == *= ]] && compopt -o nospace else - local args + local ret _comp_count_args - case $args in + case $ret in 1) _comp_compgen_filedir ;; diff --git a/completions/nc b/completions/nc index 9298abcbdb8..bb44418b14f 100644 --- a/completions/nc +++ b/completions/nc @@ -38,9 +38,9 @@ _comp_cmd_nc() fi # Complete 1st non-option arg only - local args + local ret _comp_count_args "" "-*[IiMmOPpqsTVWwXx]" - ((args == 1)) || return + ((ret == 1)) || return _known_hosts_real -- "$cur" } && diff --git a/completions/nslookup b/completions/nslookup index a73a2ca7dd0..13b029f23d6 100644 --- a/completions/nslookup +++ b/completions/nslookup @@ -52,11 +52,11 @@ _comp_cmd_nslookup() return fi - local args + local ret _comp_count_args "=" - if ((args <= 2)); then + if ((ret <= 2)); then _known_hosts_real -- "$cur" - [[ $args -eq 1 && $cur == @(|-) ]] && COMPREPLY+=(-) + [[ $ret -eq 1 && $cur == @(|-) ]] && COMPREPLY+=(-) fi } && complete -F _comp_cmd_nslookup nslookup @@ -89,11 +89,11 @@ _comp_cmd_host() return fi - local args + local ret _comp_count_args "" "-*[ctmNRW]" - if ((args == 1)); then + if ((ret == 1)); then _known_hosts_real -- "$cur" - elif ((args == 2)); then + elif ((ret == 2)); then local ipvx [[ ${words[*]} =~ \ -[^\ ]*([46]) ]] && ipvx=-${BASH_REMATCH[1]} # shellcheck disable=SC2086 diff --git a/completions/patch b/completions/patch index f48cf340ad7..8497788fe37 100644 --- a/completions/patch +++ b/completions/patch @@ -55,9 +55,9 @@ _comp_cmd_patch() return fi - local args + local ret _comp_count_args - case $args in + case $ret in 1) _comp_compgen_filedir ;; diff --git a/completions/quota b/completions/quota index 48e6e9a5a35..66ff638ff1b 100644 --- a/completions/quota +++ b/completions/quota @@ -83,10 +83,10 @@ _comp_cmd_setquota() if [[ $cur == -* ]]; then _comp_cmd_quota__parse_help "$1" else - local args + local ret _comp_count_args - case $args in + case $ret in 1) _comp_cmd_quota__user_or_group ;; diff --git a/completions/sh b/completions/sh index 017af6b9f9a..ee680d67b0b 100644 --- a/completions/sh +++ b/completions/sh @@ -25,9 +25,9 @@ _comp_cmd_sh() return fi - local args ext= + local ret ext= _comp_count_args "" "@(-c|[-+]o)" - ((args == 1)) && ext="sh" + ((ret == 1)) && ext="sh" _comp_compgen_filedir $ext } && complete -F _comp_cmd_sh sh diff --git a/completions/ssh b/completions/ssh index 648dec48a68..d67c35d9f50 100644 --- a/completions/ssh +++ b/completions/ssh @@ -373,10 +373,10 @@ _comp_cmd_ssh() elif [[ $cur == -* ]]; then _comp_compgen_usage else - local args + local ret # Keep glob sort in sync with cases above _comp_count_args "=" "-*[BbcDeLpRWEFSIiJlmOoQw]" - if ((args > 1)); then + if ((ret > 1)); then compopt -o filenames _comp_compgen_commands else diff --git a/completions/xdg-mime b/completions/xdg-mime index f1dda12135c..acdd09851eb 100644 --- a/completions/xdg-mime +++ b/completions/xdg-mime @@ -25,9 +25,9 @@ _comp_cmd_xdg_mime() local cur prev words cword comp_args _comp_initialize -- "$@" || return - local args + local ret _comp_count_args - + local args=$ret if ((args == 1)); then if [[ $cur == -* ]]; then _comp_compgen -- -W '--help --manual --version' diff --git a/completions/xdg-settings b/completions/xdg-settings index d91aa7ec3e3..2a4289d03a4 100644 --- a/completions/xdg-settings +++ b/completions/xdg-settings @@ -16,11 +16,11 @@ _comp_cmd_xdg_settings() return fi - local args + local ret _comp_count_args - if ((args == 1)); then + if ((ret == 1)); then _comp_compgen -- -W "get check set" - elif ((args == 2)); then + elif ((ret == 2)); then _comp_compgen_split -- "$("$1" --list | awk '!/^Known/ { print $1 }')" fi } && diff --git a/completions/zopflipng b/completions/zopflipng index 22df490791e..0392502e07e 100644 --- a/completions/zopflipng +++ b/completions/zopflipng @@ -27,9 +27,9 @@ _comp_cmd_zopflipng() if [[ ${words[*]} != *\ --prefix=* ]]; then # 2 png args only if --prefix not given - local args + local ret _comp_count_args - ((args < 3)) && _comp_compgen_filedir png + ((ret < 3)) && _comp_compgen_filedir png else # otherwise arbitrary number of png args _comp_compgen_filedir png diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index c343ef843f6..bdaed672ac2 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -4,7 +4,7 @@ @pytest.mark.bashcomp( - cmd=None, ignore_env=r"^[+-](args|COMP_(WORDS|CWORD|LINE|POINT))=" + cmd=None, ignore_env=r"^[+-](ret|COMP_(WORDS|CWORD|LINE|POINT))=" ) class TestUnitCountArgs(TestUnitBase): def _test(self, *args, **kwargs): From 9fee0e87cc80536524422867c5e8b0e22a2aafa4 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 1 Sep 2023 07:21:21 +0900 Subject: [PATCH 07/14] test(_comp_get_first_arg): add unit tests --- test/t/unit/Makefile.am | 3 +- test/t/unit/test_unit_get_first_arg.py | 50 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/t/unit/test_unit_get_first_arg.py diff --git a/test/t/unit/Makefile.am b/test/t/unit/Makefile.am index e494396719c..fa14d15647d 100644 --- a/test/t/unit/Makefile.am +++ b/test/t/unit/Makefile.am @@ -12,8 +12,9 @@ EXTRA_DIST = \ test_unit_expand_tilde_by_ref.py \ test_unit_filedir.py \ test_unit_find_unique_completion_pair.py \ - test_unit_get_words.py \ + test_unit_get_first_arg.py \ test_unit_get_cword.py \ + test_unit_get_words.py \ test_unit_initialize.py \ test_unit_ip_addresses.py \ test_unit_known_hosts_real.py \ diff --git a/test/t/unit/test_unit_get_first_arg.py b/test/t/unit/test_unit_get_first_arg.py new file mode 100644 index 00000000000..b13153e85eb --- /dev/null +++ b/test/t/unit/test_unit_get_first_arg.py @@ -0,0 +1,50 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitGetFirstArg: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_unit() { local -a "words=$1"; local cword=$2 arg=; shift 2; _comp_get_first_arg "$@" && printf "%s\\n" "$arg"; return 0; }', + ) + + def test_1(self, bash, functions): + assert_bash_exec(bash, "_comp__test_unit '()' 0") + + def test_2(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_unit "(a b)" 2', want_output=None + ).strip() + assert output == "b" + + def test_3(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_unit "(a bc)" 2', want_output=None + ).strip() + assert output == "bc" + + def test_4(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_unit "(a b c)" 2', want_output=None + ).strip() + assert output == "b" + + def test_5(self, bash, functions): + """Neither of the current word and the command name should be picked + as the first argument""" + output = assert_bash_exec( + bash, '_comp__test_unit "(a b c)" 1', want_output=None + ).strip() + assert output == "" + + def test_6(self, bash, functions): + """Options starting with - should not be picked as the first + argument""" + output = assert_bash_exec( + bash, '_comp__test_unit "(a -b -c d e)" 4', want_output=None + ).strip() + assert output == "d" From e23a79e0fd715b0ae564082d9f1e4e7a907a195d Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 19:30:32 +0900 Subject: [PATCH 08/14] fix(_comp_{first_arg,count_args}): count - as argument --- bash_completion | 4 ++-- .../000_bash_completion_compat.bash | 17 +++++++++++++---- test/t/unit/test_unit_count_args.py | 12 ++++++++++++ test/t/unit/test_unit_get_first_arg.py | 7 +++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/bash_completion b/bash_completion index 9433541584c..a71dd69a0df 100644 --- a/bash_completion +++ b/bash_completion @@ -2165,7 +2165,7 @@ _comp_get_first_arg() arg= for ((i = 1; i < cword; i++)); do - if [[ ${words[i]} != -* ]]; then + if [[ ${words[i]} != -?* ]]; then arg=${words[i]} break fi @@ -2187,7 +2187,7 @@ _comp_count_args() ret=1 for ((i = 1; i < cword; i++)); do # shellcheck disable=SC2053 - if [[ ${words[i]} != -* && ${words[i - 1]} != ${2-} || + if [[ ${words[i]} != -?* && ${words[i - 1]} != ${2-} || ${words[i]} == ${3-} ]]; then ((ret++)) fi diff --git a/bash_completion.d/000_bash_completion_compat.bash b/bash_completion.d/000_bash_completion_compat.bash index 3e4318e7af1..04fb6e7a9b0 100644 --- a/bash_completion.d/000_bash_completion_compat.bash +++ b/bash_completion.d/000_bash_completion_compat.bash @@ -423,12 +423,21 @@ _get_first_arg() # @var[out] args Return the number of arguments # @deprecated 2.12 Use `_comp_count_args`. Note that the new function # `_comp_count_args` returns the result in variable `ret` instead of `args`. +# In the new function, `-` is also counted as an argument. +# shellcheck disable=SC2178 # assignments are not intended for global "args" _count_args() { - local ret - _comp_count_args "$@" - # shellcheck disable=SC2178 - args=$ret + local i cword words + _comp__reassemble_words "${1-}" words cword + + args=1 + for ((i = 1; i < cword; i++)); do + # shellcheck disable=SC2053 + if [[ ${words[i]} != -* && ${words[i - 1]} != ${2-} || + ${words[i]} == ${3-} ]]; then + ((args++)) + fi + done } # ex: filetype=sh diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index bdaed672ac2..e942a67c64f 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -66,3 +66,15 @@ def test_9(self, bash): bash, "(a -b -c d e)", 4, "a -b -c d e", 11, arg='"" "" "-b"' ) assert output == "3" + + def test_10_single_hyphen_1(self, bash): + """- should be counted as an argument representing stdout/stdin""" + output = self._test(bash, "(a -b - c -d e)", 5, "a -b - c -d e", 12) + assert output == "3" + + def test_10_single_hyphen_2(self, bash): + """- in an option argument should be skipped""" + output = self._test( + bash, "(a -b - c - e)", 5, "a -b - c - e", 11, arg='"" "-b"' + ) + assert output == "3" diff --git a/test/t/unit/test_unit_get_first_arg.py b/test/t/unit/test_unit_get_first_arg.py index b13153e85eb..9a4245d212e 100644 --- a/test/t/unit/test_unit_get_first_arg.py +++ b/test/t/unit/test_unit_get_first_arg.py @@ -48,3 +48,10 @@ def test_6(self, bash, functions): bash, '_comp__test_unit "(a -b -c d e)" 4', want_output=None ).strip() assert output == "d" + + def test_7_single_hyphen(self, bash, functions): + """- should be counted as an argument representing stdout/stdin""" + output = assert_bash_exec( + bash, '_comp__test_unit "(a -b - c -d e)" 5', want_output=None + ).strip() + assert output == "-" From 9bfd760c1192da7b3d0d8f0f9c8bea8ed16f4e47 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 19:47:40 +0900 Subject: [PATCH 09/14] fix(_comp_{first_arg,count_args}): count any arguments after -- --- bash_completion | 6 ++++++ bash_completion.d/000_bash_completion_compat.bash | 6 ++++-- test/t/unit/test_unit_count_args.py | 12 ++++++++++++ test/t/unit/test_unit_get_first_arg.py | 14 ++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/bash_completion b/bash_completion index a71dd69a0df..63b63a042df 100644 --- a/bash_completion +++ b/bash_completion @@ -2168,6 +2168,9 @@ _comp_get_first_arg() if [[ ${words[i]} != -?* ]]; then arg=${words[i]} break + elif [[ ${words[i]} == -- ]]; then + ((i + 1 < cword)) && arg=${words[i + 1]} + break fi done } @@ -2190,6 +2193,9 @@ _comp_count_args() if [[ ${words[i]} != -?* && ${words[i - 1]} != ${2-} || ${words[i]} == ${3-} ]]; then ((ret++)) + elif [[ ${words[i]} == -- ]]; then + ((ret += cword - i - 1)) + break fi done } diff --git a/bash_completion.d/000_bash_completion_compat.bash b/bash_completion.d/000_bash_completion_compat.bash index 04fb6e7a9b0..715f6e7f141 100644 --- a/bash_completion.d/000_bash_completion_compat.bash +++ b/bash_completion.d/000_bash_completion_compat.bash @@ -401,7 +401,8 @@ _fstypes() # This function returns the first argument, excluding options # @deprecated 2.12 Use `_comp_get_first_arg`. Note that the new function # `_comp_get_first_arg` operates on `words` and `cword` instead of `COMP_WORDS` -# and `COMP_CWORD`. +# and `COMP_CWORD`. The new function considers a command-line argument after +# `--` as an argument. _get_first_arg() { local i @@ -423,7 +424,8 @@ _get_first_arg() # @var[out] args Return the number of arguments # @deprecated 2.12 Use `_comp_count_args`. Note that the new function # `_comp_count_args` returns the result in variable `ret` instead of `args`. -# In the new function, `-` is also counted as an argument. +# In the new function, `-` is also counted as an argument. The new function +# counts all the arguments after `--`. # shellcheck disable=SC2178 # assignments are not intended for global "args" _count_args() { diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index e942a67c64f..317db8bd196 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -78,3 +78,15 @@ def test_10_single_hyphen_2(self, bash): bash, "(a -b - c - e)", 5, "a -b - c - e", 11, arg='"" "-b"' ) assert output == "3" + + def test_11_double_hyphen_1(self, bash): + """all the words after -- should be counted""" + output = self._test( + bash, "(a -b -- -c -d e)", 5, "a -b -- -c -d e", 14 + ) + assert output == "3" + + def test_11_double_hyphen_2(self, bash): + """all the words after -- should be counted""" + output = self._test(bash, "(a b -- -c -d e)", 5, "a b -- -c -d e", 13) + assert output == "4" diff --git a/test/t/unit/test_unit_get_first_arg.py b/test/t/unit/test_unit_get_first_arg.py index 9a4245d212e..f03c2bb3543 100644 --- a/test/t/unit/test_unit_get_first_arg.py +++ b/test/t/unit/test_unit_get_first_arg.py @@ -55,3 +55,17 @@ def test_7_single_hyphen(self, bash, functions): bash, '_comp__test_unit "(a -b - c -d e)" 5', want_output=None ).strip() assert output == "-" + + def test_8_double_hyphen_1(self, bash, functions): + """any word after -- should be picked""" + output = assert_bash_exec( + bash, '_comp__test_unit "(a -b -- -c -d e)" 5', want_output=None + ).strip() + assert output == "-c" + + def test_8_double_hyphen_2(self, bash, functions): + """any word after -- should be picked only without any preceding argument""" + output = assert_bash_exec( + bash, '_comp__test_unit "(a b -- -c -d e)" 5', want_output=None + ).strip() + assert output == "b" From 21d3122fa933169849e9743212248c80b7843abb Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 20:17:45 +0900 Subject: [PATCH 10/14] fix(_comp_count_args): perform optarg check also on $3 The check that unconditionally accepts a word by a pattern has been added by commits 75ec298e and 3809d955. However, even if the word matches the specified pattern, if the word is an option argument of the previous option, it should not be counted as an argument. This patch changes the behavior so that it does not treat the word matching $3 as an argument when it is an option argument. The current use case of $3 is only by chmod, where $2 is unspecified, so the behavior is unaffected by this change. --- bash_completion | 4 ++-- test/t/unit/test_unit_count_args.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/bash_completion b/bash_completion index 63b63a042df..6d93105bd26 100644 --- a/bash_completion +++ b/bash_completion @@ -2190,8 +2190,8 @@ _comp_count_args() ret=1 for ((i = 1; i < cword; i++)); do # shellcheck disable=SC2053 - if [[ ${words[i]} != -?* && ${words[i - 1]} != ${2-} || - ${words[i]} == ${3-} ]]; then + if [[ (${words[i]} != -?* || ${words[i]} == ${3-}) && + ${words[i - 1]} != ${2-} ]]; then ((ret++)) elif [[ ${words[i]} == -- ]]; then ((ret += cword - i - 1)) diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 317db8bd196..53ae85bd97a 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -90,3 +90,24 @@ def test_11_double_hyphen_2(self, bash): """all the words after -- should be counted""" output = self._test(bash, "(a b -- -c -d e)", 5, "a b -- -c -d e", 13) assert output == "4" + + def test_12_exclude_optarg_1(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, "(a -o -x b c)", 4, "a -o -x b c", 10, arg='"" "-o" "-x"' + ) + assert output == "2" + + def test_12_exclude_optarg_2(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, "(a -o -x -x c)", 4, "a -o -x -x c", 11, arg='"" "-o" "-x"' + ) + assert output == "2" + + def test_12_exclude_optarg_3(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, "(a -o -x -y c)", 4, "a -o -x -y c", 11, arg='"" "-o" "-x"' + ) + assert output == "1" From 55d1e080d909c790cdbb4eb4828eec1064889581 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 20:33:40 +0900 Subject: [PATCH 11/14] refactor(_comp_get_first_arg): return result via `ret` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ville Skyttä --- bash_completion | 9 +++--- .../000_bash_completion_compat.bash | 3 +- completions/cryptsetup | 4 +-- completions/hcitool | 30 +++++++++---------- test/t/unit/test_unit_get_first_arg.py | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bash_completion b/bash_completion index 6d93105bd26..fddcc0dffc9 100644 --- a/bash_completion +++ b/bash_completion @@ -2157,19 +2157,20 @@ _comp_realcommand() } # This function returns the first argument, excluding options -# @var[out] arg First argument if any, or otherwise an empty string +# @var[out] ret First argument before current being completed if any, or +# otherwise an empty string # @since 2.12 _comp_get_first_arg() { local i - arg= + ret= for ((i = 1; i < cword; i++)); do if [[ ${words[i]} != -?* ]]; then - arg=${words[i]} + ret=${words[i]} break elif [[ ${words[i]} == -- ]]; then - ((i + 1 < cword)) && arg=${words[i + 1]} + ((i + 1 < cword)) && ret=${words[i + 1]} break fi done diff --git a/bash_completion.d/000_bash_completion_compat.bash b/bash_completion.d/000_bash_completion_compat.bash index 715f6e7f141..fb32ae1211f 100644 --- a/bash_completion.d/000_bash_completion_compat.bash +++ b/bash_completion.d/000_bash_completion_compat.bash @@ -402,7 +402,8 @@ _fstypes() # @deprecated 2.12 Use `_comp_get_first_arg`. Note that the new function # `_comp_get_first_arg` operates on `words` and `cword` instead of `COMP_WORDS` # and `COMP_CWORD`. The new function considers a command-line argument after -# `--` as an argument. +# `--` as an argument. The new function returns the result in variable `ret` +# instead of `arg`. _get_first_arg() { local i diff --git a/completions/cryptsetup b/completions/cryptsetup index 1c6fb090d38..0f4e781830d 100644 --- a/completions/cryptsetup +++ b/completions/cryptsetup @@ -34,8 +34,9 @@ _comp_cmd_cryptsetup() [[ $was_split ]] && return - local arg + local ret _comp_get_first_arg + local arg=$ret if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help @@ -47,7 +48,6 @@ _comp_cmd_cryptsetup() luksResume luksHeaderBackup luksHeaderRestore' fi else - local ret _comp_count_args "" "-${noargopts}[chslSbopitTdM]" local args=$ret case $arg in diff --git a/completions/hcitool b/completions/hcitool index 828114fb40b..fdca0780a55 100644 --- a/completions/hcitool +++ b/completions/hcitool @@ -45,9 +45,9 @@ _comp_cmd_hcitool() [[ $was_split ]] && return - local arg + local ret _comp_get_first_arg - if [[ ! $arg ]]; then + if [[ ! $ret ]]; then if [[ $cur == -* ]]; then _comp_compgen_help else @@ -55,8 +55,7 @@ _comp_cmd_hcitool() dc sr cpt rssi lq tpl afh lst auth enc key clkoff clock' fi else - local ret - case $arg in + case $ret in name | info | dc | rssi | lq | afh | auth | key | clkoff | lst) _comp_count_args if ((ret == 2)); then @@ -116,9 +115,9 @@ _comp_cmd_sdptool() [[ $was_split ]] && return - local arg + local ret _comp_get_first_arg - if [[ ! $arg ]]; then + if [[ ! $ret ]]; then if [[ $cur == -* ]]; then _comp_compgen_help else @@ -126,7 +125,7 @@ _comp_cmd_sdptool() setseq' fi else - case $arg in + case $ret in search) if [[ $cur == -* ]]; then _comp_compgen -- -W '--bdaddr --tree --raw --xml' @@ -198,8 +197,9 @@ _comp_cmd_rfcomm() ;; esac - local arg + local ret _comp_get_first_arg + local arg=$ret if [[ ! $arg ]]; then if [[ $cur == -* ]]; then _comp_compgen_help @@ -207,7 +207,6 @@ _comp_cmd_rfcomm() _comp_compgen -- -W 'show connect listen watch bind release' fi else - local ret _comp_count_args local args=$ret if ((args == 2)); then @@ -238,16 +237,16 @@ _comp_cmd_ciptool() ;; esac - local arg + local ret _comp_get_first_arg - if [[ ! $arg ]]; then + if [[ ! $ret ]]; then if [[ $cur == -* ]]; then _comp_compgen_help else _comp_compgen -- -W 'show search connect release loopback' fi else - case $arg in + case $ret in connect | release | loopback) local ret _comp_count_args @@ -294,9 +293,9 @@ _comp_cmd_hciconfig() local cur prev words cword comp_args _comp_initialize -- "$@" || return - local arg + local ret _comp_get_first_arg - if [[ ! $arg ]]; then + if [[ ! $ret ]]; then if [[ $cur == -* ]]; then _comp_compgen -- -W '--help --all' else @@ -307,8 +306,7 @@ _comp_cmd_hciconfig() version revision lm' fi else - local ret - case $arg in + case $ret in putkey | delkey) _comp_count_args if ((ret == 2)); then diff --git a/test/t/unit/test_unit_get_first_arg.py b/test/t/unit/test_unit_get_first_arg.py index f03c2bb3543..78186c4a6c7 100644 --- a/test/t/unit/test_unit_get_first_arg.py +++ b/test/t/unit/test_unit_get_first_arg.py @@ -9,7 +9,7 @@ class TestUnitGetFirstArg: def functions(self, bash): assert_bash_exec( bash, - '_comp__test_unit() { local -a "words=$1"; local cword=$2 arg=; shift 2; _comp_get_first_arg "$@" && printf "%s\\n" "$arg"; return 0; }', + '_comp__test_unit() { local -a "words=$1"; local cword=$2 ret=; shift 2; _comp_get_first_arg "$@" && printf "%s\\n" "$ret"; return 0; }', ) def test_1(self, bash, functions): From 503e3a91cbee4d64b51d967319f3ccef2d6adcf6 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 11 Aug 2023 20:49:19 +0900 Subject: [PATCH 12/14] refactor(_comp_get_first_arg): return exit status The current interface does not distinguish the case where no argument is found and the case where the first argument is an empty string. This patch returns whether the first argument is found through the exit status. This also modifies the callers so that they use the exit status to check if the first argument is specified. --- bash_completion | 6 ++- completions/cryptsetup | 25 ++++++------ completions/hcitool | 89 ++++++++++++++++++++---------------------- 3 files changed, 58 insertions(+), 62 deletions(-) diff --git a/bash_completion b/bash_completion index fddcc0dffc9..91450688726 100644 --- a/bash_completion +++ b/bash_completion @@ -2159,6 +2159,7 @@ _comp_realcommand() # This function returns the first argument, excluding options # @var[out] ret First argument before current being completed if any, or # otherwise an empty string +# @return True (0) if any argument is found, False (> 0) otherwise. # @since 2.12 _comp_get_first_arg() { @@ -2168,12 +2169,13 @@ _comp_get_first_arg() for ((i = 1; i < cword; i++)); do if [[ ${words[i]} != -?* ]]; then ret=${words[i]} - break + return 0 elif [[ ${words[i]} == -- ]]; then - ((i + 1 < cword)) && ret=${words[i + 1]} + ((i + 1 < cword)) && ret=${words[i + 1]} && return 0 break fi done + return 1 } # This function counts the number of args, excluding options diff --git a/completions/cryptsetup b/completions/cryptsetup index 0f4e781830d..f7e6675c9fd 100644 --- a/completions/cryptsetup +++ b/completions/cryptsetup @@ -35,19 +35,8 @@ _comp_cmd_cryptsetup() [[ $was_split ]] && return local ret - _comp_get_first_arg - local arg=$ret - if [[ ! $arg ]]; then - if [[ $cur == -* ]]; then - _comp_compgen_help - [[ ${COMPREPLY-} == *= ]] && compopt -o nospace - else - _comp_compgen -- -W 'open close resize status benchmark repair - erase luksFormat luksAddKey luksRemoveKey luksChangeKey - luksKillSlot luksUUID isLuks luksDump tcryptDump luksSuspend - luksResume luksHeaderBackup luksHeaderRestore' - fi - else + if _comp_get_first_arg; then + local arg=$ret _comp_count_args "" "-${noargopts}[chslSbopitTdM]" local args=$ret case $arg in @@ -97,6 +86,16 @@ _comp_cmd_cryptsetup() esac ;; esac + else + if [[ $cur == -* ]]; then + _comp_compgen_help + [[ ${COMPREPLY-} == *= ]] && compopt -o nospace + else + _comp_compgen -- -W 'open close resize status benchmark repair + erase luksFormat luksAddKey luksRemoveKey luksChangeKey + luksKillSlot luksUUID isLuks luksDump tcryptDump luksSuspend + luksResume luksHeaderBackup luksHeaderRestore' + fi fi } && diff --git a/completions/hcitool b/completions/hcitool index fdca0780a55..ce5b35d7261 100644 --- a/completions/hcitool +++ b/completions/hcitool @@ -46,15 +46,7 @@ _comp_cmd_hcitool() [[ $was_split ]] && return local ret - _comp_get_first_arg - if [[ ! $ret ]]; then - if [[ $cur == -* ]]; then - _comp_compgen_help - else - _comp_compgen -- -W 'dev inq scan name info spinq epinq cmd con cc - dc sr cpt rssi lq tpl afh lst auth enc key clkoff clock' - fi - else + if _comp_get_first_arg; then case $ret in name | info | dc | rssi | lq | afh | auth | key | clkoff | lst) _comp_count_args @@ -97,6 +89,13 @@ _comp_cmd_hcitool() fi ;; esac + else + if [[ $cur == -* ]]; then + _comp_compgen_help + else + _comp_compgen -- -W 'dev inq scan name info spinq epinq cmd con cc + dc sr cpt rssi lq tpl afh lst auth enc key clkoff clock' + fi fi } && complete -F _comp_cmd_hcitool hcitool @@ -116,15 +115,7 @@ _comp_cmd_sdptool() [[ $was_split ]] && return local ret - _comp_get_first_arg - if [[ ! $ret ]]; then - if [[ $cur == -* ]]; then - _comp_compgen_help - else - _comp_compgen -- -W 'search browse records add del get setattr - setseq' - fi - else + if _comp_get_first_arg; then case $ret in search) if [[ $cur == -* ]]; then @@ -153,6 +144,13 @@ _comp_cmd_sdptool() fi ;; esac + else + if [[ $cur == -* ]]; then + _comp_compgen_help + else + _comp_compgen -- -W 'search browse records add del get setattr + setseq' + fi fi } && complete -F _comp_cmd_sdptool sdptool @@ -198,15 +196,8 @@ _comp_cmd_rfcomm() esac local ret - _comp_get_first_arg - local arg=$ret - if [[ ! $arg ]]; then - if [[ $cur == -* ]]; then - _comp_compgen_help - else - _comp_compgen -- -W 'show connect listen watch bind release' - fi - else + if _comp_get_first_arg; then + local arg=$ret _comp_count_args local args=$ret if ((args == 2)); then @@ -220,6 +211,12 @@ _comp_cmd_rfcomm() ;; esac fi + else + if [[ $cur == -* ]]; then + _comp_compgen_help + else + _comp_compgen -- -W 'show connect listen watch bind release' + fi fi } && complete -F _comp_cmd_rfcomm rfcomm @@ -238,14 +235,7 @@ _comp_cmd_ciptool() esac local ret - _comp_get_first_arg - if [[ ! $ret ]]; then - if [[ $cur == -* ]]; then - _comp_compgen_help - else - _comp_compgen -- -W 'show search connect release loopback' - fi - else + if _comp_get_first_arg; then case $ret in connect | release | loopback) local ret @@ -255,6 +245,12 @@ _comp_cmd_ciptool() fi ;; esac + else + if [[ $cur == -* ]]; then + _comp_compgen_help + else + _comp_compgen -- -W 'show search connect release loopback' + fi fi } && complete -F _comp_cmd_ciptool ciptool @@ -294,18 +290,7 @@ _comp_cmd_hciconfig() _comp_initialize -- "$@" || return local ret - _comp_get_first_arg - if [[ ! $ret ]]; then - if [[ $cur == -* ]]; then - _comp_compgen -- -W '--help --all' - else - _comp_compgen -- -W 'up down reset rstat auth noauth encrypt - noencrypt secmgr nosecmgr piscan noscan iscan pscan ptype name - class voice iac inqmode inqdata inqtype inqparams pageparms - pageto afhmode aclmtu scomtu putkey delkey commands features - version revision lm' - fi - else + if _comp_get_first_arg; then case $ret in putkey | delkey) _comp_count_args @@ -326,6 +311,16 @@ _comp_cmd_hciconfig() fi ;; esac + else + if [[ $cur == -* ]]; then + _comp_compgen -- -W '--help --all' + else + _comp_compgen -- -W 'up down reset rstat auth noauth encrypt + noencrypt secmgr nosecmgr piscan noscan iscan pscan ptype name + class voice iac inqmode inqdata inqtype inqparams pageparms + pageto afhmode aclmtu scomtu putkey delkey commands features + version revision lm' + fi fi } && complete -F _comp_cmd_hciconfig hciconfig From 76eea74581d4f404e817f686ea86236c8c75a9fc Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 12 Aug 2023 06:12:20 +0900 Subject: [PATCH 13/14] fix(_comp_count_args): ignore empty $3 --- bash_completion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bash_completion b/bash_completion index 91450688726..a0f65954b1f 100644 --- a/bash_completion +++ b/bash_completion @@ -2193,7 +2193,7 @@ _comp_count_args() ret=1 for ((i = 1; i < cword; i++)); do # shellcheck disable=SC2053 - if [[ (${words[i]} != -?* || ${words[i]} == ${3-}) && + if [[ (${words[i]} != -?* || ${3-} && ${words[i]} == ${3-}) && ${words[i - 1]} != ${2-} ]]; then ((ret++)) elif [[ ${words[i]} == -- ]]; then From 874c5031986e23a3ca7843d216d7dde94b0f1d4b Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 12 Aug 2023 12:34:08 +0900 Subject: [PATCH 14/14] fix(_comp_count_args): check optarg correctly When the current implementation checks an option argument, it tests whether the previous word matches $2 (i.e., a pattern of options taking an option argument). This implementation has multiple issues: * When the options taking an option argument does not start with `-`, the option is counted as an independent argument. For example, `ssh` completion passes `@(-c|[-+]o)` as $2, but `+o` is counted as an argument with the current implementation. * An option argument that looks like an option taking an option argument can prevent the next word counted as an argument. For example, when `cmd -o -o arg` is processed with $2 being `-o`, the second `-o` should be treated as an option argument and `arg` should be counted as an argument. However, with the current implementation, `arg` is not counted because the previous word `-o` matches the pattern. * When `cmd -o -- -x` is processed with $2 being `-o`, `--` should be treated as an option argument so should lose its special meaning, and `-x` is still treated as an option. However, with the current implementation, `--` does not lose its special meaning, so `-x` is unexpectedly treated as an argument. * Also, with the current implementation, when $2 is not specified, an argument after an empty word is not counted as an argument because $2 matches the empty word, (though Readline usually do not store an empty word in COMP_WORDS). This patch fixes those issues by changing how options taking an option argument are processed. --- bash_completion | 5 ++-- test/t/unit/test_unit_count_args.py | 38 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/bash_completion b/bash_completion index a0f65954b1f..38578f4bd0d 100644 --- a/bash_completion +++ b/bash_completion @@ -2193,8 +2193,9 @@ _comp_count_args() ret=1 for ((i = 1; i < cword; i++)); do # shellcheck disable=SC2053 - if [[ (${words[i]} != -?* || ${3-} && ${words[i]} == ${3-}) && - ${words[i - 1]} != ${2-} ]]; then + if [[ ${2-} && ${words[i]} == ${2-} ]]; then + ((i++)) + elif [[ ${words[i]} != -?* || ${3-} && ${words[i]} == ${3-} ]]; then ((ret++)) elif [[ ${words[i]} == -- ]]; then ((ret += cword - i - 1)) diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 53ae85bd97a..064041bf615 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -111,3 +111,41 @@ def test_12_exclude_optarg_3(self, bash): bash, "(a -o -x -y c)", 4, "a -o -x -y c", 11, arg='"" "-o" "-x"' ) assert output == "1" + + def test_13_plus_option_optarg(self, bash): + """When +o is specified to be an option taking an option argument, it should not be counted as an argument""" + output = self._test( + bash, "(a +o b c)", 3, "a +o b c", 7, arg='"" "+o"' + ) + assert output == "1" + + def test_14_no_optarg_chain_1(self, bash): + """an option argument should not take another option argument""" + output = self._test( + bash, "(a -o -o -o -o c)", 5, "a -o -o -o -o c", 14, arg='"" "-o"' + ) + assert output == "1" + + def test_14_no_optarg_chain_2(self, bash): + """an option argument should not take another option argument""" + output = self._test( + bash, + "(a -o -o b -o -o c)", + 6, + "a -o -o b -o -o c", + 16, + arg='"" "-o"', + ) + assert output == "2" + + def test_15_double_hyphen_optarg(self, bash): + """-- should lose its meaning when it is an option argument""" + output = self._test( + bash, "(a -o -- -b -c d)", 5, "a -o -- -b -c d", 14, arg='"" "-o"' + ) + assert output == "1" + + def test_16_empty_word(self, bash): + """An empty word should not take an option argument""" + output = self._test(bash, "(a '' x '' y d)", 5, "a x y d", 8) + assert output == "5"