Skip to content

Commit 021c030

Browse files
authored
Merge pull request #736 from akinomyoga/_comp_dequote
refactor(bash_completion): refactor `{ => _comp_}{dequote,quote}`
2 parents 03e60a2 + 9225cd0 commit 021c030

File tree

10 files changed

+341
-54
lines changed

10 files changed

+341
-54
lines changed

bash_completion

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ _comp_readline_variable_on()
148148
_comp_deprecate_func _rl_enabled _comp_readline_variable_on
149149

150150
# This function shell-quotes the argument
151+
# @param $1 String to be quoted
152+
# @var[out] ret Resulting string
153+
_comp_quote()
154+
{
155+
ret=\'${1//\'/\'\\\'\'}\'
156+
}
157+
158+
# This function shell-quotes the argument
159+
# @deprecated Use `_comp_quote` instead. Note that `_comp_quote` stores
160+
# the results in the variable `ret` instead of writing them to stdout.
151161
quote()
152162
{
153163
local quoted=${1//\'/\'\\\'\'}
@@ -162,10 +172,77 @@ quote_readline()
162172
printf %s "$ret"
163173
} # quote_readline()
164174

175+
# shellcheck disable=SC1003
176+
_comp_dequote__initialize()
177+
{
178+
unset -f "$FUNCNAME"
179+
local regex_param='\$([_a-zA-Z][_a-zA-Z0-9]*|[-*@#?$!0-9_])|\$\{[!#]?([_a-zA-Z][_a-zA-Z0-9]*(\[([0-9]+|[*@])\])?|[-*@#?$!0-9_])\}'
180+
local regex_quoted='\\.|'\''[^'\'']*'\''|\$?"([^\"$`!]|'$regex_param'|\\.)*"|\$'\''([^\'\'']|\\.)*'\'''
181+
_comp_dequote__regex_safe_word='^([^\'\''"$`;&|<>()!]|'$regex_quoted'|'$regex_param')*$'
182+
}
183+
_comp_dequote__initialize
184+
185+
# This function expands a word using `eval` in a safe way. This function can
186+
# be typically used to get the expanded value of `${word[i]}` as
187+
# `_comp_dequote "${word[i]}"`. When the word contains unquoted shell special
188+
# characters, command substitutions, and other unsafe strings, the function
189+
# call fails before applying `eval`. Otherwise, `eval` is applied to the
190+
# string to generate the result.
191+
#
192+
# @param $1 String to be expanded. A safe word consists of the following
193+
# sequence of substrings:
194+
#
195+
# - Shell non-special characaters: [^\'"$`;&|<>()!].
196+
# - Parameter expansions of the forms $PARAM, ${!PARAM},
197+
# ${#PARAM}, ${NAME[INDEX]}, ${!NAME[INDEX]}, ${#NAME[INDEX]}
198+
# where INDEX is an integer, `*` or `@`, NAME is a valid
199+
# variable name [_a-zA-Z][_a-zA-Z0-9]*, and PARAM is NAME or a
200+
# parameter [-*@#?$!0-9_].
201+
# - Quotes \?, '...', "...", $'...', and $"...". In the double
202+
# quotations, parameter expansions are allowed.
203+
#
204+
# @var[out] ret Array that contains the expanded results. Multiple words or no
205+
# words may be generated through pathname expansions.
206+
#
207+
# Note: This function allows parameter expansions as safe strings, which might
208+
# cause unexpected results:
209+
#
210+
# * This allows execution of arbitrary commands through extra expansions of
211+
# array subscripts in name references. For example,
212+
#
213+
# declare -n v='dummy[$(echo xxx >/dev/tty)]'
214+
# echo "$v" # This line executes the command 'echo xxx'.
215+
# _comp_dequote '"$v"' # This line also executes it.
216+
#
217+
# * This may change the internal state of the variable that has side effects.
218+
# For example, the state of the random number generator of RANDOM can change:
219+
#
220+
# RANDOM=1234 # Set seed
221+
# echo "$RANDOM" # This produces 30658.
222+
# RANDOM=1234 # Reset seed
223+
# _comp_dequote '"$RANDOM"' # This line changes the internal state.
224+
# echo "$RANDOM" # This fails to reproduce 30658.
225+
#
226+
# We allow these parameter expansions as a part of safe strings assuming the
227+
# referential transparency of the simple parameter expansions and the sane
228+
# setup of the variables by the user or other frameworks that the user loads.
229+
_comp_dequote()
230+
{
231+
ret=() # fallback value for unsafe word and failglob
232+
[[ $1 =~ $_comp_dequote__regex_safe_word ]] || return 1
233+
eval "ret=($1)" 2>/dev/null # may produce failglob
234+
}
235+
165236
# This function shell-dequotes the argument
237+
# @deprecated Use `_comp_dequote' instead. Note that `_comp_dequote` stores
238+
# the results in the array `ret` instead of writing them to stdout.
166239
dequote()
167240
{
168-
eval printf %s "$1" 2>/dev/null
241+
local ret
242+
_comp_dequote "$1"
243+
local rc=$?
244+
printf %s "$ret"
245+
return $rc
169246
}
170247

171248
# Unset the given variables across a scope boundary. Useful for unshadowing
@@ -1030,14 +1107,14 @@ _parse_help()
10301107
shopt -s lastpipe
10311108
set -o noglob
10321109
1033-
eval local cmd="$(quote "$1")"
1110+
local cmd=$1
10341111
local line rc=1
1035-
{
1112+
(
10361113
case $cmd in
1037-
-) cat ;;
1038-
*) LC_ALL=C "$(dequote "$cmd")" ${2:---help} 2>&1 ;;
1114+
-) exec cat ;;
1115+
*) _comp_dequote "$cmd" && LC_ALL=C "$ret" ${2:---help} 2>&1 ;;
10391116
esac
1040-
} |
1117+
) |
10411118
while read -r line; do
10421119
10431120
[[ $line == *([[:blank:]])-* ]] || continue
@@ -1067,14 +1144,14 @@ _parse_usage()
10671144
shopt -s lastpipe
10681145
set -o noglob
10691146
1070-
eval local cmd="$(quote "$1")"
1147+
local cmd=$1
10711148
local line match option i char rc=1
1072-
{
1149+
(
10731150
case $cmd in
1074-
-) cat ;;
1075-
*) LC_ALL=C "$(dequote "$cmd")" ${2:---usage} 2>&1 ;;
1151+
-) exec cat ;;
1152+
*) _comp_dequote "$cmd" && LC_ALL=C "$ret" ${2:---usage} 2>&1 ;;
10761153
esac
1077-
} |
1154+
) |
10781155
while read -r line; do
10791156
10801157
while [[ $line =~ \[[[:space:]]*(-[^]]+)[[:space:]]*\] ]]; do

