Bash hostname completion

As part of its programmable completion suite, Bash includes hostname completion. This completion mode reads hostnames from a file in hosts(5) format to find possible completions matching the current word. On Unix-like operating systems, it defaults to reading the file in its usual path at /etc/hosts.

For example, given the following hosts(5) file in place at /etc/hosts:

127.0.0.1      localhost
192.0.2.1      web.example.com www
198.51.100.10  mail.example.com mx
203.0.113.52   radius.example.com rad

An appropriate call to compgen would yield this output:

$ compgen -A hostname
localhost
web.example.com
www
mail.example.com
mx
radius.example.com
rad

We could then use this to complete hostnames for network diagnostic tools like ping(8):

$ complete -A hostname ping

Typing ping we and then pressing Tab would then complete to ping web.example.com. If the shopt option hostcomplete is on, which it is by default, Bash will also attempt host completion if completing any word with an @ character in it. This can be useful for email address completion or for SSH username@hostname completion.

We could also trigger hostname completion in any other Bash command line (regardless of complete settings) with the Readline shortcut Alt+@ (i.e. Alt+Shift+2). This works even if hostcomplete is turned off.

However, with DNS so widely deployed, and with system /etc/hosts files normally so brief on internet-connected systems, this may not seem terribly useful; you’d just end up completing localhost, and (somewhat erroneously) a few IPv6 addresses that don’t begin with a digit. It may seem even less useful if you have your own set of hosts in which you’re interested, since they may not correspond to the hosts in the system’s /etc/hosts file, and you probably really do want them looked up via DNS each time, rather than maintaining static addresses for them.

There’s a simple way to make host completion much more useful by defining the HOSTFILE variable in ~/.bashrc to point to any other file containing a list of hostnames. You could, for example, create a simple file ~/.hosts in your home directory, and then include this in your ~/.bashrc:

# Use a private mock hosts(5) file for completion
HOSTFILE=$HOME/.hosts

You could then populate the ~/.hosts file with a list of hostnames in which you’re interested, which will allow you to influence hostname completion usefully without messing with your system’s DNS resolution process at all. Because of the way the Bash HOSTFILE parsing works, you don’t even have to fake an IP address as the first field; it simply scans the file for any word that doesn’t start with a digit:

# Comments with leading hashes will be excluded
external.example.com
router.example.com router
github.com
google.com
...

You can even include other files from it with an $include directive!

$include /home/tom/.hosts.home
$include /home/tom/.hosts.work

This really surprised me when reading the source, because I don’t think /etc/hosts files generally support that for their usual name resolution function. I would love to know if any systems out there actually do support this.

The behaviour of the HOSTFILE variable is a bit weird; all of the hosts from the HOSTFILE are appended to the in-memory list of completion hosts each time the HOSTFILE variable is set (not even just changed), and host completion is attempted, even if the hostnames were already in the list. It’s probably sufficient just to set the file once in ~/.bashrc.

This setup allows you to set hostname completion as the default method for all sorts of network-poking tools, falling back on the usual filename completion if nothing matches with -o default:

$ complete -A hostname -o default curl dig host netcat ping telnet

You could also use hostname completions for ssh(1), but to account for hostname aliases and other ssh_config(5) tricks, I prefer to read Host directives values from ~/.ssh/config for that.

If you have machine-readable access to the complete zone data for your home or work domain, it may even be worth periodically enumerating all of the hostnames into that file, perhaps using rndc dumpdb -zones for a BIND9 setup, or using an AXFR request. If you have a locally caching recursive nameserver, you could even periodically examine the contents of its cache for new and interesting hosts to add to the file.

Lazier Tab completion

Bash completion allows you to complete paths, commands, and filenames, and where implemented can even expand the syntax of commands like git and svn. It’s very convenient by default, but there are a couple of tweaks that can make it much faster and easier to use.

Single Tab press

With Bash completion enabled, the default is to complete the current path to the longest unique string it can on the first press of the Tab key, and then show a list of all the possible completions if the Tab key is then pressed a second time.

If you don’t like pressing Tab twice, you can fix this with a line in .inputrc so that the line is both completed to the longest unambiguous pattern, and then possible values are also printed:

set show-all-if-ambiguous on

