bash autocompletion

01 Jan 2012

Update: It’s highly recommended to upgrade bash-completion to version >= 2.0 for an improved performance.

I really like minimalism systems (and cli apps), they are faster, more stable and easier to control. I think it’s pretty cool to be able to write a command and get without hesitation a result (I’m aware I’m probably already deprecated, in a world where touch and gui applications are the norm, who would still prefer text based systems?). Sadly many of these commands are not specially user friendly, they contain tons of options and sometimes these options are quite hard to write correctly, when you download scripts from Internet it gets worse, all options must be written by hand because the lack autocompletion.

Many people don’t realize this autocompletion magic work by programming simple bash scripts, so I decided to write a few notes about the process.

Introduction

Bash triage autocompletion every time an user press <Tab><Tab>, for most simple commands a single call to complete would be enough to generate correct alternatives. Let’s suppose foo is a command who only take directories as arguments, the autocompletion logic can be described as:

$ complete -o plusdirs foo

From then on $ foo <Tab><Tab> will return a directory list, that one was easy 😉 Now, let’s give more examples:

$ complete -A user bar #will autocomplete bar with a list of system users
$ complete -W "-v --verbose -h" wop #will autocomplete wop with "-v", "--verbose" and "-h"
$ complete -f -X '!*.[pP][dD][fF]' evince foo #will autocomplete evince and foo with all pdf files

The full syntax for complete can be reviewed in the bash help, $ man bash

Function based

One of the options complete accept is -F who calls a function, this function can be programmed at any length ✌ e.g:

$ source file_where_pump_function_is_defined
$ complete -F \_pump pump

Now whenever pump is typed followed by <Tab><Tab> _primp() will be called and will require to fill the COMPREPLY array.

Most of the files who contain these functions live in /etc/bash_completion.d/ and in recent years in /usr/share/bash-completion/completions/ (in Debian/Ubuntu systems), let’s suppose we’ve the following hand made script:

$ fix -h
Usage: fix module
   -h  or --help     List available arguments and usage (this message).
   -v  or --version  print version.
   apache            poves /etc/init.d/apache2.1 to apache2.
   ipw2200           restart the ipw2200 module.
   wl                restart the wl module.
   iwlagn            restart the iwlagn module.
   mpd               restart mpd.

The autocompletion logic can be defined in two ways, the easiest one would be to dump the following line in /etc/bash_completion.d/fix.autocp:

$ complete -W "-h --help -v --version apache ipw2200 wl iwlagn mpd" fix

After doing, it would be necessary to reload the environment:

$ source $HOME/.bashrc

WARNING: autocompletion will only work if it’s initialized in $HOME/.bashrc or other files read by bash (it also must be installed $ sudo apt-get install bash-completion):

if [ -f /etc/bash_completion ]; then
    source /etc/bash_completion
fi

The more elaborated case would involve defining a function, let’s replace the /etc/bash_completion.d/fix.autocp content with this:

\_fix()
{

    if ! command -v "fix" >/dev/null 2>&1; then
        return
    fi

    #defining local vars
    local cur prev words cword
    _init_completion || return #comment this line for bash-completion <2.0

    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"

    COMPREPLY=() #clean out last completions, important!

    COMMANDS="apache ipw2200 wl iwlagn mpd"
    OPTS="-h --help -v --version"

    case "${cur}" in #if the current word have a '-' at the beginning..
        -*) completions="${OPTS}"     ;;
        *)  completions="${COMMANDS}" ;;
    esac
    COMPREPLY=($(compgen -W "${completions}" -- ${cur}))
    return 0
}
complete -F \_fix fix

The $cur variable is important, parameters would be compared against it, in more complex examples, $prev and even $prev_prev can be compared.

To generate option lists, compgen is used regularly, this command compare and return matched results as a list, to wrap it up, here are some examples:

$ compgen -W "-v --verbose -h --help" -- "-v"
-v
$ compgen -W "-v --verbose -h --help" -- "--"
--verbose
--help
$ compgen -W "apache ipw2200 iwlagn mpd wl" -- "ap"
apache
$ compgen -W "apache ipw2200 iwlagn mpd wl" -- "i"
ipw2200
iwlagn

Once this two step process is understood it’s easy to see how most autocomplation scripts work. I’ll review a more complex example, android:

