Bash: completion, introduction
October 26, 2021•782 words
If you have ever used the TAB key in your terminal, you have interacted with your shell's programmable completion facilities.
Sometimes you probably have found existing completions to be lacking sometimes and wondered about how you can add your own. The Bash manual explains this in great detail, but not in a very accessible manner.
The Process
Bash is going through a series of steps to arrive at the list of completion candidates that it is ultimately displaying:
command
--> compspec
comspec actions: -f, -d, -G, -W
--> completion function or command
--> completion filter
--> add prefixes/suffixes
--> present to user
The compspec provides initial matches for a given word and is set up with the complete
builtin.
A completion function or command follows a simple protocol: it receives completion context information through the environment and returns the list of completions, one per line, on stdout.
A special case of this is when the completion function is a Bash function: in this case possible completions can be added to the COMPREPLY
array.
complete and compgen
Bash comes with two builtin functions that control how completion: complete
and compgen
.
In theory you only need complete
: it is the entry point for the completion framework and tells Bash how to attempt completion for a command.
The compgen
builtin is just a convenience for the common case of completion: given a set of available options and a string the user typed, which options start with the string the user typed?
A minimal example
The complete
builtin is a real beast, using almost the full English alphabet for all its single-letter options:
complete: complete [-abcdefgjksuv] [-pr] [-DEI] [-o option] [-A action] [-G globpat] [-W wordlist] [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [name ...]
Let's ignore most of this and focus on a minimal example: we have a function that accept's a human description of a date, like next Thursday
and we want to complete the options for that function.
Under the hood this is just GNU date:
d() {
LC_TIME=C date --date="$*" +%F
}
And then we want to be able to invoke it like this:
d next Fri
d last Thu
d next month
In the simplest case we just want completion to match the input against a list of words and complete
already supports this simple case using the -W
option:
complete -W "$(printf "%q\n" {'next ','last '}{Monday,Mon,Tuesday,Tue,Wednesday,Wed,Thursday,Thu,Friday,Fri,Saturday,Sat,Sunday,Sun})" d
The braces generate all possible permutations of the two sets [next, last]
and [Monday, Mon, ... Sun]
, so in essence our wordlist will look like this:
next Monday
next Mon
...
last Sunday
last Sun
Note that we used the %q
format specifier to make sure the spaces in our completion entries are properly quoted.
Now typing d <TAB>
will cycle through the list and d n<TAB>
will only show the next ...
entries.
Refactoring into a function
The complete
command we've used before is very unwieldy, so let's move completion for d
into a separate function. That also allows us to add more logic to it if we want to support more of date
's syntax.
_comp_d() {
local wordlist=$(printf "%q\n" {'next ','last '}{Mon{,day},Tue{,sday},Wed{,nesday},Thu{,rsday},Fri{,day},Sat{,urday},Sun{,day}})
local IFS=$'\n' # separate wordlist entries by newline
COMPREPLY=( $(compgen -W "$wordlist" "$2") )
}
Let's unpack this:
- we create our wordlist like before
- we set
IFS
to\n
, so thatcompgen
knows that newlines separate our wordlist entries (since a single entry contains spaces) - tell Bash about completion candidates by setting
COMPREPLY
compgen -W "$wordlist" "$2"
takes our wordlist just like before and narrows it down to entries starting with $2
, the word under the cursor.
Anatomy of a completion function
A completion function is invoked with three arguments:
$1
is the command for which completion is attempted,$2
is the word under the cursor,$3
is the word preceding it.
Here are some examples (_
indicates the cursor position):
command $1 $2 $3
d next Fr_ d Fr next
d_ d d
d next_ d next d
Additionally Bash sets a bunch of variables starting with COMP
. Let's inspect them:
_comp_dump() {
printf "\n"
declare -p ${!COMP*}
}
complete -F _comp_dump dump
$ dump example one two<TAB>
declare -- COMP_CWORD="3"
declare -- COMP_KEY="9"
declare -- COMP_LINE="dump example one two"
declare -- COMP_POINT="20"
declare -- COMP_TYPE="37"
declare -- COMP_WORDBREAKS="
\"'><=;|&(:"
declare -a COMP_WORDS=([0]="dump" [1]="example" [2]="one" [3]="two")
In the next article we'll explore how to make use of this information and how to generate useful completions for common development tools.