Bash-completion

Freddy Vulto (FVu)

Revision History
Revision 1.0Mar 2009FV(

Table of Contents

Preface
I. Bash completion
1. Configuration files
2. Environment variables
II. Coding Style Guide
3. Introduction
4. Indentation
5. Globbing in case labels
6. [[ ]] vs [ ]
7. Line wrapping
8. $(…) vs `…`
9. -o filenames
10. [[ $COMPREPLY == *= ]] && compopt -o nospace
11. $split && return
III. Automated testing
12. Introduction
13. Coding Style Guide
14. Installing dependencies
14.1. Debian/Ubuntu
14.2. Fedora/RHEL/CentOS
15. Structure
15.1. Main areas (DejaGnu tools)
15.2. Completion
16. Running the tests
16.1. Running tests via cron
16.2. Specifying bash binary
17. Maintenance
17.1. Adding a completion test
17.2. Fixing a completion test
17.3. Fixing a unit test
18. Rationale
18.1. Naming conventions
19. Reference
20. Test context
20.1. What happens when tests are run?
20.2. bashrc
20.3. inputrc
Index

Preface

Bash completion extends bashs standard completion behavior to achieve complex command lines with just a few keystrokes. This project was conceived to produce programmable completion routines for the most common Linux/UNIX commands, reducing the amount of typing sysadmins and programmers need to do on a daily basis.

Part I. Bash completion

Chapter 1. Configuration files

~/.bash_completion
Sourced late by bash_completion, pretty much after everything else. Use this file for example to load additional completions, and to remove and override ones installed by bash_completion.
$XDG_CONFIG_HOME/bash_completion
Sourced by the bash_completion.sh profile.d script. This file is suitable for definitions of all COMP_* environment variables below. If $XDG_CONFIG_HOME is unset or null, ~/.config is used instead of it.

Chapter 2. Environment variables

COMP_CONFIGURE_HINTS
If set and not null, configure completion will return the entire option string (e.g. --this-option=DESCRIPTION) so one can see what kind of data is required and then simply delete the descriptive text and add one’s own data. If unset or null (default), configure completion will strip everything after the = when returning completions.
COMP_CVS_REMOTE
If set and not null, cvs commit completion will try to complete on remotely checked-out files. This requires passwordless access to the remote repository. Default is unset.
COMP_FILEDIR_FALLBACK
If set and not null, completions that look for filenames based on their "extensions" will fall back to suggesting all files if there are none matching the sought ones.
COMP_IWLIST_SCAN
If set and not null, iwconfig completion will try to complete on available wireless networks identifiers. Default is unset.
COMP_KNOWN_HOSTS_WITH_HOSTFILE
If set and not null (default), known hosts completion will complement hostnames from ssh’s known_hosts files with hostnames taken from the file specified by the HOSTFILE shell variable (compgen -A hostname). If null, known hosts completion will omit hostnames from HOSTFILE. Omitting hostnames from HOSTFILE is useful if HOSTFILE contains many entries for local web development or ad-blocking.
COMP_KNOWN_HOSTS_WITH_AVAHI
If set and not null, known hosts completion will try to use avahi-browse for additional completions. This may be a slow operation in some setups. Default is unset.
COMP_TAR_INTERNAL_PATHS
If set and not null before sourcing bash_completion, ‘tar` completion will do correct path completion for tar file contents. If unset or null, `tar’ completion will do correct completion for paths to tar files. See also README.

Part II. Coding Style Guide

Chapter 3. Introduction

This document attempts to explain the basic styles and patterns that are used in the bash completion. New code should try to conform to these standards so that it is as easy to maintain as existing code. Of course every rule has an exception, but it’s important to know the rules nonetheless!

This is particularly directed at people new to the bash completion codebase, who are in the process of getting their code reviewed. Before getting a review, please read over this document and make sure your code conforms to the recommendations here.

Chapter 4. Indentation

Indent step should be 4 spaces, no tabs.

Chapter 5. Globbing in case labels

Avoid "fancy" globbing in case labels, just use traditional style when possible. For example, do "--foo|--bar)" instead of "--@(foo|bar))". Rationale: the former is easier to read, often easier to grep, and doesn’t confuse editors as bad as the latter, and is concise enough.

Chapter 6. [[ ]] vs [ ]

Always use [[ ]] instead of [ ]. Rationale: the former is less error prone, more featureful, and slightly faster.

Chapter 7. Line wrapping

Try to wrap lines at 79 characters. Never go past this limit, unless you absolutely need to (example: a long sed regular expression, or the like). This also holds true for the documentation and the testsuite. Other files, like ChangeLog, or COPYING, are exempt from this rule.

Chapter 8. $(…) vs `…`

When you need to do some code substitution in your completion script, you MUST use the $(…) construct, rather than the `…`. The former is preferable because anyone, with any keyboard layout, is able to type it. Backticks aren’t always available, without doing strange key combinations.

Chapter 9. -o filenames

As a rule of thumb, do not use "complete -o filenames". Doing it makes it take effect for all completions from the affected function, which may break things if some completions from the function must not be escaped as filenames. Instead, use "compopt -o filenames" to turn on "-o filenames" behavior dynamically when returning completions that need that kind of processing (e.g. file and command names). The _filedir and _filedir_xspec helpers do this automatically whenever they return some completions.

Chapter 10. [[ $COMPREPLY == *= ]] && compopt -o nospace

The above is functionally a shorthand for:

if [[ ${#COMPREPLY[@]} -eq 1 && ${COMPREPLY[0]} == *= ]]; then
    compopt -o nospace
fi

It is used to ensure that long options' name won’t get a space appended after the equal sign. Calling compopt -o nospace makes sense in case completion actually occurs: when only one completion is available in COMPREPLY.

Chapter 11. $split && return

Should be used in completions using the -s flag of _init_completion, or other similar cases where _split_longopt has been invoked, after $prev has been managed but before $cur is considered. If $cur of the form --foo=bar was split into $prev=--foo and $cur=bar and the $prev block did not process the option argument completion, it makes sense to return immediately after the $prev block because --foo obviously takes an argument and the remainder of the completion function is unlikely to provide meaningful results for the required argument. Think of this as a catch-all for unknown options requiring an argument.

Note that even when using this, options that are known to require an argument but for which we don’t have argument completion should be explicitly handled (non-completed) in the $prev handling block because --foo=bar options can often be written without the equals sign, and in that case the long option splitting does not occur.

Part III. Automated testing

Chapter 12. Introduction

The bash-completion package contains an automated test suite. Running the tests should help verifying that bash-completion works as expected. The tests are also very helpful in uncovering software regressions at an early stage.

The bash-completion test suite is written on top of the DejaGnu testing framework. DejaGnu is written in Expect, which in turn uses Tcl — Tool command language.

Chapter 13. Coding Style Guide

The bash-completion test suite tries to adhere to this Tcl Style Guide.

Chapter 14. Installing dependencies

Installing dependencies should be easy using your local package manager.

14.1. Debian/Ubuntu

On Debian/Ubuntu you can use apt-get:

sudo apt-get install dejagnu tcllib

This should also install the necessary expect and tcl packages.

14.2. Fedora/RHEL/CentOS

On Fedora and RHEL/CentOS (with EPEL) you can use yum:

sudo yum install dejagnu tcllib

This should also install the necessary expect and tcl packages.

Chapter 15. Structure

15.1. Main areas (DejaGnu tools)

The tests are grouped into different areas, called tool in DejaGnu:

completion
Functional tests per completion.
install
Functional tests for installation and caching of the main bash-completion package.
unit
Unit tests for bash-completion helper functions.

Each tool has a slightly different way of loading the test fixtures, see Test context below.

15.2. Completion

Completion tests are spread over two directories: completion/\*.exp calls completions in lib/completions/\*.exp. This two-file system stems from bash-completion-lib (http://code.google.com/p/bash-completion-lib/, containing dynamic loading of completions) where tests are run twice per completion; once before dynamic loading and a second time after to confirm that all dynamic loading has gone well.

For example:

set test "Completion via comp_load() should be installed"
set cmd "complete -p awk"
send "$cmd\r"
expect {
    -re "^$cmd\r\ncomplete -o filenames -F comp_load awk\r\n/@$" { pass "$test" }
    -re /@ { fail "$test at prompt" }
}


source "lib/completions/awk.exp"


set test "Completion via _longopt() should be installed"
set cmd "complete -p awk"
send "$cmd\r"
expect {
    -re "^$cmd\r\ncomplete -o filenames -F _longopt awk\r\n/@$" { pass "$test" }
    -re /@ { fail "$test at prompt" }
}


source "lib/completions/awk.exp"

Looking to the completion tests from a broader perspective, every test for a command has two stages which are now reflected in the two files:

  1. Tests concerning the command completions' environment (typically in test/completion/foo)
  2. Tests invoking actual command completion (typically in test/lib/completions/foo)

Chapter 16. Running the tests

The tests are run by calling runtest command in the test directory:

runtest --outdir log --tool completion
runtest --outdir log --tool install
runtest --outdir log --tool unit

The commands above are already wrapped up in shell scripts within the test directory:

./runCompletion
./runInstall
./runUnit

To run a particular test, specify file name of your test as an argument to runCompletion script:

./runCompletion ssh.exp

That will run test/completion/ssh.exp.

16.1. Running tests via cron

The test suite requires a connected terminal (tty). When invoked via cron, no tty is connected and the test suite may respond with this error:

can't read "multipass_name": no such variable

To run the tests successfully via cron, connect a terminal by redirecting stdin from a tty, e.g. /dev/tty40. (In Linux, you can press alt-Fx or ctrl-alt-Fx to switch the console from /dev/tty1 to tty7. There are many more /dev/tty* which are not accessed via function keys. To be safe, use a tty greater than tty7)

./runUnit < /dev/tty40

If the process doesn’t run as root (recommended), root will have to change the owner and permissions of the tty:

sudo chmod o+r /dev/tty40

To make this permission permanent (at least on Debian) - and not revert back on reboot - create the file /etc/udev/rules.d/10-mydejagnu.rules, containing:

KERNEL=="tty40", MODE="0666"

To start the test at 01:00, set the crontab to this:

* 1 * * * cd bash-completion/test && ./cron.sh < /dev/tty40

Here’s an example batch file cron.sh, to be put in the bash-completion test directory. This batch file only e-mails the output of each test-run if the test-run fails.

#!/bin/sh

set -e  # Exit if simple command fails
set -u  # Error if variable is undefined

CRON=running
LOG=/tmp/bash-completion.log~

    # Retrieve latest sources
git pull

    # Run tests on bash-4

./runUnit --outdir log/bash-4 --tool_exec /opt/bash-4.0/bin/bash > $LOG || cat $LOG
./runCompletion --outdir log/bash-4 --tool_exec /opt/bash-4.0/bin/bash > $LOG || cat $LOG

    # Clean up log file
[ -f $LOG ] && rm $LOG

16.2. Specifying bash binary

The test suite standard uses bash as found in the tcl path (/bin/bash). Using --tool_exec you can specify which bash binary you want to run the test suite against, e.g.:

./runUnit --tool_exec /opt/bash-4.0/bin/bash

Chapter 17. Maintenance

17.1. Adding a completion test

You can run cd test && ./generate cmd to add a test for the cmd command. This will add two files with a very basic tests:

test/completion/cmd.exp
test/lib/completions/cmd.exp

Place any additional tests into test/lib/completions/cmd.exp.

17.2. Fixing a completion test

Let’s consider this real-life example where an ssh completion bug is fixed. First you’re triggered by unsuccessful tests:

$ ./runCompletion
...
        === completion Summary ===

# of expected passes        283
# of unexpected failures    8
# of unresolved testcases   2
# of unsupported tests      47

Take a look in log/completion.log to find out which specific command is failing.

$ vi log/completion.log

Search for UNRESOLVED or FAIL. From there scroll up to see which .exp test is failing:

/@Running ./completion/ssh.exp ...
...
UNRESOLVED: Tab should complete ssh known-hosts at prompt

In this case it appears ssh.exp is causing the problem. Isolate the ssh tests by specifying just ssh.exp to run. Furthermore add the --debug flag, so output gets logged in dbg.log:

$ ./runCompletion ssh.exp --debug
...
        === completion Summary ===

# of expected passes        1
# of unresolved testcases   1

Now we can have a detailed look in dbg.log to find out what’s going wrong. Open dbg.log and search for UNRESOLVED (or FAIL if that’s what you’re looking for):

UNRESOLVED: Tab should complete ssh known-hosts at prompt

From there, search up for the first line saying:

expect: does "..." match regular expression "..."

This tells you where the actual output differs from the expected output. In this case it looks like the test "ssh -F fixtures/ssh/config <TAB>" is expecting just hostnames, whereas the actual completion is containing commands - but no hostnames. So what should be expected after "ssh -F fixtures/ssh/config <TAB>" are both commands and hostnames. This means both the test and the completion need fixing. Let’s start with the test.

$ vi lib/completions/ssh.exp

Search for the test "Tab should complete ssh known-hosts". Here you could’ve seen that what was expected were hostnames ($hosts):

set expected "^$cmd\r\n$hosts\r\n/@$cmd$"

Adding all commands (which could well be over 2000) to expected, seems a bit overdone so we’re gonna change things here. Lets expect the unit test for _known_hosts assures all hosts are returned. Then all we need to do here is expect one host and one command, just to be kind of sure that both hosts and commands are completed.

Looking in the fixture for ssh:

$ vi fixtures/ssh/known_hosts

it looks like we can add an additional host ls_known_host. Now if we would perform the test "ssh -F fixtures/ssh/config ls<TAB>" both the command ls and the host ls_known_host should come up. Let’s modify the test so:

$ vi lib/completions/ssh.exp
...
set expected "^$cmd\r\n.*ls.*ls_known_host.*\r\n/@$cmd$"

Running the test reveals we still have an unresolved test:

$ ./runCompletion ssh.exp --debug
...
        === completion Summary ===

# of expected passes        1
# of unresolved testcases   1

But if now look into the log file ‘dbg.log` we can see the completion only returns commands starting with ls but fails to match our regular expression which also expects the hostname `ls_known_host’:

$ vi dbg.log
...
expect: does "ssh -F fixtures/ssh/config ls\r\nls           lsattr       lsb_release  lshal        lshw         lsmod        lsof         lspci        lspcmcia     lspgpot      lss16toppm\r\nlsusb\r\n/@ssh -F fixtures/ssh/config ls" (spawn_id exp9) match regular expression "^ssh -F fixtures/ssh/config ls\r\n.*ls.*ls_known_host.*\r\n/@ssh -F fixtures/ssh/config ls$"? no

Now let’s fix ssh completion:

$ vi ../contrib/ssh
...

until the test shows:

$ ./runCompletion ssh.exp
...
        === completion Summary ===

# of expected passes        2

17.3. Fixing a unit test

Now let’s consider a unit test failure. First you’re triggered by unsuccessful tests:

$ ./runUnit
...
        === unit Summary ===

# of expected passes        1
# of unexpected failures    1

Take a look in log/unit.log to find out which specific command is failing.

$ vi log/unit.log

Search for UNRESOLVED or FAIL. From there scroll up to see which .exp test is failing:

/@Running ./unit/_known_hosts_real.exp ...
...
FAIL: Environment should stay clean

In this case it appears _known_hosts_real.exp is causing the problem. Isolate the _known_hosts_real test by specifying just _known_hosts_real.exp to run. Furthermore add the --debug flag, so output gets logged in dbg.log:

$ ./runUnit _known_hosts_real.exp --debug
...
        === completion Summary ===

# of expected passes        1
# of unexpected failures    1

Now, if we haven’t already figured out the problem, we can have a detailed look in dbg.log to find out what’s going wrong. Open dbg.log and search for UNRESOLVED (or FAIL if that’s what you’re looking for):

FAIL: Environment should stay clean

From there, search up for the first line saying:

expect: does "..." match regular expression "..."

This tells you where the actual output differs from the expected output. In this case it looks like the the function _known_hosts_real is unexpectedly modifying global variables cur and flag. In case you need to modify the test:

$ vi lib/unit/_known_hosts_real.exp

Chapter 18. Rationale

18.1. Naming conventions

18.1.1. Test suite or testsuite

The primary Wikipedia page is called test suite and not testsuite, so that’s what this document sticks to.

18.1.2. script/generate

The name and location of this code generation script come from Ruby on Rails' script/generate.

Chapter 19. Reference

Within test scripts the following library functions can be used:

Chapter 20. Test context

The test environment needs to be put to fixed states when testing. For instance the bash prompt (PS1) is set to the current test directory, followed by an at sign (@). The default settings for bash reside in config/bashrc and config/inputrc.

For each tool (completion, install, unit) a slightly different context is in effect.

20.1. What happens when tests are run?

20.1.1. completion

When the completions are tested, invoking DejaGnu will result in a call to completion_start() which in turn will start bash --rcfile config/bashrc.

What happens when completion tests are run? 

               | runtest --tool completion
               V
    +----------+-----------+
    |  lib/completion.exp  |
    |  lib/library.exp     |
    |  config/default.exp  |
    +----------+-----------+
               :
               V
    +----------+-----------+    +---------------+    +----------------+
    |  completion_start()  +<---+ config/bashrc +<---| config/inputrc |
    | (lib/completion.exp) |    +---------------+    +----------------+
    +----------+-----------+
               |                                   ,+----------------------------+
               |                               ,--+-+  "Actual completion tests" |
               V                              /   +------------------------------+
    +----------+-----------+    +-----------------------+
    |   completion/*.exp   +<---| lib/completions/*.exp |
    +----------+-----------+    +-----------------------+
               |          \                        ,+--------------------------------+
               |           `----------------------+-+  "Completion invocation tests" |
               V                                  +----------------------------------+
    +----------+-----------+
    |   completion_exit()  |
    | (lib/completion.exp) |
    +----------------------+

Setting up bash once within completion_start() has the speed advantage that bash - and bash-completion - need only initialize once when testing multiple completions, e.g.:

    runtest --tool completion alias.exp cd.exp

20.1.2. install

What happens when install tests are run? 

                 | runtest --tool install
                 V
            +----+----+
            | DejaGnu |
            +----+----+
                 |
                 V
    +------------+---------------+
    | (file: config/default.exp) |
    +------------+---------------+
                 |
                 V
    +------------+------------+
    | (file: lib/install.exp) |
    +-------------------------+

20.1.3. unit

What happens when unit tests are run? 

               | runtest --tool unit
               V
          +----+----+
          | DejaGnu |
          +----+----+
               |
               V
    +----------+-----------+
    |          -           |
    | (file: lib/unit.exp) |
    +----------------------+

20.2. bashrc

This is the bash configuration file (bashrc) used for testing:

# bashrc file for DejaGnu testsuite

        # Use emacs key bindings
set -o emacs
        # Use bash strict mode
set -o posix
        # Unset `command_not_found_handle' as defined on Debian/Ubuntu, because this
        # troubles and slows down testing
unset -f command_not_found_handle
        # Set fixed prompt `/@'
TESTDIR=$(pwd)
export PS1='/@'
export PS2='> '
        # Configure readline
export INPUTRC=$SRCDIR/config/inputrc
    # When not running via cron, avoid escape junk at beginning of line from
    # readline, see e.g.  http://bugs.gentoo.org/246091
[ "$CRON" ] || export TERM=dummy
        # Ensure enough columns so expect doesn't have to care about line breaks
stty columns 150
    # Also test completions of system administrator commands, which are
    # installed via the same PATH expansion in `bash_completion.have()'
export PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin

        # Make sure default settings are in effect
unset -v \
    COMP_CONFIGURE_HINTS \
    COMP_CVS_REMOTE \
    COMP_KNOWN_HOSTS_WITH_HOSTFILE \
    COMP_TAR_INTERNAL_PATHS

        # Load bash testsuite helper functions
. $SRCDIR/lib/library.sh

# Local variables:
# mode: shell-script
# sh-basic-offset: 4
# sh-indent-comment: t
# indent-tabs-mode: nil
# End:
# ex: ts=4 sw=4 et filetype=sh

20.3. inputrc

This is the readline configuration file (inputrc) used for testing:

# Readline init file for DejaGnu testsuite
# See: info readline

        # Press TAB once (instead of twice) to auto-complete
set show-all-if-ambiguous on
        # No bell.  No ^G in output
set bell-style none
        # Don't query user about viewing the number of possible completions
set completion-query-items -1
        # Display completions sorted horizontally, not vertically
set print-completions-horizontally on
        # Don't use pager when showing completions
set page-completions off

# Local variables:
# mode: shell-script
# sh-basic-offset: 4
# sh-indent-comment: t
# indent-tabs-mode: nil
# End:
# ex: ts=4 sw=4 et filetype=sh

Index