An Introduction to Bash Completion Definitions

For my project „pulseaudio-tcp“, a commandline utility for Pulseaudio/Pipewire redirection  (see here for more information), i wanted to have more comfortable completion in Bourne Again Shell („Bash“), offering me the available options and command arguments when using the Tabulator key. In this article i demonstrate how i created an assistance for terminal usage of the pulseaudio-tcp command, leveraging the „Programmable Completion“ feature of Bash.

For the presented solution a reasonably recent version of Bash (4.2 or later) is assumed. It is also assumed that the user has a basic understanding of control flow, variables, functions and arrays in Bash. The demonstrated techniques will make use of some advanced Bash features such as array expansion with pattern matching; these will be explained „on the go“ as required. The general solution could be reused to create completion logic for commands with parameter semantics that are similar to „pulseaudio-tcp“, and the presented knowledge can serve as a starting point for creating more complex completion assistance. I personally consider this an example of intermediate difficulty for aspiring Bash programmers, where the final example features some of Bash’s more arcane behavior (especially when it comes to arrays).

Description of the Program’s Commandline

The program pulseaudio-tcp, if used from the command line, offers several options and arguments.

All options are of the „long option“ style and have no short counterpart:

  • --help causes the program to emit a help message and then exit.
  • --debug causes the program to emit additional diagnostic information.
  • --nogui causes the program to fall back to terminal-based user interaction even if a display session was detected.

It is mandatory to provide exactly one command verb as an argument, which can be one of the following:

  • start causes the program to start the redirection.
  • stop causes the program to stop a currently started redirection.
  • restart is equivalent to an execution of stop followed by an execution of start.
  • status causes the program to check if the redirection is currently started, If true, the program exits successfully, otherwise if exits with an error.
  • setup causes the program to perform an interactive setup procedure.

Thus, from the perspective of a line-per-line command line interpreter that tokenizes each line into a series of „words“, after having encountered the name of the program, pulseaudio-tcp as first word, it would expect a series of words where one and exactly one of the words must be one of the command verbs and the others (if any) may be the supported options.

Please note: If you do not have „pulseaudio-tcp“ installed and want to follow the examples below you could define a dummy function that just reports the parameters it has been called with, like this:

pulseaudio-tcp() { echo "Arguments: $*" ; }

This will be sufficient for following the code examples below.

Introduction to Bash’s Programmable Completion

First and foremost, Bash automatically performs command completion on the first word of the command line. To experience this, type pulsea in Bash and then press the Tabulator key; Bash expands this string to the word pulseadio-tcp, appends a word separator and places the cursor at the end of the line. Bash can and will do this for every program that is present as an executable file in any of the directories listed in the environment variable PATH.

If there are multiple choices, i.e. given the string the users has already typed there are several possible completions to program names that share the given string as a common prefix, Bash provides a visual list of possible completions to choose from. For example, if you type pu, this string could potentially be completed to two possible command names: pulseaudio-tcp and the built-in command pushd ( denotes that you have pressed the „horizontal tabulator“ key):

prompt> pu

pulseaudio-tcp pushd

If you in that situation type the letter l and then press Tabulator again, only one possible completion remains ( denotes a literal blank space):

prompt> pul

prompt> pulseaudio-tcp

In the following it can be assumed that the program name already has been typed in or completed and is present on the command line as the first word. It is also assumed, that the first word has been completed, i.e. following the first word, one or more word-separating characters (i.e. blank spaces) are present on the command line, and the cursor is located after these separators. This means, the command line buffer now contains this:

prompt> pulseaudio-tcp

The command line interpreter now is able to determine that it should look up possible completions for parameters of this specific command invocation, and Bash offers the possibility to define the logic of what should happen if a user presses the Tabulator key in this situation:

  1. Variant 1: The logic is directly provided as a static list of valid words.
  2. Variant 2: The logic is provided as a Bash function that interprets the current contents of the command line and determines possible completions in a context-sensitive manner.

