This post contains a short introduction to the zsh completion system, a full example that can be used as a starting point for new scripts, and some explanation of some of the more interesting parts of writing completion scripts.
Table of contents
Basics of zsh’s completion system
The zsh completion system (compsys
) is the part of zsh that takes care of
providing the nice tab-completions you’re used to when typing in commands in
your shell. You can find the full documentation here
or you can have a look at the source code, the most
interesting bit being the _main_complete
function, however, it’s all a bit of
a mouthful so I’ll provide the basic information here.
The completion system needs to be activated. If you’re using something like
oh-my-zsh then this is already taken care of, otherwise you’ll need
to add the following to your ~/.zshrc
autoload -U compinit
compinit
When you type foobar <tab>
into your shell zsh will invoke the completion
function that has been registered for foobar
. The completion function
provides the relevant completions to zsh by invoking a set of builtin functions
that are part of compsys
– We’ll look at one of these functions later.
Completion functions can be registered manually by using the compdef
function
directly like this compdef <function-name> <program>
. However, more commonly
you’ll define the completion function in a separate file. By convention
completion functions, and the files they live in, are prefixed with an
underscore and named after the program they provide completions for. When the
completion system is being initialized through compinit
zsh will look through
all the files accessible via the fpath
and read the first line they contain,
as such you simply register a completion function by putting it somewhere
that’s on your fpath
and ensuring that the first line contains the compdef
command like this #compdef _foobar foobar
The fpath
is the list of directories that zsh will look at when searching for
functions. If you’re unsure what it’s set to simply run echo $fpath
. If you
want to append a directory just reassign the variable like so
fpath=($fpath <path-to-folder>)
With the basics out of the way, let’s have a look at a full example.
Example completion script
Imagine you have a program with an interface like the following
hello -h | --help
hello quietly [--silent] <message>
hello loudly [--repeat=<number>] <message>
This imaginary program has two command quietly
and loudly
that each have
distinct arguments you can pass to them – ideally we’d like the completion
script to complete -h
, --help
, quietly
, and loudly
when no commands are
supplied, and once either quietly
or loudly
has been entered it should give
context specific completions for those.
The following zsh script provides completions for the program as described. In the rest of the post I’ll give an explanation of the general outline of the script and dive into some of the more interesting parts.
#compdef _hello hello
function _hello {
local line
_arguments -C \
"-h[Show help information]" \
"--h[Show help information]" \
"1: :(quietly loudly)" \
"*::arg:->args"
case $line[1] in
loudly)
_hello_loudly
;;
quietly)
_hello_quietly
;;
esac
}
function _hello_quietly {
_arguments \
"--silent[Dont output anything]"
}
function _hello_loudly {
_arguments \
"--repeat=[Repat the <message> any number of times]"
}
There are a few things worth going into here, especially the arguments passed
to _arguments
function and the use of local
, but first off let us look at
the general structure of the script.
General structure
There’s nothing special about a zsh completion script. It’s just a normal zsh
script that uses #compdef <function> <program>
to register itself as a
completion script for program
, so you’re free to structure your script anyhow
you see fit, but I’ve found the following structure to be helpful.
Define a function named _<program>
that provides the default completions. For
each sub-command that the program provides define a _<program>_<sub-command>
function that provides completions for that sub-command. In my experience this
makes the completion script pretty straight-forward to write.
The use of _arguments
By invoking the _arguments
function the script provides the potential
completions to zsh. There are many other functions you can use to achieve this,
see section 20.6 in the documentation.
There are two interesting parts about the use of _arguments
in this case. The
string arguments are called specs
and they can be a bit cryptic when you
first encounter them – you really don’t have much in the way of abstraction in
zsh so everything that’s a bit complex is encoded inside of strings leaving you
to learn these small domain specific languages. In this case the specs
can
take two forms:
- option specs:
OPT[DESCRIPTION]:MESSAGE:ACTION
- command specs:
N:MESSAGE:ACTION
. N indicates that it is the Nth command argument.
The ACTION
part is again it’s own lille domain specific language.
This is best description of this language I’ve found, but again,
the documentation has all the details if you search
for specs: overview
.
The -C
flag, together with the ACTION
specification "*::arg:->args"
is
where it becomes interesting. Here’s the description of the -C
flag from the
documentation:
In this form, _arguments processes the arguments and options and then returns control to the calling function with parameters set to indicate the state of processing; the calling function then makes its own arrangements for generating completions.
The parameters they mention are the following:
local context state state_descr line
typeset -A opt_args
You can think of this as a way to have _arguments
return multiple values –
it’s modifying global variables but due to the use of typeset -A
and local
it’s only modified in the current call-graph. The -A
option to typeset
tells zsh that the parameter is an associative array.
So the -C
flags gives us to opportunity to inspect the completion state and
provide context specific completions based on what the user has entered. In our
case we’re only using the line
variable to switch on what sub-command the
user has entered and then invoking the relevant function to provide completions
for that command.
I hope this clarifies some of the aspects of writing completion scripts.
Resources
The zsh documentation has all the information you could possibly need, but it can be a bit overwhelming. I recommend having a look at the zsh-completions project. It has a ton of good examples and their guide on how to write completion scripts is great.