bash tab completion with spaces
Custom tab-completing words which might include whitespace is annoyingly difficult. And as far as I know there is no elegant solution. Perhaps some future version of compgen
will be kind enough to produce an array rather than outputting possibilities one line at a time, and even accept the wordlist argument from an array. But until then, the following approach may help.
It's important to understand the problem, which is that ( $(compgen ... ) )
is an array produced by splitting the output of the compgen
command at the characters in $IFS
, which by default is any whitespace character. So if compgen
returns:
roger waters
richard wright
then COMPREPLY
will effectively be set to the array (roger waters richard wright)
, for a total of four possible completions. If you instead use ( "$(compgen ...)")
, then COMPREPLY
will be set to the array ($'roger waters\nrichard wright')
, which has only one possible completion (with a newline inside the completion). Neither of those are what you want.
If none of the possible completions has a newline character, then you could arrange for the compgen
return to be split at the newline character by temporarily resetting IFS
and then restoring it. But I think a more elegant solution is to just use mapfile
:
_test () {
cur=${COMP_WORDS[COMP_CWORD]};
use=`pink`;
## See note at end of answer w.r.t. "$cur" ##
mapfile -t COMPREPLY < <( compgen -W "$use" -- "$cur" )
}
The mapfile
command places the lines sent by compgen
to stdout
into the array COMPREPLY
. (The -t
option causes the trailing newline to be removed from each line, which is almost always what you want when you use mapfile
. See help mapfile
for more options.)
This doesn't deal with the other annoying part of the problem, which is mangling the wordlist into a form acceptable by compgen
. Since compgen
does not allow multiple -W
options, and nor does it accept an array, the only option is to format a string in a such a way that bash
word-splitting (with quotes and all) would generate the desired list. In effect, that means manually adding escapes, as you did in your function pink
:
pink() {
echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}
But that's accident-prone and annoying. A nicer solution would allow the specification of the alternatives directly, particularly if the alternatives are being generated in some fashion. A good way of generating alternatives which might include whitespace is to put them into an array. Given an array, you can make good use of printf
's %q
format to produce a properly-quoted input string for compgen -W
:
# This is a proxy for a database query or some such which produces the alternatives
cat >/tmp/pink <<EOP
nick mason
syd-barrett
david_gilmour
roger waters
richard wright
EOP
# Generate an array with the alternatives
mapfile -t pink </tmp/pink
# Use printf to turn the array into a quoted string:
_test () {
mapfile -t COMPREPLY < <( compgen -W "$(printf '%q ' "${pink[@]}")" -- "$2" )
}
As written, that completion function does not output completions in a form which will be accepted by bash as single words. In other words, the completion roger waters
is generated as roger waters
instead of roger\ waters
. In the (likely) case that the goal is to produce correctly quoted completions, it is necessary to add escapes a second time, after compgen
filters the completion list:
_test () {
declare -a completions
mapfile -t completions < <( compgen -W "$(printf '%q ' "${pink[@]}")" -- "$2" )
local comp
COMPREPLY=()
for comp in "${completions[@]}"; do
COMPREPLY+=("$(printf "%q" "$comp")")
done
}
Note: I replaced the computation of $cur
with $2
, since the function invoked through complete -F
is passed the command as $1
and the word being completed as $2
. (It's also passed the previous word as $3
.) Also, it's important to quote it, so that it doesn't get word-split on its way into compgen
.
If you need to process the data from the string you can use Bash's built-in string replacement operator.
function _test() {
local iter use cur
cur=${COMP_WORDS[COMP_CWORD]}
use="nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
# swap out escaped spaces temporarily
use="${use//\\ /___}"
# split on all spaces
for iter in $use; do
# only reply with completions
if [[ $iter =~ ^$cur ]]; then
# swap back our escaped spaces
COMPREPLY+=( "${iter//___/ }" )
fi
done
}