Bash: completion, introduction

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:

  --> 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:

  1. we create our wordlist like before
  2. we set IFS to \n, so that compgen knows that newlines separate our wordlist entries (since a single entry contains spaces)
  3. 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.

You'll only receive email when they publish something new.

More from Dario Hamidi
All posts