Adding Autocompletion to Bash Scripts
source link: https://keyholesoftware.com/2022/07/18/adding-autocompletion-to-bash-scripts/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Adding Autocompletion to Bash Scripts
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
- Usually available, but you can verify this by using
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.
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.)
- You can also place the contents in a location your terminal already sources (ex:
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 ourtsconvert
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")) |
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:
date
: current behavior, converts a date string to the equivalent epochepoch
: 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 theCOMP_WORDS
array.
- This leverages the fact that
- We enter a
case
statement, which flexes on theCOMP_CWORD
index. - Cases where the user has only supplied 1 word (the name of the
tsconvert
program) will directly leverage thecompgen
builtin to show the hard-coded options of eitherdate
orepoch
. - 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!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK