Adding Autocompletion to Bash Scripts

Adding Autocompletion to Bash Scripts

Jake Everhart Development Technologies, Programming, Tutorial Leave a Comment

The desire to automate something can be a great asset for a developer. Manual tasks are often error-prone or monotonous, and expending the effort to package that work into an executable can pay dividends once it’s reused in the future. However, even the best executables are only useful as long as their users know how to invoke them and can do so easily.

If you have ever mashed the Tab key to finish typing a filename or to show you the available flags to use when running a program, you know that autocompletion can be a great improvement for a command-line tool. But how easy is this to implement for your own executables?

This blog is a guide providing an overview of how autocompletion can be achieved through bash. We will see some of the core concepts in action, focusing on how they interact with each other and the behavior that results.

What You Need:

  • Shell with autocompletion enabled
    • This is usually enabled by default with bash or bash-like shells
  • Ability to invoke the complete command
    • Usually available, but you can verify this by using which complete and confirming that it exists

Example 1: Stand-Alone Script with Simple Parameter

For our first example, let’s write a script to convert a timestamp into a Unix epoch. We can leverage autocompletion to supply the current date string as our starting timestamp, giving the user an example of the correct syntax. See an example of the behavior below.

Autocompletion in Bash

Since this involves adding autocompletion to a stand-alone script for a single input parameter, it allows us to explore several of the concepts involved through a simple example.

Implementation

The GNU date command provides most of the functionality we need. The following script passes a given string to the date command as a timestamp to be converted, with a guard in place for missing parameters.

#!/bin/bash
function tsconvert() {
  if [ -z "$*" ]; then
    echo "Must provide timestamp to convert"
    return
  fi
 
  date -d "$*" +%s
  # mac: date -j -f "%a %b %e %T %Z %Y" "$*" +"%s"
}

