If you aren’t a Linux user and you watch someone who knows what they are doing use Bash — the popular command line interpreter — you might get the impression they type much faster than they actually do. That’s because experienced Linux users know that pressing the tab key will tend to complete what they are typing, so you can type just a few characters and get a much longer line of text. The feature is very smart so you may not have realized it, but it knows a good bit about what you could type. For example, if you try to unzip a file, it knows the expected file name probably has a .zip extension.

How does that happen? At first, you might think, “who cares how it happens?” The problem is when you write a shell script or a program that runs on Linux, the completion gets dumb. Someone has to make Bash smart about each command line program and if you are the author then that someone is you.

Anatomy of Command Completion

Turns out completion depends on a particular GNU library known as readline . It reads text for lots of different programs, including Bash and you can configure it using the .inputrc file in your home directory. For example, here’s my .inputrc :

"\e[A": history-search-backward "\e[B": history-search-forward $if Bash Space: magic-space $endif set match-hidden-files off set completion-ignore-case on set visible-stats on set show-all-if-ambiguous on

That doesn’t look like much, but there is a system-wide configuration at /etc/inputrc which is much more substantial. You can also issue certain commands that will modify the configuration on the fly. For example, the Bash built-in “ bind ” can set things up and also show you a long list of what’s already set up. If you do this command:

bind -p

You’ll see a long list of output. Of interest here are the following few lines:

"\C-i": complete "\e\e": complete "\e!": complete-command "\e/": complete-filename "\e@": complete-hostname "\e{": complete-into-braces "\e~": complete-username "\e$": complete-variable

Yours may vary, but essentially this says that when the tab key or a double escape shows up, to do a completion operation. There are also a few specialized completions that start with escape.

But How Does it Work?

There are three built-in shell commands that cooperate to manage completion. They are:

complete – Configure completion

– Configure completion compgen – Generate possible completions

– Generate possible completions compopt – Modify completion options

I won’t get into all the possible options, although you can read the Bash manual if you are curious. For our purposes, you’ll only need a few, and all three commands mostly take the same arguments anyway.

Let’s try a few things with compgen , since that’s what bash eventually calls to get completions. Suppose you are typing ls at the command line and you press tab. Bash will make the following call:

compgen -c ls

You’ll see a bunch of output (depending on what you have installed) of all the commands that start with ls , such as ls itself, lsusb , lspci , etc. You can also use the more modern “ -A command ” syntax to get the same result.

Try this one:

compgen -d /e

Again, you could use -A directory if you prefer. Clearly, this outputs directories. This is fine, but how does Bash know what options to pass to compgen ? That’s where the complete command comes in. The complete command can tell Bash to call a shell function or a command in response to a completion for a certain command. It can also set one for an empty command or a command that otherwise has no matching rule. There are also nice shortcuts for common cases.

Consider a script called burnhex . If you type burnhex , a space, and a tab, Bash (via readline ) will offer you all the files in the current directory for completion. However, you can guess you only want files that end in .hex . You can easily do that with complete :

complete -G "*.hex" burnhex

Now only files that end in .hex will show up.

To look at even more complex completions, let’s start with a simple shell function. By convention, these start with underscores, although that isn’t a technical requirement:

function _hackaday { COMPREPLY=( "hackaday.com" "hackaday.io" ) return 0 }

This tells the system we have two completions. Let’s pretend we have a script or program called “ gohackaday ” and we want this to be the completion for it (the command doesn’t have to exist for this to work):

complete -F _hackaday gohackaday

Now if you type:

gohackaday<SPACE><TAB>

You’ll see your completion in action. The system is smart enough to know that it can fill in the “hackaday.” part, since both choices start with that. If you type a “c” or an “i” and hit tab again, it will go ahead and pick the right one.

By the way, the function gets three arguments that we aren’t using. The first argument ($1) is the name of the command that’s active, the second is the word being completed, and the third is the word before the current word. For example, consider the line below and the three arguments following:

ls -l /foo $1=ls $2=/foo $3=-l

There are also several environment variables set if you want to extract the entire line or determine how the user called up the completion. For simple cases, you probably don’t need these.

Of course, this is an easy example. For things that are more than you can process in a function, you can also run a full-blown command by using -C instead of -F . Commands get the same arguments and most of the same environment variables.

Besides that, remember the -c and -d options to compgen ? They work here, too. So if you had a command called foobar that needs a directory name as an argument you could say:

complete -d foobar

You can even call compgen in your function or command to generate data directly or for further filtering and augmentation in your code. This can get quite complex and if you look at some of the built-in ones, you’ll see what I mean.

In general, a lot of completion scripts live in either /etc/bash_completion.d or /usr/share/bash-completions. Yes, one of those is an underscore and one is a dash. One of the joys of Linux is how every system is a little bit different.

A More Typical Example

Consider the ftp command. You can find out what the completion is for that by typing:

complete -p | grep ftp

The result will look like:

complete -F _known_hosts ftp

That means there is a shell function called _known_hosts . If you want to see what it looks like, try:

declare -f _known_hosts

You should see something like this:

_known_hosts () { local cur prev words cword; _init_completion -n : || return; local options; [[ "$1" == -a || "$2" == -a ]] && options=-a; [[ "$1" == -c || "$2" == -c ]] && options+=" -c"; _known_hosts_real $options -- "$cur" }

Obviously, the real work is being done in _known_hosts_real and you can dump it the same way. It is complex, but you can guess it is going to dump host names the computer knows about. There are also other helper functions like _init_completion . The point isn’t how these work, but that they exist, and you can use them just like the ftp completion setup does.

This has just been a quick overview. If you want all the gory details, the Bash manual is helpful. You can also find a lot of useful documents at the Bash Completion project GitHub page. Bash isn’t the only game in town, by the way. Zsh has a very powerful (and different) completion mechanism if this one isn’t enough for you.

Once you understand how Bash reads command lines, there are a lot of interesting tricks you can play. I’ll show you my favorite next time. Until then, I hope this helps you type less and do more.