This site is now 100% read-only , and retired.

An introduction to bash completion: part 2

Posted by Steve on Sat 24 Dec 2005 at 03:00

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.

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. Inside that file save the following text: _foo() { local cur prev opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="--help --verbose --version" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 fi } complete -F _foo foo To test it you can now source the file: skx@lappy:~$ . /etc/bash_completion.d/foo skx@lappy:~$ foo --[TAB] --help --verbose --version 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!

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: skx@lappy:~$ compgen -W "--help --verbose --version" -- -- --help --verbose --version skx@lappy:~$ compgen -W "--help --verbose --version" -- --h --help 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. From the bash reference manual we can read the description of COMPREPLY : COMPREPLY 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

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. The sample code looks like this: _xm() { local cur prev opts base COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" # # The basic options we'll complete. # opts="console create list" # # Complete the arguments to some of the basic commands. # case "${prev}" in console) local running=$(for x in `xm list --long | grep \(name | grep -v Domain-0 | awk '{ print $2 }' | tr -d \)`; do echo ${x} ; done ) COMPREPLY=( $(compgen -W "${running}" -- ${cur}) ) return 0 ;; create) local names=$(for x in `ls -1 /etc/xen/*.cfg`; do echo ${x/\/etc\/xen\//} ; done ) COMPREPLY=( $(compgen -W "${names}" -- ${cur}) ) return 0 ;; *) ;; esac COMPREPLY=($(compgen -W "${opts}" -- ${cur})) return 0 } complete -F _xm xm 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: xm list --long | grep \(name | grep -v Domain-0 | awk '{ print $2 }' | tr -d \) This gives us a list of the running Xen systems. For the creation operation we complete based upon the output of this command: for x in `ls -1 /etc/xen/*.cfg`; do echo ${x/\/etc\/xen\//} ; done This takes a directory listing of the /etc/xen directory and outputs the names of any files ending in .cfg . For example: skx@lappy:~$ for x in `ls -1 /etc/xen/*.cfg`; do echo ${x/\/etc\/xen\//}; done etch.cfg root.cfg sarge.cfg steve.cfg x.cfg skx@lappy:~$

Using the compgen command we've shown how to match user input against particular strings, both by using a fixed set of choices and by using the output of commands. It is also possible to match directory names, process names, and other things. See the bash manual for a full description by running " man bash ". The final example demonstrates how to complete files and hostnames in response to two initial options: # # Completion for foo: # # foo file [filename] # foo hostname [hostname] # _foo() { local cur prev opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="file hostname" case "${prev}" in file) COMPREPLY=( $(compgen -f ${cur}) ) return 0 ;; hostname) COMPREPLY=( $(compgen -A hostname ${cur}) ) return 0 ;; *) ;; esac COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) } complete -F _foo foo

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.