In Bash, there are two built-in commands that are important for defining completion logic, complete and compgen. If you want to define possible completions using simple lists of words, you will only need complete.

Variant 1: Completion Definition using a List of Words

In our example, we could interpret the union of all options and all command verbs as the set of words that may follow the command name (in the following also called „valid parameter values“). Thus, the following is a list of words that may follow the command name pulseaudio-tcp:

  • --help
  • --debug
  • --nogui
  • start
  • stop
  • restart
  • status
  • setup

Bash can be instructed to associate this word list using the built-in function complete which takes a space-separated list of all these words as a single argument to it’s -W option::

complete -W "--help --debug --nogui start stop restart status setup" pulseaudio-tcp

Now, if you type the Tabulator key after the first word, you will be informed about the possible completions:

prompt> pulseaudio-tcp
--debug --help --nogui restart setup start status stop

If you start typing a prefix of one or more of the available completions, narrowing down the set of possible completions that can continue, and then press Tabulator again, a reduced set of choices will be displayed. For example, if you narrow down the possible completions to the set of options by typing -- and then pressing Tabulator, only the options of pulseaudio-tcp will be shown as possible word completions:

prompt> pulseaudio-tcp␣--
--debug --help --nogui

Congratulations, you have defined a Bash command line completion, and terminal users can now easily inform themselves about all valid arguments that the command supports.

You can use the built-in command compgen to simulate the set of possible completions the command line interpreter would provide if given some incomplete word, for example the string st:

prompt> compgen -W "--help --debug --nogui start stop restart status setup" st
start
stop
status

Possible Motivations for more complex Completion Logic

The simple word list approach has a few shortcomings:

  1. Given a word list alone, the command line interpreter can not react to more complex requirements, for example certain command verbs only supporting a specific subset of options or certain options consuming one or more of the following arguments as their values. Our example command, pulseaudio-tcp does not have such elaborate requirements.
  2. However, what might come to our attention is that the completion defined by the word list will offer completion of more than one command verb. This is not supported by the program pulseaudio-tcp and would lead to an error. For example, if you have already typed in the command verb start, it is not correct to type in any more command verbs, but Bash will show them as possible completions nonetheless:
prompt> pulseaudio-tcp␣stop␣s
setup start status stop 

This is not desirable; the user could assume that a command line such as pulseaudio-tcp stop start would be valid, which it is not.

Variant 2: Using a Function to define Completion Logic

If you pass the name of a Bash function instead of a word list, that function is called every time a Tab-completion is requested for the given command. The function interacts with the command line interpreter by means of special Shell variables the names of which all start with „COMP…“. Three such variables will be presented here; see the Bash manual sections „Programmable Completion“ and „Shell Variables“ for further information.

  • The function can assume that a list of words the interpreter already was able to parse is contained in the Bash array variable COMP_WORDS. The array contains the command name as first element.
    • Note that if the user has already started typing an (incomplete) last word, then the array includes the already typed string as last element.
    • Also note that the cursor need not necessarily be located at the last word, the user could also attempt to complete an intermediate word.
  • The function can assume that the shell variable COMP_CWORD contains the index of the word that the cursor is currently located in.
  • The function is expected to assign the list of completions that are available to the user in this situation to an array variable COMPREPLY.

Given the following command line, where the cursor is currently located at the end of the second word, st:

prompt> pulseaudio-tcpst--debug
  • COMP_WORDS would contain the elements pulseaudio-tcp, st and --debug, and
  • COMP_CWORD would have the value 1 (the index of st in COMP_WORDS).

The outline of such a function-based completion definition could look something like this:

_pulseaudio_tcp_completion() {

    1. Find the word that is currently to be completed, which will be
       the element at COMP_CWORD in array COMP_WORDS.

    2. Determine a list of words that the command in question supports
       as valid parameter values.

    3. Given the list of valid words, based on the currently possibly
       incomplete word "COMP_WORDS[COMP_CWORD]", determine a list of
       effective completions. Note: One way to do this is by determining
       the output of
    
         compgen -W "$valid_words" $incomplete_word

    4. Store the resulting list of effective completions to array COMPREPLY.

    5. Exit successfully.

}

