Skip to content

fix: work around shopt -s patsub_replacement newly supported in Bash 5.2 #1074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 24, 2023
Merged
13 changes: 7 additions & 6 deletions bash_completion
Original file line number Diff line number Diff line change
Expand Up @@ -1572,8 +1572,8 @@ _comp_compgen_usage()
_comp_compgen_signals()
{
local -a sigs
_comp_compgen -v sigs -c "SIG${cur#"${1-}"}" -- -P "${1-}" -A signal &&
_comp_compgen -U sigs set "${sigs[@]/#${1-}SIG/${1-}}"
_comp_compgen -v sigs -c "SIG${cur#"${1-}"}" -- -A signal &&
_comp_compgen -RU sigs -- -P "${1-}" -W '"${sigs[@]#SIG}"'
}

# This function completes on known mac addresses
Expand Down Expand Up @@ -2047,9 +2047,8 @@ _comp_compgen_usergroups()
if ((${#tmp[@]})); then
local _prefix=${cur%%*([^:])}
_prefix=${_prefix//\\/}
local -a _tmp=("${tmp[@]/#/$_prefix}")
_comp_unlocal tmp
_comp_compgen_set "${_tmp[@]}"
_comp_compgen -Rv tmp -- -P "$_prefix" -W '"${tmp[@]}"'
_comp_compgen -U tmp set "${tmp[@]}"
fi
elif [[ $cur == *:* ]]; then
# Completing group after 'user:gr<TAB>'.
Expand Down Expand Up @@ -2455,7 +2454,9 @@ _comp__included_ssh_config_files()
# -p PREFIX Use PREFIX
# -4 Filter IPv6 addresses from results
# -6 Filter IPv4 addresses from results
# @return Completions, starting with CWORD, are added to COMPREPLY[]
# @var[out] COMPREPLY Completions, starting with CWORD, are added
# @return True (0) if one or more completions are generated, or otherwise False
# (1).
# @since 2.12
_comp_compgen_known_hosts()
{
Expand Down
4 changes: 2 additions & 2 deletions completions/_mount.linux
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ _comp_cmd_mount()
ufs umsdos usbfs vfat xfs'
_comp_compgen -a fstypes
[[ $split ]] && ((${#COMPREPLY[@]})) &&
COMPREPLY=(${COMPREPLY[@]/#/$prev,})
_comp_compgen -Rv COMPREPLY -- -P "$prev," -W '"${COMPREPLY[@]}"'
return
;;
--bind | -B | --rbind | -R)
Expand Down Expand Up @@ -204,7 +204,7 @@ _comp_cmd_mount()
# COMP_WORDBREAKS is a real pain in the ass
prev="${prev##*["$COMP_WORDBREAKS"]}"
[[ $split ]] && ((${COMPREPLY[@]})) &&
COMPREPLY=(${COMPREPLY[@]/#/"$prev,"})
_comp_compgen -Rv COMPREPLY -- -P "$prev," -W '"${COMPREPLY[@]}"'
[[ ${COMPREPLY-} == *= ]] && compopt -o nospace
return
;;
Expand Down
2 changes: 1 addition & 1 deletion completions/_umount.linux
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ _comp_cmd_umount()
usbfs vfat xfs'
_comp_compgen -a fstypes
[[ $split ]] && ((${#COMPREPLY[@]})) &&
COMPREPLY=(${COMPREPLY[@]/#/$prev,})
_comp_compgen -Rv COMPREPLY -- -P "$prev," -W '"${COMPREPLY[@]}"'
return
;;
-O)
Expand Down
4 changes: 2 additions & 2 deletions completions/chromium-browser
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ _comp_cmd_chromium_browser()
case $cur in
*://*)
local prefix="${cur%%://*}://"
_comp_compgen_known_hosts -- "${cur#*://}"
COMPREPLY=("${COMPREPLY[@]/#/$prefix}")
_comp_compgen_known_hosts -- "${cur#*://}" &&
_comp_compgen -Rv COMPREPLY -- -P "$prefix" -W '"${COMPREPLY[@]}"'
_comp_ltrim_colon_completions "$cur"
;;
*)
Expand Down
5 changes: 3 additions & 2 deletions completions/cppcheck
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ _comp_cmd_cppcheck()
split="set"
fi
_comp_compgen -- -W 'all warning style performance portability
information unusedFunction missingInclude'
[[ $split ]] && COMPREPLY=(${COMPREPLY[@]/#/"$prev,"})
information unusedFunction missingInclude' &&
[[ $split ]] &&
_comp_compgen -Rv COMPREPLY -- -P "$prev," -W '"${COMPREPLY[@]}"'
return
;;
--error-exitcode)
Expand Down
4 changes: 2 additions & 2 deletions completions/cvs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ _comp_cmd_cvs__entries()
local prefix=${cur%/*}/ IFS=$'\n'
[[ -e ${prefix-}CVS/Entries ]] || prefix=""
entries=($(cut -d/ -f2 -s "${prefix-}CVS/Entries" 2>/dev/null))
if [[ $entries ]]; then
entries=("${entries[@]/#/${prefix-}}")
if ((${#entries[@]})); then
_comp_compgen -Rv entries -- -P "${prefix-}" -W '"${entries[@]}"'
compopt -o filenames
fi
}
Expand Down
2 changes: 1 addition & 1 deletion completions/info
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ _comp_cmd_info()

_comp_split -F : infopath "$infopath"
if ((${#infopath[@]})); then
infopath=("${infopath[@]/%//$cur*}")
_comp_compgen -Rv infopath -- -S "/$cur*" -W '"${infopath[@]}"'
local IFS=
_comp_expand_glob COMPREPLY '${infopath[@]}'
_comp_unlocal IFS
Expand Down
4 changes: 2 additions & 2 deletions completions/kcov
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ _comp_cmd_kcov()
cur="${cur##*,}"
_comp_compgen -- -W "{0..100}"
((${#COMPREPLY[@]} == 1)) &&
COMPREPLY=(${COMPREPLY/#/$prev,})
_comp_compgen -Rv COMPREPLY -- -P "$prev," -W '"$COMPREPLY"'
else
_comp_compgen -- -W "{0..100}"
((${#COMPREPLY[@]} == 1)) && COMPREPLY=(${COMPREPLY/%/,})
((${#COMPREPLY[@]} == 1)) && COMPREPLY=("${COMPREPLY/%/,}")
compopt -o nospace
fi
return
Expand Down
7 changes: 5 additions & 2 deletions completions/man
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ _comp_cmd_man()

_comp_split -F : manpath "$manpath"
if ((${#manpath[@]})); then
manpath=("${manpath[@]/%//*man$sect/$cur*}" "${manpath[@]/%//*cat$sect/$cur*}")
local manfiles
_comp_compgen -Rv manfiles -- -S "/*man$sect/$cur*" -W '"${manpath[@]}"'
_comp_compgen -aRv manfiles -- -S "/*cat$sect/$cur*" -W '"${manpath[@]}"'

local IFS=
_comp_expand_glob COMPREPLY '${manpath[@]}'
_comp_expand_glob COMPREPLY '${manfiles[@]}'
_comp_unlocal IFS

if ((${#COMPREPLY[@]} != 0)); then
Expand Down
2 changes: 1 addition & 1 deletion completions/mr
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ _comp_cmd_mr()
done
)"
# Split [online|offline] and remove `action` placeholder.
commands="${commands//@(action|[\[\|\]])/$'\n'}"
commands="${commands//@(action|[\[\|\]])/ }"
# Add standard aliases.
commands="${commands} ci co ls"
_comp_split commands "$commands"
Expand Down
5 changes: 4 additions & 1 deletion completions/mutt
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ _comp_cmd_mutt__filedir()
elif [[ $cur == !* ]]; then
spoolfile="$("$muttcmd" -F "$muttrc" -Q spoolfile 2>/dev/null |
command sed -e 's|^spoolfile=\"\(.*\)\"$|\1|')"
[[ $spoolfile ]] && eval cur="${cur/^!/$spoolfile}"
if [[ $spoolfile ]]; then
_comp_dequote "\"$spoolfile\"" && spoolfile=$REPLY
cur=$spoolfile${cur:1}
fi
fi
_comp_compgen -c "$cur" filedir
}
Expand Down
16 changes: 11 additions & 5 deletions completions/povray
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ _comp_cmd_povray()
cur="${povcur#[-+]I}" # to confuse _comp_compgen_filedir
pfx="${povcur%"$cur"}"
_comp_compgen_filedir pov
((${#COMPREPLY[@]})) && COMPREPLY=("${COMPREPLY[@]/#/$pfx}")
((${#COMPREPLY[@]})) &&
_comp_compgen -Rv COMPREPLY -- -P "$pfx" -W '"${COMPREPLY[@]}"'
return
;;
[-+]O*)
Expand All @@ -35,12 +36,16 @@ _comp_cmd_povray()
IFS=$'\n'
command grep '^[-+]I' <<<"${words[*]}"
))
COMPREPLY=(${COMPREPLY[@]#[-+]I})
COMPREPLY=(${COMPREPLY[@]/%.pov/.$oext})
_comp_compgen -Rv COMPREPLY -- -X '' -W '"${COMPREPLY[@]#[-+]I}"'
local i
for i in "${!COMPREPLY[@]}"; do
COMPREPLY[i]=${COMPREPLY[i]/%.pov/".$oext"}
done
cur="${povcur#[-+]O}" # to confuse _comp_compgen_filedir
pfx="${povcur%"$cur"}"
_comp_compgen -a filedir $oext
((${#COMPREPLY[@]})) && COMPREPLY=("${COMPREPLY[@]/#/$pfx}")
((${#COMPREPLY[@]})) &&
_comp_compgen -Rv COMPREPLY -- -P "$pfx" -W '"${COMPREPLY[@]}"'
return
;;
*.ini\[ | *.ini\[*[^]]) # sections in .ini files
Expand All @@ -50,7 +55,8 @@ _comp_cmd_povray()
COMPREPLY=($(command sed -ne \
's/^[[:space:]]*\[\('"$cur"'[^]]*\]\).*$/\1/p' -- "$pfx"))
# to prevent [bar] expand to nothing. can be done more easily?
((${#COMPREPLY[@]})) && COMPREPLY=("${COMPREPLY[@]/#/${pfx}[}")
((${#COMPREPLY[@]})) &&
_comp_compgen -Rv COMPREPLY -- -P "${pfx}[" -W '"${COMPREPLY[@]}"'
return
;;
*)
Expand Down
3 changes: 2 additions & 1 deletion completions/pylint
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ _comp_cmd_pylint()
[[ $cur == *,* ]] && prefix="${cur%,*},"
_comp_compgen -c "${cur##*,}" -- -W "HIGH INFERENCE
INFERENCE_FAILURE UNDEFINED"
((${#COMPREPLY[@]} == 1)) && COMPREPLY=(${COMPREPLY/#/$prefix})
((${#COMPREPLY[@]} == 1)) &&
_comp_compgen -Rv COMPREPLY -- -P "$prefix" -W '"$COMPREPLY"'
return
;;
--format | -${noargopts}f)
Expand Down
4 changes: 2 additions & 2 deletions completions/smartctl
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ _comp_cmd_smartctl__drivedb()
prefix=+
cur="${cur#+}"
fi
_comp_compgen_filedir h
[[ $prefix ]] && COMPREPLY=("${COMPREPLY[@]/#/$prefix}")
_comp_compgen_filedir h && [[ $prefix ]] &&
_comp_compgen -Rv COMPREPLY -- -P "$prefix" -W '"${COMPREPLY[@]}"'
}

_comp_cmd_smartctl()
Expand Down
49 changes: 49 additions & 0 deletions doc/styleguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,52 @@ avoid backslash escaping or use the one that minimizes the use of backslash
escaping. When the value contains control characters such as a tab and a
newline, we do not directly include them but we use backslash escape sequences
such as `\t` and `\n` in the escape string `$'...'`.

### `patsub_replacement` for array elements

There is a subtlety in quoting of the array expansions with a pattern
replacement when `shopt -s patsub_replacement` (Bash >= 5.2) is
enabled (which is the default of Bash >= 5.2).

For example, the array expansions with a pattern replacement may be
used to add a prefix to every element in an array:

```bash
# problem in bash >= 5.2
arr=("${arr[@]/#/$prefix}")
```

However, this has the problem. The characters `&` contained in
`$prefix`, if any, will be replaced with the matched string. The
unexpected `patsub_replacement` may be suppressed by quoting the
replacement as

```bash
# problem with bash <= 4.2 or "shopt -s compat42"
arr=("${arr[@]/#/"$prefix"}")
```

However, this has another problem in bash < 4.3 or when `shopt -s
compat42` is turned on. The inner double quotations are treated
literally so that the `PREFIX` instead of ``"PREFIX"` is prefixed to
elements. To avoid this situation, the outer double quotations might
be removed, but this has even another problem of the pathname
expansions and `IFS`.

Specifically for prefixing and suffixing, we may instead use
`_comp_compgen -- -P prefix` and `_comp_compgen -- -S suffix`.

```bash
# solution for prefixing
_comp_compgen -Rv arr -- -P "$prefix" -W '"${arr[@]}"'
```

In a general case, one needs to modify each array element in a loop,
where only the replacement is quoted.

```bash
# general solution
for i in "${!arr[@]}"; do
arr[i]=${arr[i]//pat/"$rep"}
done
```
9 changes: 9 additions & 0 deletions test/runLint
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ gitgrep '(?<!command)'"$cmdstart"'(grep|ls|sed|cd)(\s|$)' \
gitgrep '(?<!command)'"$cmdstart"'awk(\s|$)' \
'invoke awk through "_comp_awk"'

#------------------------------------------------------------------------------
# Bash pitfalls/styles/compatibilities (which are not detected by shellcheck)

gitgrep '<<<' 'herestrings use temp files, use some other way'

filter_out='^(test/|bash_completion\.sh)' gitgrep ' \[ ' \
Expand All @@ -61,3 +64,9 @@ gitgrep "$cmdstart"'unset [^-]' 'Explicitly specify "unset -v/-f"'

gitgrep "$cmdstart"'((set|shopt)\s+[+-][a-z]+\s+posix\b|(local\s+)?POSIXLY_CORRECT\b)' \
'fiddling with posix mode breaks keybindings with some bash versions'

gitgrep '\$\{([^{}\n]|\{.*\})+/([^{}\n]|\{.*\})+/([^{}"\n]|\{.*\})*\$.*\}' \
'$rep of ${var/pat/$rep} needs to be double-quoted for shopt -s patsub_replacement (bash >= 5.2) [see Sec. of patsub_replacement in doc/styleguide.md]'

gitgrep '"([^"\n]|\\.)*\$\{([^{}\n]|\{.*\})+/([^{}\n]|\{.*\})+/([^{}"\n]|\{.*\})*"([^{}"\n]|\{.*\})*\$.*\}' \
'$rep of "${var/pat/"$rep"}" should not be quoted for bash-4.2 or shopt -s compat42 (bash >= 4.3) [see Sec. of patsub_replacement in doc/styleguide.md]'