Note for Mac OS users: OSX exposes the BSD version of the date command, which requires a different syntax from the GNU version for formatting (ex: date -j -u -f "%a %b %d %T %Z %Y '[timestamp] "+%s" instead of date -d '[timestamp]' +%s). I have added explicit formatting strings to most of the GNU date commands throughout this guide to keep the syntax as close as possible and have included Mac-compatible versions beneath any lines which still differ between versions.

We can then expose this example script as a tsconvert command by sourcing the file:

    • Save the file under any name (ex: tsconvert.sh)
  • Source the file:
    • source ./tsconvert.sh
      • You can also place the contents in a location your terminal already sources (ex: ~/.bashrc, ~/.oh-my-zsh/custom, etc.)

We can now use the tsconvert command to convert a given timestamp to its epoch format, as shown below.

Now for the interesting part: how do we implement autocompletion for this script’s parameter? To achieve this, we’re going to leverage some of the Programmable Completion Builtins available in bash and bash-like shells.

The following snippet leverages the complete command and COMPREPLY variable to provide the completion behavior we described earlier, auto-populating the parameter with the current timestamp.

function __tsconvert_completion() {
  COMPREPLY=("$(date "+%a %b %e %T %Z %Y")")
}
complete -F __tsconvert_completion tsconvert

Before we go further, let’s examine how the __tsconvert_completion works on its own:

  • We define this as a function, so it can be invoked on-command (when we request the completion behavior by hitting the TAB key).
  • The standalone date command is being used to retrieve the current timestamp.
  • Double quotes surround the subshell in order to treat the entire message as a single “option” for autocompletion.
  • A COMPREPLY variable is being used to store the results and will be consumed when supplying the parameter completion.

To get a better idea of what output is being stored in the COMPREPLY variable, you can execute the following echo statement.

echo "$(date "+%a %b %e %T %Z %Y")"

But how do we associate this function with our tsconvert command? That’s where the final line of that snippet comes into play, the complete command.

complete -F __tsconvert_completion tsconvert

Again, let’s deconstruct the statement above:

  • We invoke the complete command in order to associate autocompletion behavior with our tsconvert function.
  • We supply the -F flag since the autocompletion “options” will be provided by a function.
  • We then supply the name of our function, which will provide the autocompletion.

Sourcing the __tsconvert_completion function and the complete invocation above provides us with the autocompletion behavior shown at the beginning of this example.

Example 2: Multiple Parameter Options and Multi-Layered Completion

We’ve seen how the basics of autocompletion work, but how can we expand on this? What if we want to supply multiple options to choose from? What if we want to have those options scoped to different sub-commands (ex: tsconvert epoch vs tsconvert date)?

Multiple Parameter Options

Allowing for multiple completion options is actually quite simple due to how the COMPREPLY array variable works. By default, whitespace characters are treated as separators between completion options. Since we wanted the entire date output to be treated as a single option, we side-stepped this behavior by deliberately wrapping the output in quotes. We can remove those quotes in order to get an idea of what’s happening behind the scenes.

Previous:

COMPREPLY=("$(date "+%a %b %e %T %Z %Y")")

Modified:

COMPREPLY=($(date "+%a %b %e %T %Z %Y"))

Bash for autocompletion

This behavior can be very useful when deciding how to supply your autocompletion parameter options, as many commands already format their output with whitespace for human readability. However, we can also append to the COMPREPLY variable explicitly if this default behavior isn’t sufficient for us.

The following example adds additional options to our implementation for the previous two midnights.

function __tsconvert_completion() {
  COMPREPLY=("$(date "+%a %b %e %T %Z %Y")")
  COMPREPLY+=("$(date -d 'today 0' "+%a %b %e %T %Z %Y")")
  # mac: COMPREPLY+=("$(date -v 0H -v 0M -v 0S "+%a %b %e %T %Z %Y")")
  COMPREPLY+=("$(date -d 'yesterday 0' "+%a %b %e %T %Z %Y")")
  # mac: COMPREPLY+=("$(date -v -1d -v 0H -v 0M -v 0S "+%a %b %e %T %Z %Y")")
}
complete -F __tsconvert_completion tsconvert

Notice how the options are sorted lexicographically and how our available options are limited based on what text we have already supplied. These behaviors can be modified extensively within your completion function by taking further control over the complete and compgen builtins and by modifying the completion shell variables directly.

Multi-Layered Completion

As one last example, let’s explore how completion behavior can be scoped within different subcommands. We can extend our tsconvert command to provide the following two subcommands:

  1. date: current behavior, converts a date string to the equivalent epoch
  2. epoch: reverse behavior, converts a given epoch to its corresponding date string

The following is a modified version of the tsconvert implementation from earlier, which provides this extra functionality (along with very basic error-handling for subcommand values).

# Convert a given timestamp into its epoch, or vice versa
function tsconvert() {
  local subcommand=$1
 
  if ! [ -z "$*" ]; then
    # Discard subcommand from arg string
    shift
  fi
 
  if [ "$subcommand" = "epoch" ]; then
    # Convert epoch to date
    date -d @"$*" "+%a %b %e %T %Z %Y"
    # mac: date -r "$*"
  elif [ "$subcommand" = "date" ]; then
    # Convert date to epoch
    date -d "$*" +%s
    # mac: date -j -f "%a %b %e %T %Z %Y" "$*" +"%s"
  else
    echo "Usage: tsconvert <command> <arg>"
    echo "Commands available:"
    echo "\\tdate <epoch string>"
    echo "\\tepoch <timestamp>"
  fi
}

In order to make the accompanying changes to our __tsconvert_completion function, we need to know which level of completion the user is requesting. To do this, we can leverage the COMP_WORDS array and the COMP_CWORD index variable which are exposed to us.

function __tsconvert_completion() {
  local cur prev
 
  cur=${COMP_WORDS[COMP_CWORD]}
  prev=${COMP_WORDS[COMP_CWORD-1]}
 
  case ${COMP_CWORD} in
    1)
      # Base-level completion: show subcommands
      COMPREPLY=($(compgen -W "date epoch" -- ${cur}))
      ;;
    2)
      # Inner completion
      case ${prev} in
        date)
          # 'date' subcommand completion
          COMPREPLY=("$(date "+%a %b %e %T %Z %Y")")
          COMPREPLY+=("$(date -d 'today 0' "+%a %b %e %T %Z %Y")")
          # mac: COMPREPLY+=("$(date -v 0H -v 0M -v 0S "+%a %b %e %T %Z %Y")")
          COMPREPLY+=("$(date -d 'yesterday 0' "+%a %b %e %T %Z %Y")")
          # mac: COMPREPLY+=("$(date -v -1d -v 0H -v 0M -v 0S "+%a %b %e %T %Z %Y")")
          ;;
        epoch)
          # 'epoch' subcommand completion
          COMPREPLY=("$(date +%s)")
          ;;
      esac
      ;;
    *)
      # All other cases: provide no completion
      COMPREPLY=()
      ;;
  esac
}

Let’s have a brief review of what’s happening above:

  • We create local variables to store the current word (the one being auto-completed) and previous word at the user’s prompt.
    • This leverages the fact that COMP_CWORD will store the index of the current word within the COMP_WORDS array.
  • We enter a case statement, which flexes on the COMP_CWORD index.
  • Cases where the user has only supplied 1 word (the name of the tsconvert program) will directly leverage the compgen builtin to show the hard-coded options of either date or epoch.
  • In cases where the user has supplied 2 words, we evaluate the second word to determine which subcommand is being used:
    • date: We supply the options used in previous examples.
    • epoch>/code>: We supply the current epoch as the only option.
  • In all other cases (i.e. when the user has already populated more options), we provide no completion suggestions.

Conclusion

With the code snippets above, we have seen an overview of the ways we can enrich our command-line tools through parameter autocompletion. Know, however, that this is just the tip of the iceberg. Since we know how to register completion functions and how to provide suggestions through custom logic, these concepts can be expanded upon to achieve results much more impressive than playing with timestamps.

Every tool has its purpose, and autocompletion may not be needed for everything. For cases where it can be useful, feel free to use any of the code or ideas mentioned in this guide as a starting point.

Let me know what you thought about the post in the comments below, and if you enjoyed it, you can find many more on the Keyhole Dev Blog. I encourage you to take a look!

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments