An Introduction to Programmable Completion in Bash

A Completion Function

Let us refine the outline of the function from the previous section:

  1. The 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. Since the list of valid parameters to pulseaudio-tcp is well known, we can simply provide the function with a hardcoded list of valid options and valid command verbs. It makes sense to define those two 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 as possible completions (not yet regarding the word that is currently being edited, but already regarding the other, complete words that we know from COMP_WORDS), the following rules will be applied:
    1. If any word already present in the command line (i.e. all already completed words from COMP_WORDS) is a command verb, no command verb will be considered 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. compgen will be used to determine the list of effective completions from the list of possible completions from the previous step. 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: See below for a walk-through of the completion function with lots of comments on individual aspects of the code. Click here for the function’s source code without comments.

_pulseaudio_tcp_completions() {

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

  local \
    command_list \
    command_pattern \
    commands \
    cur \
    option_list \
    options \
    word

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 command verbs supported by the command pulseaudio-tcp:

  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 arrays of options and command verbs:

  for word in "${COMP_WORDS[@]}" ; do

    [[ $word = "$cur" ]] && continue

    options=("${options[@]/$word}")
    commands=("${commands[@]/$word}")

  done

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

  option_list="${options[*]}"

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

Turn the list of command verbs into a pattern that can be used as a regular expression (command1|command2|…). This is done as follows: Using printf -v VARIABLE_NAME, join the array values with the pipe character | into a new variable command_pattern, then strip the last | and surround the result with parentheses to form a pattern of the form (value1|value2|…):

  printf -v command_pattern "%s|" "${commands[@]}"
  command_pattern="(${command_pattern%?})"

Match all present words against this pattern and determine the whitespace-separated list of command verbs:

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

Step 4: Use compgen to determine the effective list of possible completions and store the result to the array COMPREPLY.

Instead of assigning to the array directly (COMPGEN=…whatever…) we use the built-in command mapfile which expects a series of values, one per line, on standard input and assigns them to a given array. Note that the -t option to mapfile removes the newline characters from the input before assigning the array values. Also 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 "$option_list $command_list" -- "$cur")

Step 5: The function is done and returns successfully.

  return 0
}

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

complete -F _pulseaudio_tcp_completions pulseaudio-tcp

Again, click here for the code without comments