More on Using the Bash Complete Command
In the video last week I showed how to use the bash complete command for simple use cases. Today I'll show you some of the additional ways that you can use the command for more complex scenarios.
To review the simple use case I demonstrated last week here's the essence of it, or watch the video:
$ # Create a dummy command:
$ touch ~/bin/myfoo
$ chmod +x ~/bin/myfoo
$ # Create some files:
$ touch a.bar a.foo b.bar b.foo
$ # Use the command and try auto-completion.
$ # Note that all files are displayed:
$ myfoo <TAB><TAB>
a.bar a.foo b.bar b.foo
$ # Now tell bash that we only want foo files.
$ # This command tells bash args to myfoo are completed
$ # by generating a list of files and then excluding
$ # everything # that doesn't match *.foo:
$ complete -f -X '!*.foo' myfoo
$ # Tray again:
$ myfoo <TAB><TAB>
a.foo b.foo
For more complex cases where you need more control over how things are completed you can tell bash to call a function for doing the completion work. This is a function that you supply, that you would probably source from your .profile file. The function name is then supplied as an argument to the -F option of complete:
$ complete -F _mycomplete_ myfoo
A basic version of _mycomplete, which at this point doesn't do anything more than our simple command line usage of complete above, would be something like:
function _mycomplete_()
{
local cmd="${1##*/}"
local word=${COMP_WORDS[COMP_CWORD]}
local line=${COMP_LINE}
local xpat='!*.foo'
COMPREPLY=($(compgen -f -X "$xpat" -- "${word}"))
}
complete -F _mycomplete_ myfoo
In the first three lines of the function body we create some useful local variables, here mainly for showing what's available since most of them aren't used in the function. The first, cmd, gets the command that's being executed, this can be used if your completion function can handle multiple commands. The second, word, gets the word that is being completed, this can be used if your completion strategy changes based on the word that's being expanded, it's also needed so that only matching values are returned. The third, line, gets the entire command line that is being completed. The fourth variable, xpat, is our exclusion pattern, the same one used in the simple example above. Check the bash man page for other useful COMP_* variables.
The only real code in the function is the last line that sets the variable COMPREPLY, which is our reply to bash's request to expand something. This line uses compgen to generate the expansion. The compgen command accepts most of the same options that complete does but it generates results rather than just storing the rules for future use. Here we tell compgen to create a list of files with -f. Then we tell it to exclude all the files that match our exclusion pattern with -X "$xpat". And finally, we pass in the word being completed so that only items that match it are returned.
Now, let's consider a slightly more complex example. Let's use our complete function to complete multiple commands and let's also change it so that it expands things for the myfoo command differently depending on the command line arguments given to myfoo.
Specifically, let's assume that myfoo can both foo things and unfoo them, so myfoo -f myfile creates myfile.foo and myfoo -u myfile.foo creates myfile. Think of the -d option to gzip or bunzip2. So, in this case we only want to show non foo files if -f has been specified, and only foo files if -u has been specified.
Further, let's add one more enhancement, let's make our completion function include directory names so that directory names can be used to complete the argument, thereby allowing us to navigate to sub-directories for fooing and unfooing files in subdirectories. Our new function would be:
function _mycomplete_()
{
local cmd="${1##*/}"
local word=${COMP_WORDS[COMP_CWORD]}
local line=${COMP_LINE}
local xpat
# Check to see what command is being executed.
case "$cmd" in
myfoo)
# See if we are fooing or unfooing.
case "$line" in
*-f*)
xpat='*.foo'
;;
*-u*)
xpat='!*.foo'
;;
*)
xpat='*.foo'
;;
esac
;;
mybar)
xpat='!*.bar'
;;
*)
xpat='!*'
;;
esac
COMPREPLY=($(compgen -f -X "$xpat" -- "${word}"))
}
complete -d -X '.[^./]*' -F _mycomplete_ myfoo mybar
Here, we use the cmd variable to see which command we are completing and change our pattern based on it. When handling the myfoo command, we use the line variable to see if the command line includes the -f or -u option for determining whether we should exclude or include foo files.
To include directories in our output we simply modify the complete command that installs our function by including the arguments -d -X '.[^./]*', which generates a list of directories and then excludes ./ and ../ (the current directory and the parent directory). The directory list is then added to the result returned by calling our completion funtion. We also add our second command mybar to the commands handled by our function.
Now when we run it:
$ # Source our completion function:
$ . bcomp2.sh
$ # Make some files:
$ touch a b a.bar a.foo b.bar b.foo
$ # Make a directory for checking directory inclusion:
$ mkdir astuff
$ # See what we get when we want to foo something
$ myfoo -f <TAB><TAB>
a a.bar astuff/ b b.bar bcomp2.sh bcomp.sh
$ # See what we get when we want to unfoo something
$ myfoo -u <TAB><TAB>
a.foo astuff/ b.foo
Once you've digested all of this, check the file /etc/profile.d/complete.bash to see the default completions that come with bash. You'll notice in that file that there are numerous complications that we've ignored here, such as what happens when somebody is trying to complete a word such as ${ABC or $(ca. In these cases the completion needs to return, respectively, a variable name and a command name and not a data file name.