The built-in command complete can be used to assign such a function as the completion logic of a given command using the -F option:

_complete -F _pulseaudio_tcp_completion -- pulseaudio-tcp

The actual Example

Let us refine the plan for steps 1 to 5 outlined above more distinctly for this concrete example:

  1. The first step is easy: The current (possibly incomplete) word that is to be completed is the value of the array COMP_WORDS at index COMP_CWORD, i.e. "${COMP_WORDS[COMP_CWORD]}".
  2. The second step will be accomplished by simply hardcoding lists of valid options and valid command verbs. They will be defined as separate lists, because special restrictions apply for command verbs (only one may be given on a complete command line).
  3. To determine which words can potentially be offered to the user, the following rules will be applied:
    1. If the list of words already present in the command line already contains a command verb, no command verb will be considered as a potential completion.
    2. No completion will be offered that would result in a word that is already present on the command line, i.e. all words from COMP_WORDS will be filtered out from the list of potential completions.
  4. To determine the list of effective completions, compgen will be used, based on the word list from step 3 and the (possibly incomplete) word from step 1. The output of compgen will be stored to the array COMPREPLY using the built-in command mapfile.
  5. When done, the function returns successfully.

Note: In the following, a completion function is described with lots of comments on individual aspects of the code. Click here for an uncommented version.

_pulseaudio_tcp_completions() {

For starters, it is good practice to declare variables as local that will not be used outside of the function:

  local \
    all_commands \
    all_options \
    command_pattern \
    commands \
    cur \
    delete \
    options

Step 1: Determine the (possibly incomplete) word in the command line that is currently subject to completion. By mere convention, this variable is often named „cur“:

  cur=${COMP_WORDS[COMP_CWORD]}

Step 2: Assign lists of options and commands the command „pulseaudio-tcp“ supports:

  options=( "--debug" "--help" "--nogui" )
  commands=( "start" "stop" "status" "setup" "restart" )

Step 3: Filter out unwanted elements from these lists of potential completions.

Step 3, rule a: To avoid offering words that already are present in the command line as completions, delete all words in COMP_WORDS (excluding the word that is currently being edited) from the lists of options and commands:

  for delete in "${COMP_WORDS[@]}" ; do
    [[ $delete = "$cur" ]] && continue
    options=("${options[@]/$delete}")
    commands=("${commands[@]/$delete}")
  done

For later use with compgen, format the list of remaining options as a whitespace-separated list:

  all_options="${options[*]}"

Step 3, rule b: If a command already is present as a word on the command line, do not offer any commands as potential completions.

Turn the list of commands into a pattern that can be used as a regular expression „(command1|command2|…)“. To accomplish this, join the array with the „|“ character:

  printf -v command_pattern "%s|" "${commands[@]}"

Strip the last „|“ and surround the result with „(…)“:

  command_pattern="(${command_pattern%?})"

Finally, match all present words against the pattern and determine the whitespace-separated list of command verbs:

  if [[ ${COMP_WORDS[*]} =~ $command_pattern ]] ; then
    all_commands=""
  else
    all_commands="${commands[*]}"
  fi

Step 4: Use compgen to determine the effective list of possible completions.

Note that the string that is currently being edited ("$cur") is a non-option parameter to compgen, but it might start with a --. To avoid syntax errors with compgen, it has to be specifically marked as non-option using --:

  mapfile -t COMPREPLY < <(
    compgen \
      -W "$all_options $all_commands" \
      -- "$cur"
  )

Step 5: The function is done and returns successfully.

  return 0
}

Once defined, the function can be assigned as completion logic for the command:

complete -F _pulseaudio_tcp_completions pulseaudio-tcp

The above example is heavily commented to explain some of the Bash constructs that are utilized to render the code compact while (hopefully) remaining generic enough to be reusable for similar command completions. Please refer to the completion file that is part of the source code of „pulseaudio-tcp“ for a complete version without any comments.