An introduction to bash completion: part 2

Previously we showed how to add basic completion to commands, using facilities which were already provided by the bash completion routines. In this second part we'll demonstrate how to add completely new custom completion to commands.

In part one we looked at adding hostname completion to arbitrary commands by executing:

complete -F _known_hosts xvncviewer

This uses the complete command to tell bash that the function _known_hosts should be used to handle the completion of arguments to the xvncviewer.

If we wish to add custom completion to a command we will instead write our own function, and bind that to the command.

A Basic Example

As a basic example we'll first look at adding some simple completions to the binary foo. This hypothetical command takes three arguments:

--help

Shows the help options for foo, and exits.

--version

Shows the version of the foo command, and exits.

--verbose

Runs foo with extra verbosity

To handle these arguments we'll create a new file /etc/bash_completion.d/foo. This file will be automatically sourced (or loaded) when the bash completion code is loaded.

If you experiment you'll see that it successfully completes the arguments as expected. Type "foo --h[TAB]" and the --help argument is completed. Press [TAB] a few times and you'll see all the options. (In this case it doesn't actually matter if you don't have a binary called foo installed upon your system.)

So now that we have something working we should look at how it actually works!

How Completion Works

The previous example showed a simple bash function which was invoked to handle completion for a command.

This function starts out by defining some variables cur being the current word being typed, prev being the previous word typed, and opts which is our list of options to complete.

The option completing is then handled by use of the compgen command via this line:

COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )

What this does is set the value of $COMPREPLY to the output of running the command:

compgen -W "${opts}" -- ${cur}

If you replace these variables with their contents you'll see how that works:

compgen -W "--help --verbose --version" -- "userinput"

This command attempts to return the match of the current word "${cur}" against the list "--help --verbose --version". If you run this command in a shell you'll be able to experiment with it and see how it works:

Here you first see what happens if the user enters just "--" - all three options match so they are returned. In the second attempt the user enters --h and this is enough to specify --help unambiguously, so that is returned.

In our function we simply set "COMPREPLY" to this result, and return. This allows bash to replace our current word with the output. COMPREPLY is a special variable which has a particular meaning within bash. Inside completion routines it is used to denote the output of the completion attempt.

An array variable from which Bash reads the possible completions generated by a shell function invoked by the programmable completion facility

We can also see how we found the current word using the array COMP_WORDS to find both the current and the previous word by looking them up:

COMP_WORDS

An array variable consisting of the individual words in the current command line. This variable is available only in shell functions invoked by the programmable completion facilities.

COMP_CWORD

An index into ${COMP_WORDS} of the word containing the current cursor position. This variable is available only in shell functions invoked by the programmable completion facilities

A Complex Example

Many commands are more complicated to fill out, and have numerous options which depend upon their previous ones.

As a relevant example Xen ships with a command called xm this has some basic options:

xm list

List all running Xen instances

xm create ConfigName

Create a new Xen instances using the configuration file in /etc/xen called ConfigName.

xm console Name

Connect to the console of the running machine named "Name".

In general the command is "xm operation args" where "args" varies depending upon the initial operation selected.

Setting up basic completion of inital operation can be handled in much the same way as our previous example the only difference is that the operations don't start with a "--" prefix. However completing the arguments requires special handling.

If you recall we have access to the previous token upon the command line, and using that we can take different actions for each operation.

Here we've setup the initial completion of the operations and then added special handling for the two operations "create" and "console". In both cases we use compgen to complete the input based upon the text that is supplied by the user, compared against a dynamically created list.

For the "console" operation we complete based upon the output of this command:

Using these examples you should now be able to create your own custom completion functions. 95% of the time you only need to complete from a set of available options, the other times you'll need to deal with dynamic argument generation much like we did for the xm command.

Breaking the options down into a small collection of pipes and testing them outside the completion environment (just in the shell) is probably the best approach, then once you've got a working command line just paste it into your function.

bash-completion doesn't do anything out of the box. You distro will ship bash-completion scripts with many packages by default so you almost never need to add your own unless it's for software or scripts you've written yourself.

Bash completion fails to complete "apt-i" with a list of available pkgs. And, since there's no "_available_pkgs" built-in function, it appears a one-line "complete -F" won't work here. That leaves me with adding my own custom completion and using the COMREPLY function listed in /etc/bash_completion. Easy enough. But, is there an even easier way? Should I create my alias a different way? Or setup a custom completion that uses the built-in as much as possible?

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