A couple of posts back, I showed off some functions to pop up notifications when a host became pingable again or when a port became reachable. Today’s (semi) quick tip is how to use BASH’s autocomplete functionality add hostname autocompletion to those notifications functions.

BASH autocompletion is a system that provides tab completion of command arguments. You’re familiar with it’s default behavior which is to complete filenames and paths.

1
2
3
4
~ ls enctypt<TAB>
encrypt              encrypt-only-sym.rb  encrypt-time.rb
encrypt-decrypt.rb   encrypt-sym.rb       encrypt.rb
~  ls encrypt

You can override this behavior by providing BASH with a list of possible completions. The list can be a literal list of words, or it can be a function that looks at the current environment ($PWD, user, time on day, etc) and generates context aware list.

So, what we want is a way to generate a list of hosts we know about. And, it just so happens we have such a list lying around. You know how the first time you SSH to a new server, your prompted to confirm it’s identity? Well, that confirmation, along with the host’s name is stored in ~/.ssh/known_hosts. From that file we can extract a list which should cover most of the servers we care about.

The simple approach is to build a list when you login. If you Google around you’ll find lots of example scripts for pulling hostnames out of known_hosts, but the most common looks like:

1
echo `cat ~/.ssh/known_hosts | cut -f 1 -d ' ' | sed -e s/,.*//g | uniq | grep -v "\["`

The command that setups autocompletion is complete. When giving it a list, pass them in as an augument to the -W option:

1
complete -W "$(echo `cat ~/.ssh/known_hosts | cut -f 1 -d ' ' | sed -e s/,.*//g | uniq | grep -v "\["`;)" wait-for-host

I’m capturing the output of the command with $() and wrapping it in double quotes. The last argument is the name of the command (which can be also be a function or alias) that will use this autocompletion. Now we get this:

1
2
3
~ wait-for-host www<TAB>
www.example.com            www1.example.com
~ wait-for-host www

That works fine, but it’s static and applies only to one command. Instead you can create a reusable function.

1
2
3
4
5
6
7
8
9
_known_hosts() {
    local know_hosts cur
    known_hosts=$(cat ~/.ssh/known_hosts | cut -f 1 -d ' ' | sed -e s/,.*//g | uniq | grep -v "\[")

    cur="${COMP_WORDS[COMP_CWORD]}"

    COMPREPLY=( $(compgen -W "$known_hosts" -- ${cur}) )
    return 0
}

BASH auto completion functions are powerful things, but for today we’re keeping it simple. First we build a list of options as before and store it in $known_hosts. Second we get the current word, the command argument, which is be tab completed. Finally, we pass that list and that word into compgen which is BASH’s internal matcher. compgen has some powerful features, but in this case it’s just going to return a list of hosts in $known_hosts that start with the word we hit tab on.

Then we tell the completion that we are using a function by giving it the -f option, along with the function name, instead of -W:

1
complete -F _known_hosts wait-for-host

And you can use the _known_hosts function for other commands as well:

1
complete -F _known_hosts ssh

This is a good exercise in understanding autocompletion, but it’s pretty basic. Fortunately, people have already done all of the hard work in the bash-completion project. This ships by default with many Linux distros. On the Mac:

1
brew install bash-completion

The add:

1
2
3
if [ -f $(brew --prefix)/etc/bash_completion ]; then
    . $(brew --prefix)/etc/bash_completion
fi

This will add context sensitive completion to everything from SSH to rsync, and give you a much smarter _known_hosts function you can use with your own commands.

If you want to learn more, I suggest reading the bash-completion code. It does far more powerful things than I cover here and is a great jumping off point for your own completion functions.

Comments