I was reading an old debian-administration.org post about Bash programmable completion, and I had the same question as this person:
I created a script and I use bash_completion, but I cannot figure out how to allow the completion of "--name=value1 --name=value2" (the "=" sign in the middle stop any other completion, and I tried to play with suffixes/prefixes without any success :s
Inspired by that frustrated emoticon (:s
—so expressive!), here's what
I finally found that worked.
Imagine you have a script with an option -n
/ --name
that you can give
a value
in any one of the following:
-n VALUE
(case 1)-nVALUE
(case 2)--name VALUE
(case 3)--name=VALUE
(case 4)
With cases 1 and 3, your job is really easy, and it works just like any Bash programmable completion tutorial you're likely to find on the web.
For case 2, you have to remove the -n
part before passing it to compgen
for completion against the list of possible value
s, then put the -n
back
on; this is discussed in more detail below.
However, in case 4, the "current" option (that's the $cur
variable in the
programmable completion script below) is =
, that's when you have the biggest
challenge. You need to look at two options ago to see if it's --table
before deciding how to proceed.
For both cases 2 and 4, see this gist for an uncuddle
function that will return the VALUE
part of -nVALUE
or --name=VALUE
. This has nothing to do with programmable completion, but you'll find it helpful if you want to be able to parse that kind of option-value pair in your own shell scripts.
Some discussion in the SO thread "Bash completion for path in argument
(with equals sign present)" suggests that messing with COMP_WORDBREAKS
(the way npm once did, that apparently caused other problems) is possibly one
way to tackle the problem. The default value of COMP_WORDBREAKS
on my system
is
"'><=;|&(:
which kind of makes sense why I was having trouble with quote characters in some of my other Bash completion adventures.
The phrase "word breaks" suggest that these would be ignored by the completion
facilities, but no. You totally can get =
as the "current" option for
completion, and have to deal with that by looking at previous options. That's
what the purpose of the upto
array is in the programmable completion script
below.
Another way of stating the above is that you will never get a completion
where $cur
is --name=
; the --name
and the =
are always received as
separate completion candidates.
_myutil() {
local cur prev upto
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# all arguments up to but not including the "current" one; the second
# argument to Bash's array subscripting is length, _not_ ending index, so
# it's not possible to use a negative index to drop items from the end
upto=( "${COMP_WORDS[@]:0:$((${#COMP_WORDS[@]}-1))}" )
# populate the _opts and _values arrays only once per session
[[ $_opts ]] ||
export _opts=(
$( myutil --help | awk '<crazy Awk program to parse out args>' )
)
# theoretical option to list the allowed values for '-n' / '--name'
[[ $_values ]] || export _values=( $( myutil -n list ) )
case "$cur" in
-n*)
# '-n' option completion when the 'value' is "cuddled" with the
# option; must remove the '-t' from the candidate given to
# 'compgen', then have'compgen' put the '-t' back on as a prefix
cur=${cur/-t}
COMPREPLY=( $(compgen -W "${_values[*]}" -P "-t" -- "$cur" ) )
return 0
;;
=)
if [[ $prev == --table ]]; then
cur=${cur/--table=}
COMPREPLY=( $(compgen -W "${_values[*]}" -P "--table=" \
-- "$cur" ) )
return
fi
;;
""|-*)
# complete any 'myutil' options after '<TAB>' ('$cur' is empty)
# or '-<TAB>' but not in the middle of one of the '--name' values
COMPREPLY=( $(compgen -W "${_opts[*]}" -- "$cur") )
return
;;
esac
case "$prev" in
-t|--table)
# the most straightforward case where the option is completely
# separate from its argument; as simple as they get
COMPREPLY=( $(compgen -W "${_values[*]}" -- "$cur" ) )
return
;;
=)
# here, the cursor is already completing a partial 'value', as in
# '--name=val<TAB>'; since '=' is itself a candidate for
# completion (which seems contrary to the meaning of
# COMP_WORDBREAKS, but whatever), you have to check _two_
# arguments ago to see if it was '--table', _then_ proceed
#
# apart from that, it's a very straightforward call to 'compgen'
if [[ ${upto[-2]} == --table ]]; then
COMPREPLY=( $(compgen -W "${_values[*]}" -- "$cur" ) )
return
fi
;;
esac
} # _myutil
complete -F _myutil myutil
As mentioned above, the trick to making it -nvalue
work is to trim off the
option part (-t
(with the ${var/search/repl}
parameter expansion) before passing it to compgen
, but ask
compgen
to add the -t
back on as a prefix (-P
option) when actually
passing the list of completions to the readline library.
That insight was gained from (gasp—reading the manual!) this statement in the "Programmable Completion" section of the manual:
Finally, any prefix and suffix specified with the -P and -S options are added to each member of the completion list, and the result is returned to the Readline completion code as the list of possible completions.
One small downside of the way the above completion script works is that you
can't just use a catch-all default (*)
) case condition to get a list of
program options when completing on an "empty" command line (that is myutil <TAB>
). If the completion were in the middle of one of the allowable values
for -n
/ --name
, you'd wreck that.
The simple workaround requires checking whether $cur
is either an
empty string or begins with a dash (""|-*
).
Include a more detailed discusion of the "crazy Awk script" that parses
command-line options out of the output of myutil --help
.
# when the "usage:" section looks like this:
#
# usage:
# myutil [-?|--help|--help-all] [-x|--examples] [-V|--version]
#
myutil --help \
| awk -F'[][|]' '
/usage:/, /options:/ {
for (i=0; i<NF; i++) {
# if it begins with a dash, this is an option
if ($i ~ /^-/) {
# discard anything after a space (arguments)
split($i,A," "); print A[1]
}
}
}'
# and the "options:" section looks like this:
#
# options:
# -a|--option-a description of "A" option
# -b|--option-b A1,A2,... description of "B" option; takes an argument
# -c|--option-c [ARG] description of "C" option w/ optional argument
#
# Please report bugs to https://gist.github.com/ernstki/569fd38c4d164104b0074a539b689645
#
myutil --help \
| awk -F'[ |]+' '
/options:/, /Please report/ { if (/-.\|/) print $2,$3 }'
- this gist for an
uncuddle
Bash shell function that will return theVALUE
part of-nVALUE
or--name=VALUE
Kevin Ernst ernstki -at- mail.uc.edu
This work is licensed under a Creative Commons Attribution 3.0 Unported License.