$ android -h
  Usage: android [global options] action [action options]
  Global options:
      -v --verbose  Verbose mode: errors, warnings and informational messages are printed.
      -h --help     Help on a specific command.
      -s --silent   Silent mode: only errors are printed out.
  Valid actions are composed of a verb and an optional direct object:
      -   list
      -   list avd
      -   list target
      - create avd
      -   move avd
      - delete avd
      - update avd
      - create project
      - update project
      - create test-project
      - update test-project
      - create lib-project
      - update lib-project
      - update adb
      - update sdk

As it can be noted most options depend of a previous command, “avd” should only be returned when list is used as an action:

$ android list[Tab][Tab]

And avd/target should be returned when no substring is present after list

$ android create[Tab][Tab]

Should return avd, project, test-project and lib-project:

$ android create avd[Tab][Tab]

And -a, -c, -f, etc, should be returned when avd and create are the first parameters. The full autocompletion file for this example is located in github, I’ll explain now the more important parts:

\_android()
{
    if ! command -v "android" >/dev/null 2>&1; then
        return
    fi
    COMPREPLY=() #clean out last completions, important!
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    number_of_words=${#COMP_WORDS[@]}

    if [ "${number_of_words}" -gt "2" ]]; then
        prev_prev="${COMP_WORDS[COMP_CWORD-2]}"
    fi

A prev_prev variable is declared only when two or more arguments are written in the prompt.

#=======================================================
#                  General options
#=======================================================
COMMANDS="list create move delete update"
#COMMANDS=`android -h | grep '^-' | sed -r 's/: .*//' \
           | awk '{print $2}' | sort | uniq 2> /dev/null`

List options can be declared fixed or generated at run time by parsing help screens, depending of the command you can use whenever method feels more comfortable.

OPTS="-h --help -v --verbose -s --silent"
#=======================================================
#                   Nested options [1st layer]
#=======================================================
list_opts="avd target"
create_opts="avd project test-project lib-project"
move_opts="avd"...

The same can be set for subcommands

#=======================================================
#                   Nested options [2nd layer]
#=======================================================
create_avd_opts="-c --sdcard -t --target -n --name -a \
                 --snapshot -p --path -f -s --skin"
create_project_opts="-n --name -t --target -p --path -k \
                     --package -a --activity"
create_test-project_opts="-p --path -m --main -n --name"
create_lib-project_opts="-n --name -p --path -t --target \
                         -k --package"...

And subcommand options…

if [ -n "${prev_prev}" ]; then
#2nd layer
case "${prev_prev}" in
  create)
    case "${prev}" in
      avd)
         COMPREPLY=($(compgen -W "${create_avd_opts}" -- ${cur}))
         return 0
         ;;
      project)
         COMPREPLY=($(compgen -W "${create_project_opts}" -- ${cur}))
         return 0
         ;;
         ...
 esac

Depending in $prev_prev, $prev and $cur the correct list will be return, $ android subcomand option incomplete_option#CURSOR#

case "${prev}" in
    ##1st layer
    list)
        COMPREPLY=($(compgen -W "${list_opts}" -- ${cur}))
        return 0
        ;;
    create)
        COMPREPLY=($(compgen -W "${create_opts}" -- ${cur}))
        return 0
        ;;
    ...

$ android subcommand incomplete_option#CURSOR#

      #general options
      case "${cur}" in
         -*)
             COMPREPLY=($(compgen -W "${OPTS}" -- ${cur}))
             ;;
         *)
             COMPREPLY=($(compgen -W "${COMMANDS}" -- ${cur}))
             ;;
      esac
}
complete -F \_android android

$ android incomplete_subcommand#CURSOR#

Extra

Available functions

There exist plenty of available functions who can be used to autocomplete commonly used options, for example, if a command accepts a -f option for file arguments, the _filedir function can be used:

-f)
    \_filedir
    return 0
    ;;

Other pre-defined functions can be found at: http://anonscm.debian.org/gitweb/?p=bash-completion/bash-completion.git;a=blob;f=bash_completion

Debug

Bash autocompletion scripts are easy to create, however eventually (specially with larger cli commands) there are chances things doesn’t work as expected, in those cases enabling bash verbose mode is the easiest and faster method to debug such scripts:

$ set -x
$ source ~/.bashrc #reload the environment
$ command opc[Tab][Tab] #testing the autocompletion
...
...
... verbose output

Examples

There are many bash completion scripts in Internet and some others in my personal repository. Looking at examples is probably the easiest way to learn the harder details.

Final thoughts

Bash autocompletion may seems scary at the beginning but once several examples are read a clear pattern can be dazzled, depending on your system usage they can save you a lot of time/typing, so next time you find yourself writing to much give them a shot and let the computer do the job for you 😊