completions/_umount.linux

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ _reply_compgen_array()
1515
# argument.
1616
local i wlist
1717
for i in ${!COMPREPLY[*]}; do
18-
local q=$(quote "$(printf %q "${COMPREPLY[i]}")")
19-
wlist+=$q$'\n'
18+
local ret
19+
printf -v ret %q "${COMPREPLY[i]}"
20+
_comp_quote "$ret"
21+
wlist+=$ret$'\n'
2022
done
2123

2224
# We also have to add another round of escaping to $cur.

completions/export

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ _export()
3030

3131
case $cur in
3232
*=)
33-
local pval=$(quote "$(eval printf %s \"\$\{${cur%=}-\}\")")
33+
local pname=${cur%=}
34+
local ret
35+
_comp_quote "${!pname-}"
36+
local pval=$ret
3437
# Complete previous value if it's not empty.
3538
if [[ $pval != \'\' ]]; then
3639
COMPREPLY=("$pval")

completions/make

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,11 @@ _make()
135135
# with -C/--directory
136136
for ((i = 1; i < ${#words[@]}; i++)); do
137137
if [[ ${words[i]} == -@(C|-directory) ]]; then
138-
# eval for tilde expansion
139-
eval "makef_dir=( -C \"${words[i + 1]}\" )"
138+
# Expand tilde expansion
139+
local ret
140+
_comp_dequote "${words[i + 1]-}" &&
141+
[[ -d ${ret-} ]] &&
142+
makef_dir=(-C "$ret")
140143
break
141144
fi
142145
done
@@ -145,8 +148,11 @@ _make()
145148
# specified with -f/--file/--makefile
146149
for ((i = 1; i < ${#words[@]}; i++)); do
147150
if [[ ${words[i]} == -@(f|-?(make)file) ]]; then
148-
# eval for tilde expansion
149-
eval "makef=( -f \"${words[i + 1]}\" )"
151+
# Expand tilde expansion
152+
local ret
153+
_comp_dequote "${words[i + 1]-}" &&
154+
[[ -f ${ret-} ]] &&
155+
makef=(-f "$ret")
150156
break
151157
fi
152158
done

completions/mutt

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,24 @@ _muttaddr()
1616
# @output muttrc filename
1717
_muttrc()
1818
{
19+
local muttrc=
1920
# Search COMP_WORDS for '-F muttrc' or '-Fmuttrc' argument
2021
set -- "${words[@]}"
2122
while (($# > 0)); do
2223
if [[ $1 == -F* ]]; then
24+
local ret
2325
if ((${#1} > 2)); then
24-
muttrc="$(dequote "${1:2}")"
26+
_comp_dequote "${1:2}" && muttrc=$ret
2527
else
2628
shift
27-
[[ $1 ]] && muttrc="$(dequote "$1")"
29+
[[ ${1-} ]] && _comp_dequote "$1" && muttrc=$ret
2830
fi
2931
break
3032
fi
3133
shift
3234
done
3335

34-
if [[ ! -v muttrc ]]; then
36+
if [[ ! $muttrc ]]; then
3537
if [[ -f ~/.${muttcmd}rc ]]; then
3638
muttrc=\~/.${muttcmd}rc
3739
elif [[ -f ~/.${muttcmd}/${muttcmd}rc ]]; then
@@ -42,27 +44,48 @@ _muttrc()
4244
}
4345

4446
# Recursively build list of sourced config files
45-
# @param $1 List of config files found so far
46-
# @param $2 Config file to process
47-
# @output List of config files
48-
_muttconffiles()
47+
# @param $1... Config file to process
48+
# @var[out] ret List of config files
49+
# @return 0 if any conffiles are generated, 1 if none is generated.
50+
_comp_cmd_mutt__get_conffiles()
4951
{
50-
local file sofar
51-
local -a newconffiles
52-
53-
sofar=" $1 "
54-
shift
55-
while [[ ${1-} ]]; do
56-
newconffiles=($(command sed -n 's|^source[[:space:]]\{1,\}\([^[:space:]]\{1,\}\).*$|\1|p' "$(eval printf %s $1)"))
57-
for file in ${newconffiles+"${newconffiles[@]}"}; do
58-
__expand_tilde_by_ref file
59-
[[ ! -f $file || $sofar == *\ $file\ * ]] && continue
60-
sofar+=" $file"
61-
sofar=" $(eval _muttconffiles \"$sofar\" $file) "
62-
done
63-
shift
52+
local -a conffiles=()
53+
local -A visited=()
54+
local file
55+
for file; do
56+
_comp_dequote "$file"
57+
_comp_cmd_mutt__get_conffiles__visit "$ret"
58+
done
59+
((${#conffiles[@]})) || return 1
60+
ret=("${conffiles[@]}")
61+
}
62+
# Recursion function for _comp_cmd_mutt__get_conffiles
63+
# @var[ref] conffiles List of config files found so far
64+
# @var[ref] visited Dictionary of config files already visited
65+
_comp_cmd_mutt__get_conffiles__visit()
66+
{
67+
[[ -f $1 && ${visited[$1]-} != yes ]] || return 0
68+
visited[$1]=yes
69+
conffiles+=("$1")
70+
71+
local -a newconffiles=($(command sed -n 's|^source[[:space:]]\{1,\}\([^[:space:]]\{1,\}\).*$|\1|p' "$1"))
72+
((${#newconffiles[@]})) || return 0
73+
74+
local file
75+
for file in "${newconffiles[@]}"; do
76+
__expand_tilde_by_ref file
77+
_comp_cmd_mutt__get_conffiles__visit "$file"
6478
done
65-
printf '%s\n' $sofar
79+
}
80+
81+
# Recursively build list of sourced config files
82+
# @param $1... Config file to process
83+
# @output List of config files
84+
_muttconffiles()
85+
{
86+
local ret
87+
_comp_cmd_mutt__get_conffiles "$@" &&
88+
printf '%s\n' "${ret[@]}"
6689
}
6790

6891
# @param $1 (cur) Current word to complete
@@ -74,11 +97,14 @@ _muttaliases()
7497
muttrc=$(_muttrc)
7598
[[ ! $muttrc ]] && return
7699

77-
conffiles=($(eval _muttconffiles $muttrc $muttrc))
100+
local ret
101+
_comp_cmd_mutt__get_conffiles "$muttrc" || return 0
102+
conffiles=("${ret[@]}")
78103
# shellcheck disable=SC2046
79-
aliases=("$(command sed -n 's|^alias[[:space:]]\{1,\}\([^[:space:]]\{1,\}\).*$|\1|p' \
80-
"${conffiles[@]}")")
81-
COMPREPLY+=($(compgen -W "${aliases[*]}" -- "$cur"))
104+
aliases=($(command sed -n 's|^alias[[:space:]]\{1,\}\([^[:space:]]\{1,\}\).*$|\1|p' \
105+
"${conffiles[@]}"))
106+
((${#aliases[@]})) &&
107+
COMPREPLY+=($(compgen -W '"${aliases[@]}"' -- "$cur"))
82108
}
83109

84110
# @param $1 (cur) Current word to complete

completions/pkgadd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ _pkgadd()
4949
done
5050
pkginst_list="${tmplist[*]}"
5151
else
52-
pkginst_list="$(strings "$(dequote $device)" |
52+
local ret
53+
_comp_dequote "$device"
54+
pkginst_list="$(strings "$ret" |
5355
command grep ^PKG= | sort -u | cut -d= -f2)"
5456
fi
5557
COMPREPLY=($(compgen -W "$pkginst_list" -- ${cur}))

completions/pkgutil

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ _pkgutil()
2929
local catalog=$(_pkgutil_url2catalog "$url")
3030
catalog_files=("$catalog")
3131
elif [[ ${words[i]} == --config ]]; then
32-
configuration_files=("$(dequote ${words[i + 1]})")
32+
local ret
33+
_comp_dequote "${words[i + 1]}"
34+
[[ ${ret-} ]] && configuration_files=("$ret")
3335
elif [[ ${words[i]} == -@([iurdacUS]|-install|-upgrade|-remove|-download|-available|-compare|-catalog|-stream) ]]; then
3436
command="${words[i]}"
3537
fi

completions/ssh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,12 @@ _ssh_configfile()
233233
set -- "${words[@]}"
234234
while (($# > 0)); do
235235
if [[ $1 == -F* ]]; then
236+
local ret
236237
if ((${#1} > 2)); then
237-
configfile="$(dequote "${1:2}")"
238+
_comp_dequote "${1:2}" && configfile=$ret
238239
else
239240
shift
240-
[[ ${1-} ]] && configfile="$(dequote "$1")"
241+
[[ ${1-} ]] && _comp_dequote "$1" && configfile=$ret
241242
fi
242243
break
243244
fi

0 commit comments

Comments
 (0)