How Bash completion works

Published October 6, 2019 by Chris Patuzzo

This is the first of two parts on Bash completion. Part two is here

I think ‘completion’ and ‘auto-completion’ mean the same in this context. I’ll use the former for brevity.

Over the years I’ve developed a command-line tool I use for routine tasks such as provisioning my machine, generating project templates and managing secrets. The tool is written in Ruby and I invoke it with the zz command.

Most of what it does is fairly straightforward. The clever bits are usually delegated to something else. For example, zz provision is really just a wrapper that installs and runs Chef, while passing various options to it.

It uses chef-solo which is a slimmed down version of Chef that runs on a single node. No server needed.

Recently, I added Bash completion to my tool. I’ve wanted this for a while, but decided to add it now in preparation for secrets management. For example, I want to be able to type zz secret --readamaz<TAB><TAB> and have it complete to zz secret --read amazon/ . Perhaps hitting <TAB> again will list all secrets under this path, e.g. username, password, access_key, etc.

I haven’t actually implemented secrets management yet. Currently, I’m running gpg --encrypt and --decrypt a lot so hopefully I’ll find time to add this feature soon.

The mechanics

In Bash, completion is handled through the complete ‘built-in’:

$ type complete complete is a shell builtin

Builtins are commands ‘built into’ the shell. They provide basic operations like echo , help and type : $ type type type is a shell builtin

This command allows you to register a method of completion for a command. For example, an rgb command might register its known colors:

$ complete -W "red green blue yellow purple pink orange" color ▲ Setting a hardcoded list of completions

You could then complete color names:

$ color < TAB > < TAB > blue green orange pink purple red yellow $ color p < TAB > < TAB > pink purple $ color pi < TAB >

The -W switch configures a static list of completions that are printed in alphabetical order. It’s just one of the many methods of completion.

It’s also the simplest.

Listing completion methods

To see which commands have completion methods, run complete without arguments:

$ complete complete -W 'red green blue yellow purple pink orange' color complete -F _nodenv nodenv complete -F _rbenv rbenv ▲ Listing all registered completion methods

If you try to complete commands without registered methods, Bash uses its default behaviour and lists files in the current directory.

Here you can see nodenv and rbenv support completion. They use the -F switch to specify functions to handle their completion, namely _nodenv and _rbenv . When you complete one of these commands, their output is context-aware:

$ rbenv install 2 . 5 < TAB > < TAB > 2.5 .0 2.5 .0-rc1 2.5 .1 2.5 .2 2.5 .3

It seems to be a convention to name these Bash functions _<command> .

That’s helpful! rbenv has kindly listed which Ruby 2.5.x versions are available to install. We could find this out from rbenv install --list but that’s inefficient because we’d have to clear our current command then re-type it.

How completion functions work

When a function is registered as the method of completion with the -F switch, it must comply with an ‘interface’ of sorts. When the function is called, Bash sets some environment variables to be used by the completion function.

They tell it the contents of the command-line, the cursor position, etc. For example, $COMP_LINE contains the full line that was typed, $COMP_WORDS is that same line broken into an array of words and $COMP_POINT is the cursor’s index position.

In return, the completion function should set $COMPREPLY to specify which completions to print for the command.

Using environment variables as a means of passing information back and forth seems kind of awkward. Bash is pretty old, though.

An example

Everybody loves FizzBuzz, right? Let’s demonstrate Bash completion with a custom function that magically completes the next term in the sequence:

function _fizzbuzz ( ) { length = ${ # COMP_WORDS [ @ ] } number = $(( length - 1 )) if ! (( number % 15 )) ; then COMPREPLY = ( fizzbuzz ) elif ! (( number % 3 )) ; then COMPREPLY = ( fizz ) elif ! (( number % 5 )) ; then COMPREPLY = ( buzz ) else COMPREPLY = ( $number ) fi } complete -F _fizzbuzz fizzbuzz ▲ Setting a Bash function to complete fizzbuzz

In Bash, you can access elements in an array, or get its length like this: first = ${SOME_ARRAY [ 0 ] } second = ${SOME_ARRAY [ 1 ] } length = ${ # SOME_ARRAY [ @ ] }

Our command is called fizzbuzz so we name our completion function _fizzbuzz , as per the convention. We first set the length variable to the number of words on the command-line and number to one less, since ‘ fizzbuzz ’ itself counts as a word.

We’ve probably all seen FizzBuzz before so let’s skip the modulo logic. The important part is to set $COMPREPLY - in this case, to an array of the next term in the sequence.

Now, if we type fizzbuzz <TAB><TAB> , Bash completion kicks in and as if by magic the next term is appended to the current command-line. Our fizzbuzz command doesn’t actually exist but that doesn’t seem to matter!

I do wonder if this could be used as an attack vector: function _ls ( ) { } complete -F _ls ls Of course, we’d need permissions to register completion functions which we won’t have so it’s probably fine?

▲ Using Bash completion to generate the FizzBuzz sequence

If the completion function returns a single option, Bash immediately completes it without prompting us. That means we can hold down <TAB> to continuously complete the FizzBuzz sequence. Hooray!

Surely this is the best way to implement FizzBuzz in a coding interview? Or maybe this?

As you can see, there’s plenty of fun to be had! In part two we’ll implement Bash completion for my automation tool and see how it works in practice.