Skip to content
Shell & TerminalHow-To Published Updated 7 min readViews unavailable

Zsh Completion Functions: compdef, _arguments, and Match Metadata

Author maintainable Zsh completions with fpath, compinit, #compdef, _arguments, and context-aware actions instead of brittle shell aliases.

Zsh’s completion system is a programmable dispatcher rather than a fixed list of filename candidates. It selects a completion function based on the command and cursor context, then asks that function to generate matches with descriptions and actions. A good completion definition mirrors the command’s actual option and operand grammar, respects existing file completion, and remains easy to reload when the command changes.

The core components are fpath, the compinit function, completion files conventionally named with a leading underscore, #compdef metadata, and helpers such as _arguments and _files. A completion can fail even when its code is correct if it is not discoverable on fpath, was not loaded by compinit, or is registered for a different command name than the executable the user typed.

Register a completion file

Place the function in a directory that is on fpath before initializing the completion system. A file named _acme-tool can advertise that it completes the command acme-tool with a #compdef first line:

#compdef acme-tool

_acme_tool() {
	_arguments \
		'(-h --help)'{-h,--help}'[show help]' \
		'--format=[output format]:format:(text json)' \
		'*:input file:_files'
}

Load completion after configuring the directory:

fpath=("$HOME/.zsh/functions" $fpath)
autoload -Uz compinit
compinit

The completion system scans available function files when compinit runs and uses their metadata to register definitions. The leading #compdef tag is part of the file format; spelling and placement matter. The function body is autoloaded when needed, so the function name and file naming convention should be consistent with the Zsh completion tree. Keep one source of truth for the directory and avoid adding duplicate copies of the same definition later in fpath.

Completion dumps can cache discovery state. Zsh can detect many changes to the set of completion files, but a changed #compdef line or function metadata may require deleting the old dump so compinit rebuilds it. When a new function appears to be ignored, verify fpath, first-line metadata, dump freshness, and registration before rewriting the function logic.

Describe options with _arguments

_arguments maps command-line options and operands to descriptions and completion actions. The syntax is compact, so treat each specification as a small grammar: identify whether it is an option, whether it takes an argument, what forms of the option are equivalent, and what candidates make sense for the argument.

The example above documents -h and --help, offers the values text and json after --format, and completes files for positional operands. The brace expansion creates the two help-option forms before _arguments sees them. For a simpler definition without brace expansion, write separate specifications for short and long forms. Use the exact option spellings accepted by the real command, including whether the command supports --option=value or only a separate argument.

An argument action can call another completion function such as _files, _directories, or _values. Do not complete arbitrary files for an option that expects a hostname, profile name, service, or enum. Reuse a command’s authoritative local metadata where possible, but keep network lookup out of every Tab press unless the user explicitly expects live remote candidates.

If options can appear in any order, describe them so the completion system can recognize already-used flags and current argument context. Avoid suggesting a mutually exclusive option after the user has already selected its counterpart. Where the command grammar is genuinely conditional, inspect the current words and delegate the remaining work to _arguments or an appropriate helper rather than manually splitting an unquoted command line.

Complete operands by context

The active cursor position matters. Completing the first operand can differ from completing a later operand; a command may accept files, directories, subcommands, resource IDs, and options in different positions. Use positional labels and actions to teach the completion system about each operand. For example, a command that accepts a subcommand followed by a file should offer subcommands in the first position and the relevant file type after that subcommand.

File completion should preserve paths with spaces and other shell metacharacters. _files uses Zsh completion machinery and understands the shell’s quoting context; avoid replacing it with ls output or command substitution. If the tool accepts only a subset of filenames, constrain candidates through documented completion arguments or a small helper that returns candidate data without executing user-provided input.

When candidate discovery is expensive, cache only stable metadata and define invalidation. Do not make completion trigger a package installation, remote API call, or destructive command. A completion function is an interactive suggestion path, not validation: the command must still reject invalid or unauthorized input when it executes.

Descriptions improve discoverability, but they should be concise and accurate. When candidates come from a live system, do not leak secrets or unrelated account data into the completion list. Keep dynamic candidate collection bounded and handle the source command failing without breaking the user’s shell.

Debug discovery before matching logic

If Tab produces the default completion instead of the custom one, check the layers in order. Confirm the directory appears in $fpath, the completion file is readable, the #compdef line matches the command, compinit ran after the path was added, and the dump reflects the current definition. Then inspect the command name Zsh sees and the current completion context.

compdef can also register a function dynamically. That is useful when a completion should be attached to a pattern or a context not represented by a single command name. Keep dynamic registrations in one initialization path and make them idempotent; a startup file sourced twice should not create duplicate wrappers or mutate the same function repeatedly.

Test a completion through an interactive shell because zsh -n checks syntax but does not prove that the completion system loaded or selected it. Exercise the command with no words, each option, option arguments, --, quoted paths, spaces, a partial subcommand, and a cursor in the middle of the line. Use Zsh’s completion debugging facilities temporarily when a helper receives unexpected words, and turn verbose tracing back off after collecting the evidence.

Keep temporary debug output off the command line. A completion helper that prints diagnostics to standard output can turn them into candidates; stderr is the appropriate diagnostic stream, but avoid flooding the terminal during every completion. Record the active command context and the helper that ran, not the full line if it may contain a token or password.

Maintain the completion as the command evolves

Treat a completion definition as a small public interface that must track the executable. When an option is renamed, an argument becomes mandatory, or a subcommand changes, update the completion and its tests in the same change. If the command supports multiple versions, detect a stable capability or version boundary and provide a conservative fallback rather than assuming every installation has the newest syntax.

Prefer upstream completion functions for widely used tools when they are maintained with the command. A custom local definition is appropriate for an internal tool or missing upstream behavior, but document its owner and source. Avoid copying a large function into a dotfile where updates cannot be reviewed. Put it in a version-controlled directory and test it in a clean Zsh environment.

Do not confuse #compdef metadata with shell execution safety. Completion code runs in the user’s shell process and can execute commands. Review third-party completion directories as code, restrict who can modify them, and avoid automatically adding writable directories from untrusted locations to fpath. This is especially important when a completion manager downloads plugins or generated definitions.

Zsh completion becomes reliable when discovery, command grammar, and candidate generation are modeled separately. Register the function through fpath and #compdef, describe options with _arguments, delegate file matching to the provided helpers, and verify the full interactive context after each command change.

Related:

Sources:

Comments