diff --git a/bash_completion b/bash_completion index 6fc816740be..a7a4d2f332e 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 @@ -1099,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 @@ -1116,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 } @@ -1373,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 } @@ -1389,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 @@ -1425,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" \ @@ -1677,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")) } @@ -1737,7 +1792,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) @@ -1760,15 +1815,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() 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..bb65d8f86aa 100644 --- a/completions/ipmitool +++ b/completions/ipmitool @@ -16,11 +16,10 @@ _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 '"${files[@]##*([^0-9])}"' -X '![0-9]*' -- "$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..97df733b195 100644 --- a/completions/mysql +++ b/completions/mysql @@ -2,15 +2,13 @@ _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 - if ((${#charsets[@]})); then - charsets=("${charsets[@]##*/}") - charsets=("${charsets[@]%%?(Index|\*).xml}" utf8) - COMPREPLY+=($(compgen -W '${charsets[@]}' -- "$cur")) - fi + local -a charsets + _comp_expand_glob charsets '/usr/share/m{ariadb,ysql}/charsets/!(Index).xml' + charsets+=(utf8) + charsets=("${charsets[@]##*/}") + charsets=("${charsets[@]%.xml}") + local IFS=$'\n' + COMPREPLY+=($(compgen -W '${charsets[@]}' -X '' -- "$cur")) } _comp_deprecate_func _mysql_character_sets _comp_xfunc_mysql_character_sets 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 diff --git a/test/t/conftest.py b/test/t/conftest.py index 4e6df9e870b..7286b73c386 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,19 @@ 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_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 self.noexcept: bool = False @@ -515,7 +521,36 @@ 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] + != bash_env_saved.saved_state.ChangesDetected + ): + return self._safe_assert( '[[ $(shopt -p %s) == "${%s_NEWSHOPT_%s}" ]]' % (name, self.prefix, name), @@ -523,7 +558,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 +573,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 +596,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 +623,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 +632,20 @@ 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 = {} + + 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 @@ -608,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: @@ -616,6 +677,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 +688,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): 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() == ""