Situatie
Auto-completion is a great way to make your own tools more accessible. Find out how to take the first steps by building a simple Bash and zsh auto-complete script.
Solutie
How Tab Completion Works
Autocomplete—found everywhere from text messaging apps to IDEs—can seem like a fairly modern innovation, but it’s been present in various forms of Unix since the early 80s, if not earlier. Linux shells like bash and zsh support a completion ecosystem that is more powerful than you might expect.You probably use tab completion most often with commands and filenames.
Command tab completion works by inspecting the PATH environment variable to identify available commands beginning with the letters you’ve typed. Filename completion will supply the rest of a file path (absolute or relative) and help you to narrow down your options, resolving ambiguities as you go. Both will save you a lot of time and manual typing.
But there’s a third type of completion, possibly less popular, but even more powerful, maybe, than the more common types. Argument autocompletion applies to everything after the command name, like options or subcommands:
How to Add Completion to Your Own Scripts
I’ll explain how you can add custom completion for zsh first, then describe a bash equivalent.
I’m going to demonstrate custom command argument completion with a trivial script I use to manage my to-do list. The full script isn’t very complicated, but this excerpt focuses on the most relevant parts:
I have this script saved in a file named todos, so the following commands now run on my system:
- todos: output contents of data file, ignoring lines beginning ~
- todos help: show usage
- todos edit: open data file in editor
This is really just a convenience command, although the full script does some extra work to maintain a separate JSON file on edits. I don’t have a man page for such a simple script, and the help subcommand is handy, but I’d like to add argument completion so that I can discover the edit subcommand or other subcommands I might add in the future.
A Custom Completion Script
Your auto complete script needs to do two things: set up the completion system for your command using built-in scaffolding, and generate possible completions. In zsh, the most basic completion is very straightforward:
The _complete_todos function is a handler that calls compadd to specify suggestions. The compadd function belongs to Zsh’s compinit module. Every word you pass as an argument to compadd represents a possible completion. For a script as simple as this example, it’s perfectly fine to hardcode these with a static call to compadd.With the function defined, the script then loads and runs the compinit module before calling compdef to connect the _complete_todos function with the command, todos.
You can put the above script into your .zshrc file, or another file that it sources. You can also use zsh’s fpath system to store it in a file beginning with an underscore, in an appropriate directory.For development and testing, you can just run the above code directly on the command line. Once you’ve done so, try typing todos and experimenting with the autocomplete behavior.
Bash Differences
Bash is quite different when it comes to completion. In particular, it won’t hold your hand as much as Zsh does. While Zsh handles complete suggestions itself, Bash expects you to work out which suggestions are appropriate, based on what has been typed and where Tab has been pressed. Consider a naive equivalent of the above zsh script:
Bash supplies a complete command instead of Zsh’s function approach, so there’s no need to load or initialize anything. The completion handler sets a known variable—COMPREPLY—instead of echoing suggestions. This variable is a Bash array. If you test this script in Bash, you’ll see that it lists all subcommand suggestions as you’d expect:
The problem is that Bash will always trust your handler to provide all relevant suggestions, without applying any of its own logic on top. So these results aren’t always useful:
It’s your responsibility to tell Bash that “he” should only suggest “help” as a possible completion. You can do this by inspecting some useful variables that Bash provides when Tab is pressed:
- COMP_WORDS is an array containing the full command line split into individual words.
- COMP_CWORD is an index into COMP_WORDS referring to the word that the cursor is currently inside.
Using this data, you can work out which word is being completed and provide contextual suggestions. While there are ways of doing this manually, Bash provides another useful command, compgen. This can generate many types of suggestion based on things like the available functions or commands, and the word typed so far. The -W option will search a static word list:
So you can slot a call to compgen into the suggestion handler and enjoy proper, context-specific suggestions with a very minimal script:
Putting Everything Together
For your personal scripts, you’re likely to be running in a known environment: i.e. either Bash or zsh. But for more portable scripts, that you might want to share with others or use across several systems, you should abstract some of this code. Here’s a full version of the completion script that works in either zsh or Bash, by detecting the shell it’s running in, and running the relevant code.
The ZSH_VERSION and BASH_VERSION environment variables announce which shell we’re currently running in. Remember, the shell that your command is running in is irrelevant, it’s the shell that is invoking the completion system, that the user has pressed Tab in, that matters here.This script stores its suggestions in a SUBCOMMANDS variable that both functions have access to.
They each use it to define the supported completions, zsh by calling the compadd function, and Bash by setting the COMPREPLY variable. Note that Bash needs to do a lot of syntactically-heavy massaging of types to convert between arrays and strings.You can save the above script in a single file—e.g. completion.sh—then source that file from your .bashrc and/or .zshrc like this: . /path/to/completion.sh .
Leave A Comment?