I'm having a problem with bash-completion when the possible options may contain spaces.
Let's say I want a function which echoes the first argument:
function test1() {
echo $1
}
I generate a list of possible completion options (some have spaces, some not), but I don't manage to handle spaces correctly.
function pink() {
# my real-world example generates a similar string using awk and other commands
echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}
function _test() {
cur=${COMP_WORDS[COMP_CWORD]}
use=`pink`
COMPREPLY=( $( compgen -W "$use" -- $cur ) )
}
complete -o filenames -F _test test
When I try this, I get:
$ test <tab><tab>
david_gilmour nick roger waters
mason richard syd-barrett wright
$ test r<tab><tab>
richard roger waters wright
which is obviously not what I meant.
If I don't assign an array to COMPREPLY
, i.e. only $( compgen -W "$use" -- $cur )
, I get it working if only one option remains:
$ test n<tab>
$ test nick\ mason <cursor>
But if several options remain, they are all printed within single quotes:
$ test r<tab><tab>
$ test 'roger waters
richard wright' <cursor>
There must be something wrong with my COMPREPLY
variable, but I can't figure out what...
(running bash on solaris, in case that makes a difference...)
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 thecompgen
command at the characters in$IFS
, which by default is any whitespace character. So ifcompgen
returns: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 ...)")
, thenCOMPREPLY
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 resettingIFS
and then restoring it. But I think a more elegant solution is to just usemapfile
:The
mapfile
command places the lines sent bycompgen
tostdout
into the arrayCOMPREPLY
. (The-t
option causes the trailing newline to be removed from each line, which is almost always what you want when you usemapfile
. Seehelp 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
. Sincecompgen
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 thatbash
word-splitting (with quotes and all) would generate the desired list. In effect, that means manually adding escapes, as you did in your functionpink
: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 forcompgen -W
: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 asroger waters
instead ofroger\ waters
. In the (likely) case that the goal is to produce correctly quoted completions, it is necessary to add escapes a second time, aftercompgen
filters the completion list:Note: I replaced the computation of
$cur
with$2
, since the function invoked throughcomplete -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 intocompgen
.Okay, this crazy contraption draws heavily on rici’s solution, and not only fully works, but also quotes any completions that need it, and only those.
So as far as I could test it, it fully implements
ls
-like behavior, minus the path-specific parts.Verbose example
Here’s a more verbose version of the
_test
function, so it becomes a bit more understandable:None of this is even remotely optimized for efficiency. Then again, this is only tab completion, and it’s not causing a noticeable delay for any reasonably large list of completions.
It works by:
awk
output into an array, usingmapfile
.%q
as a separation marker.$cur
, Very important!compgen
. And only if it contains spaces.mapfile
call.-o filenames
.And it only works with all those tricks. It fails if even a single one is missing. Trust me; I’ve tried. ;)
If you need to process the data from the string you can use Bash's built-in string replacement operator.