skip to content

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?

level: middleimportance: should knowfreq 34%

answer

  1. one file, one function, one tag line
  2. a helper that parses the whole command line
  3. specs carry exclusions, description, action
  4. an action that hands control back to you
  5. values paired with their descriptions

basics

~20 s

Write 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 s

You 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
bash
#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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context