More on Using the Bash Complete Command

 in

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.

______________________

Mitch Frazier is an Associate Editor for Linux Journal.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Tab completion like ncftp?

Louise Hoffman's picture

Dear Mitch,

Awesome post.

I have been trying to write a tab completion that works just like in ncftp, which is:

$ ls
file1 file2 file3
$ cat file[TAB]
1 2 3

So I was wondering, if you could help me out? =)

Right now I have

_b() {
local word=${COMP_WORDS[COMP_CWORD]}
echo $word;
}
complete -F _b TEST

Is it possible to have this work for all commands, and not just TEST?

And how do I remove $word from "compgen"'s output?

Best regards,
Louise

Completion

Mitch Frazier's picture

The following should remove the "word" from the completion list. Completion functions just return a bash array and you can manipulate it to contain whatever you like:

_b() {
        local word=${COMP_WORDS[COMP_CWORD]}
        COMPREPLY=($(compgen -f -- "${word}"))
        if [[ "$word" ]]; then
                local  w
                local  i=0
                local  n=${#COMPREPLY[*]}
                while [[ $i -lt $n ]]
                do
                        w=${COMPREPLY[$i]}
                        COMPREPLY[$i]="${w:${#word}}"
                        let i++
                done
        fi
}

I don't see any way to get a completion to work for all commands but I suppose you could always do something like this if you really need to:

for c in /bin/* /usr/bin/* ~/bin/*
do
        complete -F _b $(basename $c)
done

Mitch Frazier is an Associate Editor for Linux Journal.

Thank u sir, useful one.

rajesh.r.s.'s picture

Thank u sir, useful one.

Autocompletion

John Hardin's picture

You know you're using autocompletion too much when you start hitting [TAB] to try to autocomplete your username (or worse, your password) during login...

:)

TAB TAB TAB

Mitch Frazier's picture

TAB TAB TAB -- hmmm I had a really snappy response, why won't this thing complete it.

Mitch Frazier is an Associate Editor for Linux Journal.

White Paper
Linux Management with Red Hat Satellite: Measuring Business Impact and ROI

Linux has become a key foundation for supporting today's rapidly growing IT environments. Linux is being used to deploy business applications and databases, trading on its reputation as a low-cost operating environment. For many IT organizations, Linux is a mainstay for deploying Web servers and has evolved from handling basic file, print, and utility workloads to running mission-critical applications and databases, physically, virtually, and in the cloud. As Linux grows in importance in terms of value to the business, managing Linux environments to high standards of service quality — availability, security, and performance — becomes an essential requirement for business success.

Learn More

Sponsored by Red Hat

White Paper
Private PaaS for the Agile Enterprise

If you already use virtualized infrastructure, you are well on your way to leveraging the power of the cloud. Virtualization offers the promise of limitless resources, but how do you manage that scalability when your DevOps team doesn’t scale? In today’s hypercompetitive markets, fast results can make a difference between leading the pack vs. obsolescence. Organizations need more benefits from cloud computing than just raw resources. They need agility, flexibility, convenience, ROI, and control.

Stackato private Platform-as-a-Service technology from ActiveState extends your private cloud infrastructure by creating a private PaaS to provide on-demand availability, flexibility, control, and ultimately, faster time-to-market for your enterprise.

Learn More

Sponsored by ActiveState