From 743d0a9be714c516dce23415e3a5c5f81d5d7bea Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sun, 13 Aug 2023 14:59:42 +0900 Subject: [PATCH 1/8] fix(_known_hosts): use array for `options` (work around SC2178,SC2179) shellcheck does not allow using a local scalar variable that has the same variable name as an array variable used in another function (SC2178,SC2179). To use array `options` in another function, we switch `options` in `_known_hosts` to an array variable (or otherwise, we will need to place disable=SC2178,SC2179 to every line using the variable as a scalar). --- bash_completion | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bash_completion b/bash_completion index 162163abcbf..0ea86e889fd 100644 --- a/bash_completion +++ b/bash_completion @@ -2265,11 +2265,11 @@ _known_hosts() # NOTE: Using `_known_hosts' as a helper function and passing options # to `_known_hosts' is deprecated: Use `_known_hosts_real' instead. - local options="" - [[ ${1-} == -a || ${2-} == -a ]] && options=-a - [[ ${1-} == -c || ${2-} == -c ]] && options+=" -c" - # shellcheck disable=SC2086 - _known_hosts_real ${options-} -- "$cur" + local -a options=() + [[ ${1-} == -a || ${2-} == -a ]] && options+=(-a) + [[ ${1-} == -c || ${2-} == -c ]] && options+=(-c) + local IFS=$' \t\n' # Workaround for connected ${v+"$@"} in bash < 4.4 + _known_hosts_real ${options[@]+"${options[@]}"} -- "$cur" } # _known_hosts() # Helper function to locate ssh included files in configs From 201239cb597a3d7d0165a5e37a093f063fe700d1 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sun, 13 Aug 2023 15:05:29 +0900 Subject: [PATCH 2/8] fix(_get_cword_at_cursor,cvs): quote array length --- bash_completion | 2 +- completions/cvs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bash_completion b/bash_completion index 0ea86e889fd..0ec10c8b64c 100644 --- a/bash_completion +++ b/bash_completion @@ -838,7 +838,7 @@ _comp__get_cword_at_cursor() fi local IFS=$' \t\n' - local "$2" "$3" "$4" && _comp_upvars -a${#words[@]} "$2" ${words[@]+"${words[@]}"} \ + local "$2" "$3" "$4" && _comp_upvars -a"${#words[@]}" "$2" ${words[@]+"${words[@]}"} \ -v "$3" "$cword" -v "$4" "${cur:0:index}" } diff --git a/completions/cvs b/completions/cvs index a3a7585846d..9eb434ad22c 100644 --- a/completions/cvs +++ b/completions/cvs @@ -43,7 +43,7 @@ _comp_xfunc_cvs_compgen_roots() local -a cvsroots=() [[ -v CVSROOT ]] && cvsroots=("$CVSROOT") [[ -r ~/.cvspass ]] && cvsroots+=($(awk '{ print $2 }' ~/.cvspass)) - [[ -r CVS/Root ]] && mapfile -tO ${#cvsroots[@]} cvsroots Date: Mon, 7 Aug 2023 00:36:59 +0900 Subject: [PATCH 3/8] feat(_comp_compgen): support `-U var` to unlocal var When `-U var` is specified for a builtin `compgen` call, variable `var` is unlocalized by `_comp_unlocal` just before storing the result to the target array. When `-U var` is specified for a generator call, variable `var` is unlocalized by `_comp_unlocal` just before calling the generator function `_comp_compgen_G2` (where `G2` is the generator name).. A generator should basically define local variables with the names starting with `_`. However, a generator sometimes needs to use local variable names that do not start with `_`. When the child generator call with a variable name (such as `local var; _comp_compgen -v var`) is used within the generator, the local variable can unexpectedly mask a local variable of the upper call. For example, the following call fails to obtain the result of generator `mygen1` because the array `arr` is masked by the same name of a local variable in `_comp_compgen_mygen1`. # generator with a problem _comp_compgen_mygen1() { local -a arr=(1 2 3) _comp_compgen -av arr -- -W '4 5 6' _comp_compgen_set "${arr[@]/#p}" } _comp_compgen -v arr mygen1 # fails to get the result in array `arr` To avoid this, a generator that defines a local variable that does not start with `_` can use the option `-U var` to unlocalize the variable on assigning the final result. # properly designed generator _comp_compgen_mygen1() { local -a arr=(1 2 3) _comp_compgen -av arr -- -W '4 5 6' _comp_compgen -U arr set "${arr[@]/#p}" } --- bash_completion | 149 ++++++++++++++++++------------- doc/api-and-naming.md | 44 ++++++++- test/t/unit/test_unit_compgen.py | 12 +++ 3 files changed, 141 insertions(+), 64 deletions(-) diff --git a/bash_completion b/bash_completion index 0ec10c8b64c..00e3b6bf7b1 100644 --- a/bash_completion +++ b/bash_completion @@ -422,6 +422,20 @@ _comp_compgen__error_fallback() # The array name should not start with an underscores "_", which is # internally used. The array name should not be either "IFS" or # "OPT{IND,ARG,ERR}". +# -U var Unlocalize VAR before performing the assignments. This option can +# be specified multiple times to register multiple variables. This +# option is supposed to be used in implementing a generator (G1) when +# G1 defines a local variable name that does not start with `_`. In +# such a case, when the target variable specified to G1 by `-v VAR1` +# conflicts with the local variable, the assignment to the target +# variable fails to propagate outside G1. To avoid such a situation, +# G1 can call `_comp_compgen` with `-U VAR` to unlocalize `VAR` +# before accessing the target variable. For a builtin compgen call +# (i.e., _comp_compgen [options] -- options), VAR is unlocalized +# after calling the builtin `compgen` but before assigning results to +# the target array. For a generator call (i.e., _comp_compgen +# [options] G2 ...), VAR is unlocalized before calling the child +# generator function `_comp_compgen_G2`. # -c cur Set a word used as a prefix to filter the completions. The default # is ${cur-}. # -R The same as -c ''. Use raw outputs without filtering. @@ -512,6 +526,7 @@ _comp_compgen() local _dir="" local _ifs=$' \t\n' _has_ifs="" local _icmd="" _xcmd="" + local -a _upvars=() local _old_nocasematch="" if shopt -q nocasematch; then @@ -519,7 +534,7 @@ _comp_compgen() shopt -u nocasematch fi local OPTIND=1 OPTARG="" OPTERR=0 _opt - while getopts ':av:Rc:C:lF:i:x:' _opt "$@"; do + while getopts ':av:U:Rc:C:lF:i:x:' _opt "$@"; do case $_opt in a) _append=set ;; v) @@ -529,6 +544,16 @@ _comp_compgen() fi _var=$OPTARG ;; + U) + if [[ $OPTARG == @(*[^_a-zA-Z0-9]*|[0-9]*|'') ]]; then + printf 'bash_completion: %s: -U: invalid variable name `%s'\''\n' "$FUNCNAME" "$OPTARG" >&2 + return 2 + elif [[ $OPTARG == @(_*|IFS|OPTIND|OPTARG|OPTERR) ]]; then + printf 'bash_completion: %s: -U: unnecessary to mark `%s'\'' as upvar\n' "$FUNCNAME" "$OPTARG" >&2 + return 2 + fi + _upvars+=("$OPTARG") + ;; c) _cur=$OPTARG ;; R) _cur="" ;; C) @@ -588,6 +613,8 @@ _comp_compgen() return 2 fi + ((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}" + if [[ $_dir ]]; then local _original_pwd=$PWD local PWD=${PWD-} OLDPWD=${OLDPWD-} @@ -653,6 +680,7 @@ _comp_compgen() return } + ((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}" _comp_split -l ${_append:+-a} "$_var" "$_result" } @@ -711,8 +739,8 @@ _comp_compgen_split() return 2 fi - local _split_input=$1 IFS=$' \t\n' - _comp_compgen -F "$_ifs" -- ${_compgen_options[@]+"${_compgen_options[@]}"} -W '$_split_input' + local input=$1 IFS=$' \t\n' + _comp_compgen -F "$_ifs" -U input -- ${_compgen_options[@]+"${_compgen_options[@]}"} -W '$input' } # Check if the argument looks like a path. @@ -959,14 +987,14 @@ _comp_get_words() _comp_compgen_ltrim_colon() { (($#)) || return 0 - local -a tmp - tmp=("$@") + local -a _tmp + _tmp=("$@") if [[ $cur == *:* && $COMP_WORDBREAKS == *:* ]]; then # Remove colon-word prefix from items - local colon_word=${cur%"${cur##*:}"} - tmp=("${tmp[@]#"$colon_word"}") + local _colon_word=${cur%"${cur##*:}"} + _tmp=("${_tmp[@]#"$_colon_word"}") fi - _comp_compgen -R -- -W '"${tmp[@]}"' + _comp_compgen_set "${_tmp[@]}" } # If the word-to-complete contains a colon (:), left-trim COMPREPLY items with @@ -1078,9 +1106,7 @@ _comp_compgen_filedir() # Note: bash < 4.4 has a bug that all the elements are connected with # ${v+"${a[@]}"} when IFS does not contain whitespace. local IFS=$' \t\n' - local -a _tmp=(${toks[@]+"${toks[@]}"}) - _comp_unlocal toks - _comp_compgen_set ${_tmp[@]+"${_tmp[@]}"} + _comp_compgen -U toks set ${toks[@]+"${toks[@]}"} } # _comp_compgen_filedir() # This function splits $cur=--foo=bar into $prev=--foo, $cur=bar, making it @@ -1413,7 +1439,7 @@ _comp_compgen_help__get_help_lines() } # Helper function for _comp_compgen_help and _comp_compgen_usage. -# @var[in,out] _options Add options +# @var[in,out] options Add options # @return True (0) if an option was found, False (> 0) otherwise _comp_compgen_help__parse() { @@ -1441,12 +1467,12 @@ _comp_compgen_help__parse() if [[ $option =~ (\[((no|dont)-?)\]). ]]; then option2=${option/"${BASH_REMATCH[1]}"/} option2=${option2%%[<{().[]*} - _options+=("${option2/=*/=}") + options+=("${option2/=*/=}") option=${option/"${BASH_REMATCH[1]}"/"${BASH_REMATCH[2]}"} fi [[ $option =~ ^([^=<{().[]|\.[A-Za-z0-9])+=? ]] && - _options+=("$BASH_REMATCH") + options+=("$BASH_REMATCH") } # Parse GNU style help output of the given command and generate and store @@ -1466,7 +1492,7 @@ _comp_compgen_help() local -a _lines _comp_compgen_help__get_help_lines "$@" || return "$?" - local -a _options=() + local -a options=() local _line for _line in "${_lines[@]}"; do [[ $_line == *([[:blank:]])-* ]] || continue @@ -1476,9 +1502,9 @@ _comp_compgen_help() done _comp_compgen_help__parse "${_line// or /, }" done - ((${#_options[@]})) || return 1 + ((${#options[@]})) || return 1 - _comp_compgen -- -W '"${_options[@]}"' + _comp_compgen -U options -- -W '"${options[@]}"' return 0 } @@ -1498,7 +1524,7 @@ _comp_compgen_usage() local -a _lines _comp_compgen_help__get_help_lines "$@" || return "$?" - local -a _options=() + local -a options=() local _line _match _option _i _char for _line in "${_lines[@]}"; do while [[ $_line =~ \[[[:space:]]*(-[^]]+)[[:space:]]*\] ]]; do @@ -1509,7 +1535,7 @@ _comp_compgen_usage() # Treat as bundled short options for ((_i = 1; _i < ${#_option}; _i++)); do _char=${_option:_i:1} - [[ $_char != '[' ]] && _options+=("-$_char") + [[ $_char != '[' ]] && options+=("-$_char") done ;; *) @@ -1519,9 +1545,9 @@ _comp_compgen_usage() _line=${_line#*"$_match"} done done - ((${#_options[@]})) || return 1 + ((${#options[@]})) || return 1 - _comp_compgen -- -W '"${_options[@]}"' + _comp_compgen -U options -- -W '"${options[@]}"' return 0 } @@ -1533,7 +1559,7 @@ _comp_compgen_signals() { local -a sigs _comp_compgen -v sigs -c "SIG${cur#"${1-}"}" -- -P "${1-}" -A signal && - _comp_compgen_set "${sigs[@]/#${1-}SIG/${1-}}" + _comp_compgen -U sigs set "${sigs[@]/#${1-}SIG/${1-}}" } # This function completes on known mac addresses @@ -1541,7 +1567,7 @@ _comp_compgen_signals() # @since 2.12 _comp_compgen_mac_addresses() { - local re='\([A-Fa-f0-9]\{2\}:\)\{5\}[A-Fa-f0-9]\{2\}' + local _re='\([A-Fa-f0-9]\{2\}:\)\{5\}[A-Fa-f0-9]\{2\}' local PATH="$PATH:/sbin:/usr/sbin" local -a addresses @@ -1553,10 +1579,10 @@ _comp_compgen_mac_addresses() { LC_ALL=C ifconfig -a || ip -c=never link show || ip link show } 2>/dev/null | command sed -ne \ - "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($re\)[[:space:]].*/\1/p" -ne \ - "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($re\)[[:space:]]*$/\1/p" -ne \ - "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($re\)[[:space:]].*|\2|p" -ne \ - "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($re\)[[:space:]]*$|\2|p" + "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($_re\)[[:space:]].*/\1/p" -ne \ + "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($_re\)[[:space:]]*$/\1/p" -ne \ + "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($_re\)[[:space:]].*|\2|p" -ne \ + "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($_re\)[[:space:]]*$|\2|p" )" # ARP cache @@ -1564,15 +1590,15 @@ _comp_compgen_mac_addresses() { arp -an || ip -c=never neigh show || ip neigh show } 2>/dev/null | command sed -ne \ - "s/.*[[:space:]]\($re\)[[:space:]].*/\1/p" -ne \ - "s/.*[[:space:]]\($re\)[[:space:]]*$/\1/p" + "s/.*[[:space:]]\($_re\)[[:space:]].*/\1/p" -ne \ + "s/.*[[:space:]]\($_re\)[[:space:]]*$/\1/p" )" # /etc/ethers _comp_compgen -av addresses split -- "$(command sed -ne \ - "s/^[[:space:]]*\($re\)[[:space:]].*/\1/p" /etc/ethers 2>/dev/null)" + "s/^[[:space:]]*\($_re\)[[:space:]].*/\1/p" /etc/ethers 2>/dev/null)" - _comp_compgen_ltrim_colon "${addresses[@]}" + _comp_compgen -U addresses ltrim_colon "${addresses[@]}" } # This function completes on configured network interfaces @@ -1585,23 +1611,23 @@ _comp_compgen_configured_interfaces() # Debian system _comp_expand_glob files '/etc/network/interfaces /etc/network/interfaces.d/*' ((${#files[@]})) || return 0 - _comp_compgen_split -- "$(command sed -ne \ + _comp_compgen -U files split -- "$(command sed -ne \ 's|^iface \([^ ]\{1,\}\).*$|\1|p' "${files[@]}" 2>/dev/null)" elif [[ -f /etc/SuSE-release ]]; then # SuSE system _comp_expand_glob files '/etc/sysconfig/network/ifcfg-*' ((${#files[@]})) || return 0 - _comp_compgen_split -- "$(printf '%s\n' "${files[@]}" | + _comp_compgen -U files split -- "$(printf '%s\n' "${files[@]}" | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" elif [[ -f /etc/pld-release ]]; then # PLD Linux - _comp_compgen_split -- "$(command ls -B /etc/sysconfig/interfaces | + _comp_compgen -U files split -- "$(command ls -B /etc/sysconfig/interfaces | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" else # Assume Red Hat _comp_expand_glob files '/etc/sysconfig/network-scripts/ifcfg-*' ((${#files[@]})) || return 0 - _comp_compgen_split -- "$(printf '%s\n' "${files[@]}" | + _comp_compgen -U files split -- "$(printf '%s\n' "${files[@]}" | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" fi } @@ -1616,11 +1642,11 @@ _comp_compgen_configured_interfaces() # @since 2.12 _comp_compgen_ip_addresses() { - local n + local _n case ${1-} in - -a) n='6\{0,1\}' ;; - -6) n='6' ;; - *) n= ;; + -a) _n='6\{0,1\}' ;; + -6) _n='6' ;; + *) _n= ;; esac local PATH=$PATH:/sbin local addrs @@ -1628,13 +1654,13 @@ _comp_compgen_ip_addresses() LC_ALL=C ifconfig -a || ip -c=never addr show || ip addr show } 2>/dev/null | command sed -e 's/[[:space:]]addr:/ /' -ne \ - "s|.*inet${n}[[:space:]]\{1,\}\([^[:space:]/]*\).*|\1|p")" || + "s|.*inet${_n}[[:space:]]\{1,\}\([^[:space:]/]*\).*|\1|p")" || return - if [[ ! $n ]]; then - _comp_compgen -R -- -W '"${addrs[@]}"' + if [[ ! $_n ]]; then + _comp_compgen -U addrs set "${addrs[@]}" else - _comp_compgen_ltrim_colon "${addrs[@]}" + _comp_compgen -U addrs ltrim_colon "${addrs[@]}" fi } @@ -1665,7 +1691,7 @@ _comp_compgen_available_interfaces() fi } 2>/dev/null | awk \ '/^[^ \t]/ { if ($1 ~ /^[0-9]+:/) { print $2 } else { print $1 } }')" && - _comp_compgen -R -- -W '"${generated[@]/%[[:punct:]]/}"' + _comp_compgen -U generated set "${generated[@]}" } # Echo number of CPUs, falling back to 1 on failure. @@ -1831,7 +1857,7 @@ else fi fi ((${#procs[@]})) && - _comp_compgen -- -X "" -W '"${procs[@]}"' + _comp_compgen -U procs -- -X "" -W '"${procs[@]}"' } fi @@ -1879,7 +1905,7 @@ _comp_compgen_xinetd_services() local -a svcs _comp_expand_glob svcs '$xinetddir/!($_comp_backup_glob)' if ((${#svcs[@]})); then - _comp_compgen -- -W '"${svcs[@]#$xinetddir/}"' + _comp_compgen -U svcs -U xinetddir -- -W '"${svcs[@]#$xinetddir/}"' fi fi } @@ -1957,9 +1983,8 @@ _comp__init_set_up_service_completions # @since 2.12 _comp_compgen_kernel_modules() { - local modpath - modpath=/lib/modules/$1 - _comp_compgen_split -- "$(command ls -RL "$modpath" 2>/dev/null | + local _modpath=/lib/modules/$1 + _comp_compgen_split -- "$(command ls -RL "$_modpath" 2>/dev/null | command sed -ne 's/^\(.*\)\.k\{0,1\}o\(\.[gx]z\)\{0,1\}$/\1/p' \ -e 's/^\(.*\)\.ko\.zst$/\1/p')" } @@ -1992,26 +2017,24 @@ _comp_compgen_usergroup() # Completing group after 'user\:gr'. # Reply with a list of groups prefixed with 'user:', readline will # escape to the colon. - local prefix - prefix=${cur%%*([^:])} - prefix=${prefix//\\/} - local mycur=${cur#*[:]} + local _prefix + _prefix=${cur%%*([^:])} + _prefix=${_prefix//\\/} if [[ ${1-} == -u ]]; then - _comp_compgen -c "$mycur" allowed_groups + _comp_compgen -c "${cur#*:}" allowed_groups else - _comp_compgen -c "$mycur" -- -g + _comp_compgen -c "${cur#*:}" -- -g fi ((${#COMPREPLY[@]})) && - COMPREPLY=("${COMPREPLY[@]/#/$prefix}") + COMPREPLY=("${COMPREPLY[@]/#/$_prefix}") elif [[ $cur == *:* ]]; then # Completing group after 'user:gr'. # Reply with a list of unprefixed groups since readline with split on : # and only replace the 'gr' part - local mycur=${cur#*:} if [[ ${1-} == -u ]]; then - _comp_compgen -c "$mycur" allowed_groups + _comp_compgen -c "${cur#*:}" allowed_groups else - _comp_compgen -c "$mycur" -- -g + _comp_compgen -c "${cur#*:}" -- -g fi else # Completing a partial 'usernam'. @@ -2071,15 +2094,15 @@ _shells() # @since 2.12 _comp_compgen_fstypes() { - local fss + local _fss if [[ -e /proc/filesystems ]]; then # Linux - fss="$(cut -d$'\t' -f2 /proc/filesystems) + _fss="$(cut -d$'\t' -f2 /proc/filesystems) $(awk '! /\*/ { print $NF }' /etc/filesystems 2>/dev/null)" else # Generic - fss="$(awk '/^[ \t]*[^#]/ { print $3 }' /etc/fstab 2>/dev/null) + _fss="$(awk '/^[ \t]*[^#]/ { print $3 }' /etc/fstab 2>/dev/null) $(awk '/^[ \t]*[^#]/ { print $3 }' /etc/mnttab 2>/dev/null) $(awk '/^[ \t]*[^#]/ { print $4 }' /etc/vfstab 2>/dev/null) $(awk '{ print $1 }' /etc/dfs/fstypes 2>/dev/null) @@ -2087,7 +2110,7 @@ _comp_compgen_fstypes() $([[ -d /etc/fs ]] && command ls /etc/fs)" fi - [[ $fss ]] && _comp_compgen -- -W "$fss" + [[ $_fss ]] && _comp_compgen -- -W "$_fss" } # Get absolute path to a file, with rudimentary canonicalization. diff --git a/doc/api-and-naming.md b/doc/api-and-naming.md index eee33df8355..a83bcfa313e 100644 --- a/doc/api-and-naming.md +++ b/doc/api-and-naming.md @@ -150,7 +150,7 @@ calling `_comp_compgen` or other generators. To avoid conflicts with the options specified to `_comp_compgen`, one should not directly modify or reference the target variable. When post-filtering is needed, store them in a local array, filter them, and finally append them by -`_comp_compgen -- -W '"${arr[@]}"'`. To split the output of commands and +`_comp_compgen -- -W '"${_arr[@]}"'`. To split the output of commands and append the results to the target variable, use `_comp_compgen_split -- "$(cmd ...)"` instead of using `_comp_split COMPREPLY "$(cmd ...)"`. @@ -180,3 +180,45 @@ Exported generators are defined with the names `_comp_xfunc_CMD_compgen_NAME` and called by `_comp_compgen [opts] -x CMD NAME args`. Internal generators are defined with the names `_comp_cmd_CMD__compgen_NAME` and called by `_comp_compgen [opts] -i CMD NAME args`. + +#### Local variables of generator and `_comp_compgen -U var` + +A generator should basically define local variables with the names starting +with `_`. However, a generator sometimes needs to use local variable names +that do not start with `_`. When the child generator call with a variable name +(such as `local var; _comp_compgen -v var`) is used within the generator, the +local variable can unexpectedly mask a local variable of the upper call. + +For example, the following call fails to obtain the result of generator +`mygen1` because the array `arr` is masked by the same name of a local variable +in `_comp_compgen_mygen1`. + +```bash +# generator with a problem +_comp_compgen_mygen1() +{ + local -a arr=(1 2 3) + _comp_compgen -av arr -- -W '4 5 6' + _comp_compgen_set "${arr[@]/#p}" +} + +_comp_compgen -v arr mygen1 # fails to get the result in array `arr` +``` + +To avoid this, a generator that defines a local variable with its name not +starting with `_` can use the option `-U var` to unlocalize the variable on +assigning the final result. + +```bash +# properly designed generator +_comp_compgen_mygen1() +{ + local -a arr=(1 2 3) + _comp_compgen -av arr -- -W '4 5 6' + _comp_compgen -U arr set "${arr[@]/#p}" +} +``` + +To avoid unexpected unlocalization of previous-scope variables, a generator +should specify `-U var` to a child generator (that attempts to store results to +the current target variable) at most once. diff --git a/test/t/unit/test_unit_compgen.py b/test/t/unit/test_unit_compgen.py index f70de303033..f28e9a2b135 100644 --- a/test/t/unit/test_unit_compgen.py +++ b/test/t/unit/test_unit_compgen.py @@ -37,6 +37,12 @@ def functions(self, bash): "complete -F _comp_cmd_fcd fcd", ) + # test_8_option_U + assert_bash_exec( + bash, + "_comp_compgen_gen8() { local -a arr=(x y z); _comp_compgen -U arr -- -W '\"${arr[@]}\"'; }", + ) + def test_1_basic(self, bash, functions): output = assert_bash_exec( bash, "_comp__test_words 12 34 56 ''", want_output=True @@ -146,3 +152,9 @@ def test_7_xcmd(self, bash, functions): completions = assert_complete(bash, "compgen-cmd2 '") assert completions == ["012", "123", "234", "5foo", "6bar", "7baz"] + + def test_8_option_U(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_compgen gen8", want_output=True + ) + assert output.strip() == "" From 7d2867c01527c3037579fe24da62cccfdfd1883e Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 12 Aug 2023 07:28:10 +0900 Subject: [PATCH 4/8] fix(bts): update functions to generators and fix variable conflicts --- completions/bts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/completions/bts b/completions/bts index bdf8df0d049..c9293dac786 100644 --- a/completions/bts +++ b/completions/bts @@ -9,14 +9,14 @@ _comp_cmd_bts__compgen_cached_bugs() -name "${cur}[0-9]*.html" \ -printf "%f\n" | cut -d'.' -f1 ) - _comp_compgen -aR -- -W '$bugs' + _comp_compgen -RU bugs -- -W '$bugs' } # Generate APT source packages prefixed with "src:" _comp_cmd_bts__compgen_src_packages_with_prefix() { local ppn=${cur:4} # partial package name, after stripping "src:" - _comp_compgen -ac "$ppn" split -P "src:" -- \ + _comp_compgen -c "$ppn" -U ppn split -P "src:" -- \ "$(_comp_xfunc apt-cache sources "$ppn")" } @@ -28,8 +28,8 @@ _comp_cmd_bts() case $prev in show | bugs) _comp_compgen -- -W 'release-critical RC from: tag: usertag:' - _comp_cmd_bts__compgen_cached_bugs - _comp_cmd_bts__compgen_src_packages_with_prefix + _comp_compgen -ai bts cached_bugs + _comp_compgen -ai bts src_packages_with_prefix return ;; select) @@ -40,7 +40,7 @@ _comp_cmd_bts() ;; status) _comp_compgen -- -W 'file: fields: verbose' - _comp_cmd_bts__compgen_cached_bugs + _comp_compgen -ai bts cached_bugs return ;; block | unblock) @@ -58,7 +58,7 @@ _comp_cmd_bts() return ;; clone | "done" | reopen | archive | unarchive | retitle | summary | submitter | found | notfound | fixed | notfixed | merge | forcemerge | unmerge | claim | unclaim | forwarded | notforwarded | owner | noowner | subscribe | unsubscribe | reportspam | spamreport | affects | usertag | usertags | reassign | tag | tags) - _comp_cmd_bts__compgen_cached_bugs + _comp_compgen -i bts cached_bugs return ;; package) @@ -67,13 +67,13 @@ _comp_cmd_bts() ;; cache) COMPREPLY=($(_comp_xfunc apt-cache packages)) - _comp_cmd_bts__compgen_src_packages_with_prefix + _comp_compgen -ai bts src_packages_with_prefix _comp_compgen -a -- -W 'from: release-critical RC' return ;; cleancache) COMPREPLY=($(_comp_xfunc apt-cache packages)) - _comp_cmd_bts__compgen_src_packages_with_prefix + _comp_compgen -ai bts src_packages_with_prefix _comp_compgen -a -- -W 'from: tag: usertag: ALL' return ;; From 0d145c9f2852955848ca1855d9fae216ebbd78c3 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 12 Aug 2023 07:34:06 +0900 Subject: [PATCH 5/8] fix(cvs): avoid variable conflicts --- completions/cvs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completions/cvs b/completions/cvs index 9eb434ad22c..d17f74e7169 100644 --- a/completions/cvs +++ b/completions/cvs @@ -45,7 +45,7 @@ _comp_xfunc_cvs_compgen_roots() [[ -r ~/.cvspass ]] && cvsroots+=($(awk '{ print $2 }' ~/.cvspass)) [[ -r CVS/Root ]] && mapfile -tO "${#cvsroots[@]}" cvsroots Date: Sat, 12 Aug 2023 07:37:26 +0900 Subject: [PATCH 6/8] fix(openssl): avoid variable conflicts --- completions/openssl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/completions/openssl b/completions/openssl index 4a94530c6d2..808500aaad2 100644 --- a/completions/openssl +++ b/completions/openssl @@ -2,27 +2,27 @@ _comp_cmd_openssl__compgen_sections() { - local config i f + local config _i _file # check if a specific configuration file is used - for ((i = 2; i < cword; i++)); do - if [[ ${words[i]} == -config ]]; then - config=${words[i + 1]} + for ((_i = 2; _i < cword; _i++)); do + if [[ ${words[_i]} == -config ]]; then + config=${words[_i + 1]} break fi done # if no config given, check some usual default locations if [[ ! $config ]]; then - for f in /etc/ssl/openssl.cnf /etc/pki/tls/openssl.cnf \ + for _file in /etc/ssl/openssl.cnf /etc/pki/tls/openssl.cnf \ /usr/share/ssl/openssl.cnf; do - [[ -f $f ]] && config=$f && break + [[ -f $_file ]] && config=$_file && break done fi [[ ! -f $config ]] && return - _comp_compgen_split -- "$(awk '/\[.*\]/ {print $2}' "$config")" + _comp_compgen -U config split -- "$(awk '/\[.*\]/ {print $2}' "$config")" } _comp_cmd_openssl__compgen_digests() From bef94c39b9fac23655c8b50181d7834869d72702 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 12 Aug 2023 07:49:52 +0900 Subject: [PATCH 7/8] fix(python): avoid variable conflicts --- completions/python | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/completions/python b/completions/python index 920d9266ff6..1dffac18d1e 100644 --- a/completions/python +++ b/completions/python @@ -3,9 +3,9 @@ # @since 2.12 _comp_xfunc_python_compgen_modules() { - local python=python - [[ ${comp_args[0]##*/} == *3* ]] && python=python3 - _comp_cmd_python__compgen_modules "$python" + local _python=python + [[ ${comp_args[0]##*/} == *3* ]] && _python=python3 + _comp_cmd_python__compgen_modules "$_python" } # @deprecated 2.12 use `_comp_xfunc_python_compgen_modules` instead From f2df91d378004780d5237ab89faea2d6789169c0 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 12 Aug 2023 07:53:43 +0900 Subject: [PATCH 8/8] fix(ssh): avoid variable conflicts --- completions/ssh | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/completions/ssh b/completions/ssh index 7418b983e03..c83bf26b12c 100644 --- a/completions/ssh +++ b/completions/ssh @@ -8,7 +8,7 @@ _comp_cmd_ssh__compgen_queries() key-plain key-sig protocol-version compression sig ciphers macs kexalgorithms pubkeyacceptedkeytypes hostkeyalgorithms hostbasedkeytypes hostbasedacceptedkeytypes) - _comp_compgen -c "${cur,,}" -- -W '"${ret[@]}" help"' + _comp_compgen -c "${cur,,}" -U ret -- -W '"${ret[@]}" help"' } # @since 2.12 @@ -37,7 +37,7 @@ _comp_cmd_ssh__compgen_ciphers() [[ ${ret-} ]] || ret=(3des-cbc aes128-cbc aes192-cbc aes256-cbc aes128-ctr aes192-ctr aes256-ctr arcfour128 arcfour256 arcfour blowfish-cbc cast128-cbc) - _comp_compgen -- -W '"${ret[@]}"' + _comp_compgen -U ret -- -W '"${ret[@]}"' } _comp_cmd_ssh__compgen_macs() @@ -46,7 +46,7 @@ _comp_cmd_ssh__compgen_macs() _comp_compgen -v ret -i ssh query "$1" mac [[ ${ret-} ]] || ret=(hmac-md5 hmac-sha1 umac-64@openssh.com hmac-ripemd160 hmac-sha1-96 hmac-md5-96) - _comp_compgen -- -W '"${ret[@]}"' + _comp_compgen -U ret -- -W '"${ret[@]}"' } # @since 2.12 @@ -283,7 +283,7 @@ _comp_xfunc_ssh_compgen_identityfile() local cur=$cur tmp [[ ! $cur && -d ~/.ssh ]] && cur=~/.ssh/id _comp_compgen -v tmp -c "$cur" filedir && - _comp_compgen -- -W '"${tmp[@]}"' -X "${1:+!}*.pub" + _comp_compgen -U tmp -- -W '"${tmp[@]}"' -X "${1:+!}*.pub" } _comp_deprecate_func 2.12 _ssh_identityfile _comp_xfunc_ssh_compgen_identityfile @@ -467,35 +467,35 @@ _comp_xfunc_scp_compgen_remote_files() # remove backslash escape from the first colon cur=${cur/\\:/:} - local userhost=${cur%%?(\\):*} - local path=${cur#*:} + local _userhost=${cur%%?(\\):*} + local _path=${cur#*:} # unescape (3 backslashes to 1 for chars we escaped) # shellcheck disable=SC2090 - path=$(command sed -e 's/\\\\\\\('"$_comp_cmd_scp__path_esc"'\)/\\\1/g' <<<"$path") + _path=$(command sed -e 's/\\\\\\\('"$_comp_cmd_scp__path_esc"'\)/\\\1/g' <<<"$_path") # default to home dir of specified user on remote host - if [[ ! $path ]]; then - path=$(ssh -o 'Batchmode yes' "$userhost" pwd 2>/dev/null) + if [[ ! $_path ]]; then + _path=$(ssh -o 'Batchmode yes' "$_userhost" pwd 2>/dev/null) fi - local files + local _files if [[ ${1-} == -d ]]; then # escape problematic characters; remove non-dirs # shellcheck disable=SC2090 - files=$(ssh -o 'Batchmode yes' "$userhost" \ - command ls -aF1dL "$path*" 2>/dev/null | + _files=$(ssh -o 'Batchmode yes' "$_userhost" \ + command ls -aF1dL "$_path*" 2>/dev/null | command sed -e 's/'"$_comp_cmd_scp__path_esc"'/\\\\\\&/g' -e '/[^\/]$/d') else # escape problematic characters; remove executables, aliases, pipes # and sockets; add space at end of file names # shellcheck disable=SC2090 - files=$(ssh -o 'Batchmode yes' "$userhost" \ - command ls -aF1dL "$path*" 2>/dev/null | + _files=$(ssh -o 'Batchmode yes' "$_userhost" \ + command ls -aF1dL "$_path*" 2>/dev/null | command sed -e 's/'"$_comp_cmd_scp__path_esc"'/\\\\\\&/g' -e 's/[*@|=]$//g' \ -e 's/[^\/]$/& /g') fi - _comp_split -l COMPREPLY "$files" + _comp_compgen_split -l -- "$_files" } # @deprecated 2.12 use `_comp_compgen -ax ssh remote_files` instead