Your team ships an internal CLI called `deploytool` that takes a few flags and then a subcommand. In zsh, how would you write a completion function for it, and how do you make zsh use that function for that command?
answer
- one file, one function, one tag line
- a helper that parses the whole command line
- specs carry exclusions, description, action
- an action that hands control back to you
- values paired with their descriptions
basics
~20 sWrite a function in a file named _deploytool whose first line is the #compdef tag line, describe the flags and arguments with the _arguments helper, offer subcommands with _describe, and place the file in a directory on fpath so compinit registers it. compdef binds it manually when you cannot.
solid answer
~50 sYou write a file called `_deploytool` starting with the line `#compdef deploytool`, put it in a directory on `fpath`, and let `compinit` register it. Inside, the workhorse is `_arguments`: you hand it one specification per option — `'(-v --verbose)'{-v,--verbose}'[print each step]'` for a flag with an alias and a description, `'--config=[config file]:config file:_files'` for an option that takes a filename — plus positional specs. For subcommands you give a positional spec an action of the form `->state`, then switch on `$state` and call `_describe` with an array of `name:description` pairs, so each subcommand shows with its help text. Deeper levels use `$words` and `$CURRENT` to see what the user already typed. `compdef` is the manual binding: `compdef _deploytool deploytool` registers a function directly, and `compdef dt=deploytool` gives a wrapper or alias the same completion as the original.
code
bash · 24 lines#compdef deploytool
_deploytool() {
local curcontext="$curcontext" state line
typeset -A opt_args
local -a subcmds
subcmds=(
'plan:show what would change'
'apply:apply the pending changes'
)
_arguments -s -C \
'(-v --verbose)'{-v,--verbose}'[print each step]' \
'--config=[path to config file]:config file:_files' \
'1: :->subcmd' \
'*:: :->args'
case $state in
subcmd) _describe 'subcommand' subcmds ;;
args)
case $words[1] in
apply) _arguments '--yes[do not prompt before applying]' ;;
esac
;;
esac
}
_deploytool "$@"go deeper
Know the shape of the answer: a file named _deploytool with a #compdef first line, placed on fpath, using the _arguments helper rather than parsing the command line yourself.
Read and write an _arguments specification: the exclusion list, the bracketed description, and the :message:action tail, plus using _describe to attach help text to subcommand names.
Handle the subcommand case properly with ->state actions, know why -C and *:: matter for nested completion, and mention the test loop where an already-autoloaded function hides your edits.
Decide how completions are produced and shipped at all: hand-written specs that drift, generated ones tied to the CLI's own parser, or _gnu_generic with no maintenance and no argument completion.
## The file and the tag line A zsh completion for a command is a single autoloadable function, conventionally in a file named after the function with a leading underscore. The first line is the registration directive compinit reads during its scan: ```bash #compdef deploytool ``` One file can serve several commands by listing them all on that line. Drop the file in any directory on `fpath` (before `compinit` runs), and Tab against `deploytool` calls your function. ## _arguments: describing the command line Almost every shipped zsh completion is built on `_arguments`. You give it a list of **specifications**, each describing one option or one positional argument, and it works out which one the cursor is on and offers the right matches. An option specification has three parts: an exclusion list, the option itself, and a bracketed description. ```bash '(-v --verbose)'{-v,--verbose}'[print each step]' ``` The leading parenthesised list says "once one of these is present, stop offering any of them" — that is how `_arguments` avoids suggesting `--verbose` when `-v` is already typed. The brace expansion is plain zsh, producing two specifications that share the same description. An option that takes a value adds colon-separated fields after the description: ```bash '--config=[path to config file]:config file:_files' ``` The pattern is `:message:action`. The message is what the user sees as the group heading; the action says how to generate candidates. Actions can be another completion function (`_files`, `_hosts`, `_users`), a literal list in parentheses, or a `->state` request. Positional arguments use a number instead of an option name — `'1:subcommand:->subcmd'` is the first positional — and `'*::args:->args'` collects everything after it. ## States: handling subcommands A subcommand-style CLI needs two phases: complete the subcommand name, then complete that subcommand's own arguments. `_arguments` supports this with the `->state` action, which does not generate matches itself but sets the `$state` parameter and returns, letting you decide: ```bash _arguments -s -C \ '1: :->subcmd' \ '*:: :->args' case $state in subcmd) _describe 'subcommand' subcmds ;; args) case $words[1] in apply) _arguments '--yes[do not prompt]' ;; esac ;; esac ``` `-s` allows single-letter options to be stacked (`-vf`); `-C` lets `_arguments` update `$curcontext` for the `->state` actions, so your inner completions get their own zstyle context. With `*::`, the double colon makes `$words` and `$CURRENT` be rewritten relative to the sub-command, so the nested `_arguments` sees a command line that starts at the subcommand — exactly what you want for per-subcommand flags. ## _describe: names with descriptions `_describe` takes a tag description and the name of an array of `value:description` strings: ```bash local -a subcmds subcmds=( 'plan:show what would change' 'apply:apply the pending changes' ) _describe 'subcommand' subcmds ``` The descriptions are shown alongside the candidates in the listing, which is the difference between a completion that merely saves typing and one that documents the tool while you use it. Where the values have no descriptions, `_values` or a literal action list `(plan apply)` is enough; `_alternative` combines several kinds of candidate at one point. ## Declaring locals Completion functions run in your interactive shell, so leaking parameters is a real hazard. The conventional preamble is: ```bash local curcontext="$curcontext" state line typeset -A opt_args ``` `$line` and `$opt_args` are populated by `_arguments` with the positional words and the options-with-values seen so far, which is how a later spec can depend on an earlier flag. ## compdef: binding without a file `compdef` is the runtime equivalent of the `#compdef` line. `compdef _deploytool deploytool` binds a function you already defined. The assignment form re-uses an existing completion for another name: `compdef dt=deploytool` makes a wrapper or alias complete like the original, and `compdef -d name` removes a binding. There is also `compdef _gnu_generic mytool`, which completes options by running the command's `--help` and parsing it — zero maintenance, option names only. ## Testing the loop Completion functions are autoloaded once per session, so editing the file does not affect the running shell. Either start a fresh shell, or re-source the function explicitly with `unfunction _deploytool && autoload -Uz _deploytool` before trying again — otherwise you will spend an hour debugging the version zsh loaded ten minutes ago.
- What does the parenthesised list at the start of an `_arguments` option specification do?It names options that become unavailable once this one is present. Writing `'(-v --verbose)'{-v,--verbose}'[print each step]'` stops zsh from offering `--verbose` after the user typed `-v`, and is also how you express mutually exclusive flags. Without it, `_arguments` happily suggests both forms of the same switch.
- Why does editing a completion file have no effect on the shell you are testing in?Completion functions are autoloaded once and then cached as defined functions for the life of the session. Either open a new shell or drop and re-autoload the function — `unfunction _deploytool && autoload -Uz _deploytool` — before retesting. Forgetting this makes a correct fix look broken.
- When is `compdef _gnu_generic mytool` a reasonable choice?When the tool has a conventional `--help` output, you only need option-name completion, and nobody wants to maintain a spec. It parses the help text at completion time, so it stays in sync automatically. The cost is a subprocess per completion, no value or argument completion, and nothing at all if the help format is unusual.
- What is the difference between the `*:` and `*::` positional specifications?Both collect the remaining words, but `*::` additionally rewrites `$words` and `$CURRENT` so they are relative to that point on the command line. That is what lets a nested `_arguments` call complete a subcommand's own flags as if the subcommand were the command being typed.
saying these in an interview costs you the question
- Puts the completion file anywhere and expects it to work
- Forgets the #compdef line and wonders why nothing binds
- Omits local declarations, leaking state into the shell
- Edits the file and retests in the same session
- Thinks _arguments only handles options, not positionals