From 4411787ee6e40b9a1d81a2f430c59ca36a9c44f1 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Thu, 24 Feb 2022 13:28:00 +0900 Subject: [PATCH 1/8] test(bash_env_saved): add "save_{shopt,variable}" to allow any changes --- test/t/conftest.py | 49 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/test/t/conftest.py b/test/t/conftest.py index 4e6df9e870b..41df0c16fa3 100644 --- a/test/t/conftest.py +++ b/test/t/conftest.py @@ -7,6 +7,7 @@ import sys import tempfile import time +from enum import Enum from pathlib import Path from types import TracebackType from typing import ( @@ -444,14 +445,18 @@ def assert_bash_exec( class bash_env_saved: counter: int = 0 + class saved_state(Enum): + ChangesDetected = 1 + ChangesIgnored = 2 + def __init__(self, bash: pexpect.spawn, sendintr: bool = False): bash_env_saved.counter += 1 self.prefix: str = "_comp__test_%d" % bash_env_saved.counter self.bash = bash self.cwd_changed: bool = False - self.saved_shopt: Dict[str, int] = {} - self.saved_variables: Dict[str, int] = {} + self.saved_shopt: Dict[str, bash_env_saved.saved_state] = {} + self.saved_variables: Dict[str, bash_env_saved.saved_state] = {} self.sendintr = sendintr self.noexcept: bool = False @@ -516,6 +521,11 @@ def _save_cwd(self): self._copy_variable("PWD", "%s_OLDPWD" % self.prefix) def _check_shopt(self, name: str): + if ( + self.saved_shopt[name] + != bash_env_saved.saved_state.ChangesDetected + ): + return self._safe_assert( '[[ $(shopt -p %s) == "${%s_NEWSHOPT_%s}" ]]' % (name, self.prefix, name), @@ -523,7 +533,7 @@ def _check_shopt(self, name: str): def _unprotect_shopt(self, name: str): if name not in self.saved_shopt: - self.saved_shopt[name] = 1 + self.saved_shopt[name] = bash_env_saved.saved_state.ChangesDetected self._safe_exec( "%s_OLDSHOPT_%s=$(shopt -p %s || true)" % (self.prefix, name, name), @@ -538,6 +548,11 @@ def _protect_shopt(self, name: str): ) def _check_variable(self, varname: str): + if ( + self.saved_variables[varname] + != bash_env_saved.saved_state.ChangesDetected + ): + return try: self._safe_assert( '[[ ${%s-%s} == "${%s_NEWVAR_%s-%s}" ]]' @@ -556,7 +571,9 @@ def _check_variable(self, varname: str): def _unprotect_variable(self, varname: str): if varname not in self.saved_variables: - self.saved_variables[varname] = 1 + self.saved_variables[ + varname + ] = bash_env_saved.saved_state.ChangesDetected self._copy_variable( varname, "%s_OLDVAR_%s" % (self.prefix, varname) ) @@ -581,13 +598,6 @@ def _restore_env(self): self._unset_variable("%s_OLDPWD" % self.prefix) self.cwd_changed = False - for name in self.saved_shopt: - self._check_shopt(name) - self._safe_exec('eval "$%s_OLDSHOPT_%s"' % (self.prefix, name)) - self._unset_variable("%s_OLDSHOPT_%s" % (self.prefix, name)) - self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name)) - self.saved_shopt = {} - for varname in self.saved_variables: self._check_variable(varname) self._copy_variable( @@ -597,6 +607,13 @@ def _restore_env(self): self._unset_variable("%s_NEWVAR_%s" % (self.prefix, varname)) self.saved_variables = {} + for name in self.saved_shopt: + self._check_shopt(name) + self._safe_exec('eval "$%s_OLDSHOPT_%s"' % (self.prefix, name)) + self._unset_variable("%s_OLDSHOPT_%s" % (self.prefix, name)) + self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name)) + self.saved_shopt = {} + self.noexcept = False if self.captured_error: raise self.captured_error @@ -616,6 +633,10 @@ def shopt(self, name: str, value: bool): self._safe_exec("shopt -u %s" % name) self._protect_shopt(name) + def save_shopt(self, name: str): + self._unprotect_shopt(name) + self.saved_shopt[name] = bash_env_saved.saved_state.ChangesIgnored + def write_variable(self, varname: str, new_value: str, quote: bool = True): if quote: new_value = shlex.quote(new_value) @@ -623,6 +644,12 @@ def write_variable(self, varname: str, new_value: str, quote: bool = True): self._safe_exec("%s=%s" % (varname, new_value)) self._protect_variable(varname) + def save_variable(self, varname: str): + self._unprotect_variable(varname) + self.saved_variables[ + varname + ] = bash_env_saved.saved_state.ChangesIgnored + # TODO: We may restore the "export" attribute as well though it is # not currently tested in "diff_env" def write_env(self, envname: str, new_value: str, quote: bool = True): From 6bc91eda3f58ff70b6da7bebaed657258d4b8283 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Thu, 24 Feb 2022 13:36:40 +0900 Subject: [PATCH 2/8] test(bash_env_saved): add "{,save_}set" to save/restore "set" options --- test/t/conftest.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/t/conftest.py b/test/t/conftest.py index 41df0c16fa3..7286b73c386 100644 --- a/test/t/conftest.py +++ b/test/t/conftest.py @@ -455,6 +455,7 @@ def __init__(self, bash: pexpect.spawn, sendintr: bool = False): self.bash = bash self.cwd_changed: bool = False + self.saved_set: Dict[str, bash_env_saved.saved_state] = {} self.saved_shopt: Dict[str, bash_env_saved.saved_state] = {} self.saved_variables: Dict[str, bash_env_saved.saved_state] = {} self.sendintr = sendintr @@ -520,6 +521,30 @@ def _save_cwd(self): self.cwd_changed = True self._copy_variable("PWD", "%s_OLDPWD" % self.prefix) + def _check_set(self, name: str): + if self.saved_set[name] != bash_env_saved.saved_state.ChangesDetected: + return + self._safe_assert( + '[[ $(shopt -po %s) == "${%s_NEWSHOPT_%s}" ]]' + % (name, self.prefix, name), + ) + + def _unprotect_set(self, name: str): + if name not in self.saved_set: + self.saved_set[name] = bash_env_saved.saved_state.ChangesDetected + self._safe_exec( + "%s_OLDSHOPT_%s=$(shopt -po %s || true)" + % (self.prefix, name, name), + ) + else: + self._check_set(name) + + def _protect_set(self, name: str): + self._safe_exec( + "%s_NEWSHOPT_%s=$(shopt -po %s || true)" + % (self.prefix, name, name), + ) + def _check_shopt(self, name: str): if ( self.saved_shopt[name] @@ -614,6 +639,13 @@ def _restore_env(self): self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name)) self.saved_shopt = {} + for name in self.saved_set: + self._check_set(name) + self._safe_exec('eval "$%s_OLDSHOPT_%s"' % (self.prefix, name)) + self._unset_variable("%s_OLDSHOPT_%s" % (self.prefix, name)) + self._unset_variable("%s_NEWSHOPT_%s" % (self.prefix, name)) + self.saved_set = {} + self.noexcept = False if self.captured_error: raise self.captured_error @@ -625,6 +657,18 @@ def chdir(self, path: str): self._safe_exec("command cd -- %s" % shlex.quote(path)) self._protect_variable("OLDPWD") + def set(self, name: str, value: bool): + self._unprotect_set(name) + if value: + self._safe_exec("set -u %s" % name) + else: + self._safe_exec("set +o %s" % name) + self._protect_set(name) + + def save_set(self, name: str): + self._unprotect_set(name) + self.saved_set[name] = bash_env_saved.saved_state.ChangesIgnored + def shopt(self, name: str, value: bool): self._unprotect_shopt(name) if value: From 28c4ed9cc9d9d4f2f83fd1d592c41d4273f0ef85 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 11 Sep 2021 15:10:54 +0900 Subject: [PATCH 3/8] feat(_comp_expand_glob): add utility to safely expand pathnames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ville Skyttä --- bash_completion | 49 +++++++++++++ test/t/unit/test_unit_expand_glob.py | 106 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 test/t/unit/test_unit_expand_glob.py diff --git a/bash_completion b/bash_completion index 6fc816740be..7599ca7d587 100644 --- a/bash_completion +++ b/bash_completion @@ -260,6 +260,55 @@ _upvars() done } +# Get the list of filenames that match with the specified glob pattern. +# This function does the globbing in a controlled environment, avoiding +# interference from user's shell options/settings or environment variables. +# @param $1 array_name Array name +# The array name should not start with the double underscores "__". The +# array name should not be "GLOBIGNORE". +# @param $2 pattern Pattern string to be evaluated. +# This pattern string will be evaluated using "eval", so brace expansions, +# parameter expansions, command substitutions, and other expansions will be +# processed. The user-provided strings should not be directly specified to +# this argument. +_comp_expand_glob() +{ + if (($# != 2)); then + printf 'bash-completion: %s: unexpected number of arguments\n' "$FUNCNAME" >&2 + printf 'usage: %s ARRAY_NAME PATTERN\n' "$FUNCNAME" >&2 + return 2 + elif [[ $1 == @(GLOBIGNORE|__*|*[^_a-zA-Z0-9]*|[0-9]*|'') ]]; then + printf 'bash-completion: %s: invalid array name "%s"\n' "$FUNCNAME" "$1" >&2 + return 2 + fi + + # Save and adjust the settings. + local __original_opts=$SHELLOPTS:$BASHOPTS + set +o noglob + shopt -s nullglob + shopt -u failglob dotglob + + # Also the user's GLOBIGNORE may affect the result of pathname expansions. + local GLOBIGNORE= + + eval -- "$1=()" # a fallback in case that the next line fails. + eval -- "$1=($2)" + + # Restore the settings. Note: Changing GLOBIGNORE affects the state of + # "shopt -q dotglob", so we need to explicitly restore the original state + # of "shopt -q dotglob". + _comp_unlocal GLOBIGNORE + if [[ :$__original_opts: == *:dotglob:* ]]; then + shopt -s dotglob + else + shopt -u dotglob + fi + [[ :$__original_opts: == *:nullglob:* ]] || shopt -u nullglob + [[ :$__original_opts: == *:failglob:* ]] && shopt -s failglob + [[ :$__original_opts: == *:noglob:* ]] && set -o noglob + return 0 +} + # Reassemble command line words, excluding specified characters from the # list of word completion separators (COMP_WORDBREAKS). # @param $1 chars Characters out of $COMP_WORDBREAKS which should diff --git a/test/t/unit/test_unit_expand_glob.py b/test/t/unit/test_unit_expand_glob.py new file mode 100644 index 00000000000..2d3b85ef28b --- /dev/null +++ b/test/t/unit/test_unit_expand_glob.py @@ -0,0 +1,106 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_filedir", + ignore_env=r"^\+(my_array=|declare -f dump_array$)", +) +class TestExpandGlob: + def test_match_all(self, bash): + assert_bash_exec( + bash, + "dump_array() { ((${#my_array[@]})) && printf '<%s>' \"${my_array[@]}\"; echo; }", + ) + output = assert_bash_exec( + bash, + "LC_ALL= LC_COLLATE=C _comp_expand_glob my_array '*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_match_pattern(self, bash): + output = assert_bash_exec( + bash, + "LC_ALL= LC_COLLATE=C _comp_expand_glob my_array 'a*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_match_unmatched(self, bash): + output = assert_bash_exec( + bash, + "_comp_expand_glob my_array 'unmatched-*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_match_multiple_words(self, bash): + output = assert_bash_exec( + bash, + "_comp_expand_glob my_array 'b* e*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_match_brace_expansion(self, bash): + output = assert_bash_exec( + bash, + "_comp_expand_glob my_array 'brac{ket,unmatched}*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_protect_from_noglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.set("noglob", True) + output = assert_bash_exec( + bash, + "LC_ALL= LC_COLLATE=C _comp_expand_glob my_array 'a*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_protect_from_failglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, + "_comp_expand_glob my_array 'unmatched-*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_protect_from_nullglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", False) + output = assert_bash_exec( + bash, + "_comp_expand_glob my_array 'unmatched-*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_protect_from_dotglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("dotglob", True) + output = assert_bash_exec( + bash, + "_comp_expand_glob my_array 'ext/foo/*';dump_array", + want_output=True, + ) + assert output.strip() == "" + + def test_protect_from_GLOBIGNORE(self, bash): + with bash_env_saved(bash) as bash_env: + # Note: dotglob is changed by GLOBIGNORE + bash_env.save_shopt("dotglob") + bash_env.write_variable("GLOBIGNORE", "*") + output = assert_bash_exec( + bash, + "LC_ALL= LC_COLLATE=C _comp_expand_glob my_array 'a*';dump_array", + want_output=True, + ) + assert output.strip() == "" From 13464e4bc32c9ab8bd0d091fc0b134ccd70c8720 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Thu, 24 Feb 2022 13:53:29 +0900 Subject: [PATCH 4/8] fix(_included_ssh_config_files): fix for failglob --- bash_completion | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bash_completion b/bash_completion index 7599ca7d587..19ba16a015d 100644 --- a/bash_completion +++ b/bash_completion @@ -1786,7 +1786,7 @@ _included_ssh_config_files() { (($# < 1)) && echo "bash_completion: $FUNCNAME: missing mandatory argument CONFIG" >&2 - local configfile i f + local configfile i files f configfile=$1 local IFS=$' \t\n' reset=$(shopt -po noglob) @@ -1809,15 +1809,16 @@ _included_ssh_config_files() fi __expand_tilde_by_ref i # In case the expanded variable contains multiple paths - set +o noglob - for f in $i; do - if [[ -r $f ]]; then - config+=("$f") - # The Included file is processed to look for Included files in itself - _included_ssh_config_files $f - fi - done - $reset + _comp_expand_glob files '$i' + if ((${#files[@]})); then + for f in "${files[@]}"; do + if [[ -r $f ]]; then + config+=("$f") + # The Included file is processed to look for Included files in itself + _included_ssh_config_files $f + fi + done + fi done } # _included_ssh_config_files() From 5d49f2457fdb5179500f24833e72ab21d519266c Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Thu, 24 Feb 2022 14:54:02 +0900 Subject: [PATCH 5/8] fix: use "_comp_expand_glob" for failglob/nullglob/etc --- bash_completion | 36 +++++++++++++++++++++--------------- completions/_rtcwake | 2 +- completions/_yum | 5 ++++- completions/dpkg | 2 +- completions/hcitool | 2 +- completions/hunspell | 9 +++------ completions/ipmitool | 13 ++++++++----- completions/lintian | 24 +++++++++++++++--------- completions/minicom | 10 +++++----- completions/mysql | 9 ++++----- completions/ps | 5 +---- completions/qemu | 9 ++------- completions/screen | 9 +++------ completions/valgrind | 14 ++++++++------ completions/vpnc | 8 +++----- completions/xdg-mime | 8 +++----- 16 files changed, 83 insertions(+), 82 deletions(-) diff --git a/bash_completion b/bash_completion index 19ba16a015d..a7a4d2f332e 100644 --- a/bash_completion +++ b/bash_completion @@ -1148,15 +1148,20 @@ _mac_addresses() # _configured_interfaces() { + local -a files if [[ -f /etc/debian_version ]]; then # Debian system + _comp_expand_glob files '/etc/network/interfaces /etc/network/interfaces.d/*' + ((${#files[@]})) || return 0 COMPREPLY=($(compgen -W "$(command sed -ne 's|^iface \([^ ]\{1,\}\).*$|\1|p' \ - /etc/network/interfaces /etc/network/interfaces.d/* 2>/dev/null)" \ + "${files[@]}" 2>/dev/null)" \ -- "$cur")) elif [[ -f /etc/SuSE-release ]]; then # SuSE system + _comp_expand_glob files '/etc/sysconfig/network/ifcfg-*' + ((${#files[@]})) || return 0 COMPREPLY=($(compgen -W "$(printf '%s\n' \ - /etc/sysconfig/network/ifcfg-* | + "${files[@]}" | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")) elif [[ -f /etc/pld-release ]]; then # PLD Linux @@ -1165,8 +1170,10 @@ _configured_interfaces() command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")) else # Assume Red Hat + _comp_expand_glob files '/etc/sysconfig/network-scripts/ifcfg-*' + ((${#files[@]})) || return 0 COMPREPLY=($(compgen -W "$(printf '%s\n' \ - /etc/sysconfig/network-scripts/ifcfg-* | + "${files[@]}" | command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")) fi } @@ -1422,12 +1429,12 @@ _xinetd_services() { local xinetddir=${_comp__test_xinetd_dir:-/etc/xinetd.d} if [[ -d $xinetddir ]]; then - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - local -a svcs=($xinetddir/!($_comp_backup_glob)) - $reset - ((!${#svcs[@]})) || + local -a svcs + _comp_expand_glob svcs '$xinetddir/!($_comp_backup_glob)' + if ((${#svcs[@]})); then + local IFS=$'\n' COMPREPLY+=($(compgen -W '"${svcs[@]#$xinetddir/}"' -- "${cur-}")) + fi fi } @@ -1438,12 +1445,9 @@ _services() local sysvdirs _sysvdirs - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - COMPREPLY=( - $(printf '%s\n' ${sysvdirs[0]}/!($_comp_backup_glob|functions|README))) - $reset + _comp_expand_glob COMPREPLY '${sysvdirs[0]}/!($_comp_backup_glob|functions|README)' + local IFS=$'\n' COMPREPLY+=($({ systemctl list-units --full --all || systemctl list-unit-files @@ -1474,7 +1478,7 @@ _service() _services [[ -e /etc/mandrake-release ]] && _xinetd_services else - local sysvdirs + local IFS=$'\n' sysvdirs _sysvdirs COMPREPLY=($(compgen -W '`command sed -e "y/|/ /" \ -ne "s/^.*\(U\|msg_u\)sage.*{\(.*\)}.*$/\2/p" \ @@ -1726,7 +1730,9 @@ _terms() { toe -a || toe } | awk '{ print $1 }' - find /{etc,lib,usr/lib,usr/share}/terminfo/? -type f -maxdepth 1 | + _comp_expand_glob dirs '/{etc,lib,usr/lib,usr/share}/terminfo/?' + ((${#dirs[@]})) && + find "${dirs[@]}" -type f -maxdepth 1 | awk -F/ '{ print $NF }' } 2>/dev/null)" -- "$cur")) } diff --git a/completions/_rtcwake b/completions/_rtcwake index 6a0bb6cd983..c7b69c46521 100644 --- a/completions/_rtcwake +++ b/completions/_rtcwake @@ -17,7 +17,7 @@ _rtcwake() return ;; --device | -d) - COMPREPLY=($(command ls -d /dev/rtc?* 2>/dev/null)) + _comp_expand_glob COMPREPLY '/dev/rtc?*' ((${#COMPREPLY[@]})) && COMPREPLY=($(compgen -W '"${COMPREPLY[@]#/dev/}"' -- "$cur")) return diff --git a/completions/_yum b/completions/_yum index c3a2487378b..1802627d8fe 100644 --- a/completions/_yum +++ b/completions/_yum @@ -30,7 +30,10 @@ _yum_repolist() _yum_plugins() { - command ls /usr/lib/yum-plugins/*.py{,c,o} 2>/dev/null | + local -a files + _comp_expand_glob files '/usr/lib/yum-plugins/*.py{,c,o}' + ((${#files[@]})) && + printf '%s\n' "${files[@]}" | command sed -ne 's|.*/\([^./]*\)\.py[co]\{0,1\}$|\1|p' | sort -u } diff --git a/completions/dpkg b/completions/dpkg index 9baed1edfaf..8f409a08c7f 100644 --- a/completions/dpkg +++ b/completions/dpkg @@ -133,7 +133,7 @@ _dpkg_reconfigure() case $prev in --frontend | -!(-*)f) - opt=(/usr/share/perl5/Debconf/FrontEnd/*) + _comp_expand_glob opt '/usr/share/perl5/Debconf/FrontEnd/*' if ((${#opt[@]})); then opt=(${opt[@]##*/}) opt=(${opt[@]%.pm}) diff --git a/completions/hcitool b/completions/hcitool index 13fcfef592a..14ae05c60e1 100644 --- a/completions/hcitool +++ b/completions/hcitool @@ -354,7 +354,7 @@ _hciattach() _count_args case $args in 1) - COMPREPLY=(/dev/tty*) + _comp_expand_glob COMPREPLY '/dev/tty*' ((${#COMPREPLY[@]})) && COMPREPLY=($(compgen -W '"${COMPREPLY[@]}" "${COMPREPLY[@]#/dev/}"' -- "$cur")) diff --git a/completions/hunspell b/completions/hunspell index 8368ee25194..44f04b540ac 100644 --- a/completions/hunspell +++ b/completions/hunspell @@ -10,15 +10,12 @@ _hunspell() return ;; -d) - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - local -a dicts=(/usr/share/hunspell/*.dic - /usr/local/share/hunspell/*.dic) - $reset + local -a dicts + _comp_expand_glob dicts '/usr/share/hunspell/*.dic /usr/local/share/hunspell/*.dic' if ((${#dicts[@]})); then dicts=("${dicts[@]##*/}") dicts=("${dicts[@]%.dic}") - IFS=$'\n' + local IFS=$'\n' COMPREPLY=($(compgen -W '${dicts[@]}' -- "$cur")) fi return diff --git a/completions/ipmitool b/completions/ipmitool index 920287dd568..d0b8ec0c7ae 100644 --- a/completions/ipmitool +++ b/completions/ipmitool @@ -16,11 +16,14 @@ _ipmitool() return ;; -*d) - COMPREPLY=($(compgen -W "$( - command ls -d /dev/ipmi* /dev/ipmi/* /dev/ipmidev/* \ - 2>/dev/null | command sed -ne 's/^[^0-9]*\([0-9]\{1,\}\)/\1/p' - )" \ - -- "$cur")) + local -a files + _comp_expand_glob files '/dev/ipmi* /dev/ipmi/* /dev/ipmidev/*' + ((${#files[@]})) && + COMPREPLY=($(compgen -W "$( + printf '%s\n' "${files[@]}" | + command sed -ne 's/^[^0-9]*\([0-9]\{1,\}\)/\1/p' + )" \ + -- "$cur")) return ;; -*I) diff --git a/completions/lintian b/completions/lintian index 9343832523f..09167d3d683 100644 --- a/completions/lintian +++ b/completions/lintian @@ -2,14 +2,16 @@ _lintian_tags() { - local match search tags + local match search tags check_files + _comp_expand_glob check_files '/usr/share/lintian/checks/*.desc' + ((${#check_files[@]})) || return 0 - tags=$(awk '/^Tag/ { print $2 }' /usr/share/lintian/checks/*.desc) + tags=$(awk '/^Tag/ { print $2 }' "${check_files[@]}") if [[ $cur == *, ]]; then search=${cur//,/ } for item in $search; do match=$(command grep -nE "^Tag: $item$" \ - /usr/share/lintian/checks/*.desc | cut -d: -f1) + "${check_files[@]}" | cut -d: -f1) tags=$(command sed -e "s/\<$item\>//g" <<<$tags) done COMPREPLY+=($(compgen -W "$tags")) @@ -22,15 +24,17 @@ _lintian_tags() _lintian_checks() { - local match search todisable checks + local match search todisable checks check_files + _comp_expand_glob check_files '/usr/share/lintian/checks/*.desc' + ((${#check_files[@]})) || return 0 checks=$(awk '/^(Check-Script|Abbrev)/ { print $2 }' \ - /usr/share/lintian/checks/*.desc) + "${check_files[@]}") if [[ $cur == *, ]]; then search=${cur//,/ } for item in $search; do match=$(command grep -nE "^(Check-Script|Abbrev): $item$" \ - /usr/share/lintian/checks/*.desc | cut -d: -f1) + "${check_files[@]}" | cut -d: -f1) todisable=$(awk '/^(Check-Script|Abbrev)/ { print $2 }' $match) for name in $todisable; do checks=$(command sed -e "s/\<$name\>//g" <<<$checks) @@ -46,15 +50,17 @@ _lintian_checks() _lintian_infos() { - local match search infos + local match search infos collection_files + _comp_expand_glob collection_files '/usr/share/lintian/collection/*.desc' + ((${#collection_files[@]})) || return 0 infos=$(awk '/^Collector/ { print $2 }' \ - /usr/share/lintian/collection/*.desc) + "${collection_files[@]}") if [[ $cur == *, ]]; then search=${cur//,/ } for item in $search; do match=$(command grep -nE "^Collector: $item$" \ - /usr/share/lintian/collection/*.desc | cut -d: -f1) + "${collection_files[@]}" | cut -d: -f1) infos=$(command sed -e "s/\<$item\>//g" <<<$infos) done COMPREPLY+=($(compgen -W "$infos")) diff --git a/completions/minicom b/completions/minicom index 44894d08d06..5f41dc415a5 100644 --- a/completions/minicom +++ b/completions/minicom @@ -15,7 +15,7 @@ _minicom() return ;; --ptty | -!(-*)p) - COMPREPLY=(/dev/tty*) + _comp_expand_glob COMPREPLY '/dev/tty*' ((${#COMPREPLY[@]})) && COMPREPLY=($(compgen -W '"${COMPREPLY[@]}" "${COMPREPLY[@]#/dev/}}' \ -- "$cur")) @@ -31,10 +31,10 @@ _minicom() return fi - COMPREPLY=( - $(printf '%s\n' /etc/minirc.* /etc/minicom/minirc.* ~/.minirc.* | - command sed -e '/\*$/d' -e 's/^.*minirc\.//' | - command grep "^${cur}")) + local -a files + _comp_expand_glob files '/etc/minirc.* /etc/minicom/minirc.* ~/.minirc.*' + ((${#files[@]})) && + COMPREPLY=($(compgen -W '"${files[@]##*minirc.}"' -- "$cur")) } && complete -F _minicom -o default minicom diff --git a/completions/mysql b/completions/mysql index 9084efe08f6..a8f011ea887 100644 --- a/completions/mysql +++ b/completions/mysql @@ -2,13 +2,12 @@ _comp_xfunc_mysql_character_sets() { - local IFS=$' \t\n' reset=$(shopt -p failglob) - shopt -u failglob - local -a charsets=(/usr/share/m{ariadb,ysql}/charsets/*.xml) - $reset + local -a charsets + _comp_expand_glob charsets '/usr/share/m{ariadb,ysql}/charsets/*.xml' if ((${#charsets[@]})); then charsets=("${charsets[@]##*/}") - charsets=("${charsets[@]%%?(Index|\*).xml}" utf8) + charsets=("${charsets[@]%%?(Index).xml}" utf8) + local IFS=$'\n' COMPREPLY+=($(compgen -W '${charsets[@]}' -- "$cur")) fi } diff --git a/completions/ps b/completions/ps index c3d1e06acc6..8c8b5833074 100644 --- a/completions/ps +++ b/completions/ps @@ -39,10 +39,7 @@ _comp_cmd_ps() return ;; ?(-)t | [^-]*t | --tty) - COMPREPLY=($( - shopt -s nullglob - printf '%s\n' /dev/tty* - )) + _comp_expand_glob COMPREPLY '/dev/tty*' ((${#COMPREPLY[@]})) && COMPREPLY=($(compgen -W '"${COMPREPLY[@]}" "${COMPREPLY[@]#/dev/}"' \ -- "$cur")) diff --git a/completions/qemu b/completions/qemu index 325c61386a0..8d83d4f97ac 100644 --- a/completions/qemu +++ b/completions/qemu @@ -20,13 +20,8 @@ _qemu() return ;; -k) - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - local -a keymaps=($( - printf "%s\n" \ - /usr/{local/,}share/qemu/keymaps/!(common|modifiers) - )) - $reset + local -a keymaps + _comp_expand_glob keymaps '/usr/{local/,}share/qemu/keymaps/!(common|modifiers)' ((${#keymaps[@]})) && COMPREPLY=($(compgen -W '"${keymaps[@]##*/}"}' -- "$cur")) return diff --git a/completions/screen b/completions/screen index ca21a9ac8d9..b97c3545c77 100644 --- a/completions/screen +++ b/completions/screen @@ -28,12 +28,9 @@ _screen_sessions() if ((cword == 1)); then if [[ $cur == /dev* ]]; then - COMPREPLY=($(compgen -W "$( - shopt -s nullglob - printf '%s\n' \ - /dev/serial/*/* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null - )" \ - -- "$cur")) + _comp_expand_glob COMPREPLY '/dev/serial/*/* /dev/ttyUSB* /dev/ttyACM*' + ((${#COMPREPLY[@]})) && + COMPREPLY=($(compgen -W '"${COMPREPLY[@]}"' -- "$cur")) return fi if [[ $cur == //* ]]; then diff --git a/completions/valgrind b/completions/valgrind index 2afc8d8fba7..fb5f4c4e574 100644 --- a/completions/valgrind +++ b/completions/valgrind @@ -31,12 +31,14 @@ _valgrind() --tool) # Tools seem to be named e.g. like memcheck-amd64-linux from which # we want to grab memcheck. - COMPREPLY=($(compgen -W '$( - for f in /usr{,/local}/lib{,64,exec}{/*-linux-gnu,}/valgrind/* - do - [[ $f != *.so && -x $f && $f =~ ^.*/(.*)-[^-]+-[^-]+ ]] && - printf "%s\n" "${BASH_REMATCH[1]}" - done)' -- "$cur")) + local -a files + _comp_expand_glob files '/usr{,/local}/lib{,64,exec}{/*-linux-gnu,}/valgrind/*' + ((${#files[@]})) && + COMPREPLY=($(compgen -W '$( + for f in "${files[@]}"; do + [[ $f != *.so && -x $f && $f =~ ^.*/(.*)-[^-]+-[^-]+ ]] && + printf "%s\n" "${BASH_REMATCH[1]}" + done)' -- "$cur")) return ;; --sim-hints) diff --git a/completions/vpnc b/completions/vpnc index a34b46bd842..d9abfd3cbd6 100644 --- a/completions/vpnc +++ b/completions/vpnc @@ -66,14 +66,12 @@ _vpnc() _filedir conf else # config name, /etc/vpnc/.conf - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - local -a configs=(/etc/vpnc/*.conf) - $reset + local -a configs + _comp_expand_glob configs '/etc/vpnc/*.conf' if ((${#configs[@]})); then configs=("${configs[@]##*/}") configs=("${configs[@]%.conf}") - IFS=$'\n' + local IFS=$'\n' compopt -o filenames COMPREPLY=($(compgen -W '${configs[@]}' -- "$cur")) fi diff --git a/completions/xdg-mime b/completions/xdg-mime index cf94f3f4ce5..44b50968698 100644 --- a/completions/xdg-mime +++ b/completions/xdg-mime @@ -38,13 +38,11 @@ _xdg_mime() ;; default) if ((args == 2)); then - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - local -a desktops=(/usr/share/applications/*.desktop) - $reset + local -a desktops + _comp_expand_glob desktops '/usr/share/applications/*.desktop' if ((${#desktops[@]})); then desktops=("${desktops[@]##*/}") - IFS=$'\n' + local IFS=$'\n' COMPREPLY=($(compgen -W '"${desktops[@]}"' -- "$cur")) fi else From 9dd2eabc1c104f1379bd5da8449e1aae1e5cce02 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Fri, 25 Feb 2022 19:27:08 +0900 Subject: [PATCH 6/8] fix(mysql): ensure generating "utf8" for charset When there are no "charset" files, nothing has been generated although we intended to at least generate "utf8". The check for the number of generated candidates should be performed after generating "utf8". The wrong check has been introduced in commit 08dd2cdb3. Originally, we have been performing the pathname expansions without adjusting "nullglob", so the pattern "/usr/share/mysql/charsets/*.xml" has been directly strayed into the array "charsets" unless "nullgob" is set. Then the check has been always passed when "nullglob" is not set, which is the reason that we have been missing the problem until now. --- completions/mysql | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/completions/mysql b/completions/mysql index a8f011ea887..1ebc30a0216 100644 --- a/completions/mysql +++ b/completions/mysql @@ -4,12 +4,11 @@ _comp_xfunc_mysql_character_sets() { local -a charsets _comp_expand_glob charsets '/usr/share/m{ariadb,ysql}/charsets/*.xml' - if ((${#charsets[@]})); then - charsets=("${charsets[@]##*/}") - charsets=("${charsets[@]%%?(Index).xml}" utf8) - local IFS=$'\n' - COMPREPLY+=($(compgen -W '${charsets[@]}' -- "$cur")) - fi + charsets+=(utf8) + charsets=("${charsets[@]##*/}") + charsets=("${charsets[@]%%?(Index).xml}") + local IFS=$'\n' + COMPREPLY+=($(compgen -W '${charsets[@]}' -- "$cur")) } _comp_deprecate_func _mysql_character_sets _comp_xfunc_mysql_character_sets From b88152ef61a13b3efd899830b7199b6aa9d2f9ea Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 26 Feb 2022 10:26:15 +0900 Subject: [PATCH 7/8] fix(mysql): do not generate empty completion for charset * Instead of replacing "Index.xml" with an empty string after the pathname expansions, we now exclude "Index.xml" from the pathname pattern. * In case that there is an empty filename as "/usr/share/mysql/charsets/.xml", we explicitly exclude empty charset names by specifying the option, -X '', for compgen. --- completions/mysql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/completions/mysql b/completions/mysql index 1ebc30a0216..97df733b195 100644 --- a/completions/mysql +++ b/completions/mysql @@ -3,12 +3,12 @@ _comp_xfunc_mysql_character_sets() { local -a charsets - _comp_expand_glob charsets '/usr/share/m{ariadb,ysql}/charsets/*.xml' + _comp_expand_glob charsets '/usr/share/m{ariadb,ysql}/charsets/!(Index).xml' charsets+=(utf8) charsets=("${charsets[@]##*/}") - charsets=("${charsets[@]%%?(Index).xml}") + charsets=("${charsets[@]%.xml}") local IFS=$'\n' - COMPREPLY+=($(compgen -W '${charsets[@]}' -- "$cur")) + COMPREPLY+=($(compgen -W '${charsets[@]}' -X '' -- "$cur")) } _comp_deprecate_func _mysql_character_sets _comp_xfunc_mysql_character_sets From ec907d93c3e6bc94c5fa64c50abf86fd2e007378 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Mon, 18 Apr 2022 05:46:13 +0900 Subject: [PATCH 8/8] refactor(ipmitool): filter -d optargs by compgen -X --- completions/ipmitool | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/completions/ipmitool b/completions/ipmitool index d0b8ec0c7ae..bb65d8f86aa 100644 --- a/completions/ipmitool +++ b/completions/ipmitool @@ -19,11 +19,7 @@ _ipmitool() local -a files _comp_expand_glob files '/dev/ipmi* /dev/ipmi/* /dev/ipmidev/*' ((${#files[@]})) && - COMPREPLY=($(compgen -W "$( - printf '%s\n' "${files[@]}" | - command sed -ne 's/^[^0-9]*\([0-9]\{1,\}\)/\1/p' - )" \ - -- "$cur")) + COMPREPLY=($(compgen -W '"${files[@]##*([^0-9])}"' -X '![0-9]*' -- "$cur")) return ;; -*I)