Log out and then in again, or re-read your .inputrc file with Ctrl+X then Ctrl+R, and you should find that you now only need to press Tab once on completions to get both behaviours in one hit.

If you want Tab completion of filenames to work the same way in Vim’s command mode, you can include the below in your .vimrc. Note that the separator is a colon, rather than a comma, which means to perform both the command completion and suggestion at the first Tab press:

set wildmode=longest:list

Complete case-insensitively

If you commonly Tab your way through long paths that sometimes include mixed-case names, you can make Readline handle that for you by making the completion case-insensitive, with the following in your .inputrc:

set completion-ignore-case on

You can supplement this to make the completion apply similar insensitivity between hyphens and underscores:

set completion-map-case on

If you were tabbing your way through an ImageMagick directory, for example, and forgot the capital letters in typing imagem, Readline would quietly complete it to ImageMagick for you. Similarly, if you typed lib-undersc when the actual filename was lib_underscore, the shell would quietly fix that for you too.

You can get case-insensitivity for the filename completion in Vim with the following. I suggest wrapping it in a conditional as below, since it’s a reasonably new option:

if exists("&wildignorecase")
    set wildignorecase
endif

Unfortunately, there doesn’t seem to be a way that I can find to reproduce the underscore/hyphen mapping. If you know of one, please comment.

Don’t prompt for many possible completions

If you’re on a fast terminal and you’re not bothered by having the screeds of texts it can sometimes generate being spat at you, you can turn off the sometimes annoying prompt that checks with you whether you want to show more than a hundred results for possible completion, with the following in .inputrc:

set completion-query-items -1

If you do this, you should probably set Readline’s built-in pager to be off, otherwise it’ll attempt to walk you through the possible completions page-by-page:

set page-completions off

If you do happen to deal with directories with more than a thousand files in them now and then (which doesn’t tend to happen much in routine system administration), it might be a bit safer to set it to some number much higher than the default of 100:

set completion-query-items 1000

These three tips combined make tabbing your way through path names much more pleasant and intuitive; you won’t find yourself irritably tapping Tab over and over nearly as much. Even if you’re very much used to playing ball with the stricter idiosyncrasies of the usual form of tab completion, you may find making the shell do the work for you in this way starts to feel very natural very quickly.

Vim filename completion

The autocompletion for filenames in Vim in command mode is very useful, but by default it’s a bit confusing for people accustomed to tab completion in Bash because it doesn’t quite work the same way. Pressing Tab will complete the filename to the first match, and subsequent presses will not elicit any list of possible completions that might otherwise be expected; for that, by default you need to press Ctrl+D rather than Tab.

Tab then tab

Fortunately, this is easily changed by using Vim’s wildmenu, in an appropriate mode. Set the following options in your .vimrc:

set wildmenu
set wildmode=longest,list

You should now find that when you complete filenames after commands like :w and :e, the paths expand in a similar manner to the way they do in the shell. If you’d prefer to only press Tab once to get both the longest matching unique string and a list of possible complete matches, that’s possible to arrange in both Bash and Vim as well.

Ignoring file types

There are probably certain filetypes in your directories that you’ll never want to edit with Vim. There’s hence no point in making them options for the autocompletion, and you can exclude them by pattern to make searching for the right file a bit quicker. This is done using the wildignore pattern. I use the following settings:

set wildignore+=*.a,*.o
set wildignore+=*.bmp,*.gif,*.ico,*.jpg,*.png
set wildignore+=.DS_Store,.git,.hg,.svn
set wildignore+=*~,*.swp,*.tmp

Compatibility

For the sake of keeping my .vimrc consistent and compatible on both older and newer machines, I like to wrap these options in a conditional block checking that the wildmenu feature is actually available:

" Wildmenu
if has("wildmenu")
    set wildignore+=*.a,*.o
    set wildignore+=*.bmp,*.gif,*.ico,*.jpg,*.png
    set wildignore+=.DS_Store,.git,.hg,.svn
    set wildignore+=*~,*.swp,*.tmp
    set wildmenu
    set wildmode=longest,list
endif

Insert mode

You can also complete file paths and names in insert mode with Ctrl+X Ctrl+F. It can be handy to map this to Tab if you don’t use it for anything else:

inoremap <Tab> <C-X><C-F>