skip to content

Completion System

zsh's compsys — the reason many people switch shells: context-aware completion driven by compinit, completion functions and zstyle. Interviewers probe it because it is zsh's biggest concrete advantage over bash.

part ofCommand-line shellsoverview, primer and where to startread it →
on this pageshow

questions

6

A CLI you installed dropped a zsh completion function file named `_mytool` into a directory on disk, but in your zsh session pressing Tab after `mytool ` still only completes filenames. How does zsh's completion system find completion functions, and what has to be in place before that file takes effect?

level: juniorimportance: must knowfreq 62%

answer

  1. completion functions are just autoloadable functions
  2. there is a search path for functions
  3. the array is scanned once, at init
  4. fpath entries must precede compinit
  5. a stale dump file can mask a new file

basics

~20 s

zsh looks for completion functions in the directories listed in its fpath array, and only uses them once the completion system has been initialised by running autoload -Uz compinit followed by compinit. The directory must be on fpath before compinit runs.

solid answer

~40 s

zsh's new completion system (compsys) is a set of autoloadable shell functions. Files whose names start with an underscore, like `_mytool`, are found by searching the `fpath` array — the search path for autoloadable functions — and they are only registered when `compinit` runs and reads each file's `#compdef` tag line to learn which command it completes. So two things must be true: the directory holding `_mytool` has to be in `fpath`, and it has to be added **before** the `autoload -Uz compinit && compinit` lines in your `.zshrc`, because compinit scans `fpath` once at that moment. A third, common gotcha is the dump file `~/.zcompdump`: compinit caches what it found there, and if a stale dump is reused the new function is ignored until you delete it and re-run `compinit`.

go deeper

for a junior

Be able to say that completion functions are found through the fpath array and switched on by autoload -Uz compinit followed by compinit, and that the directory must be added before compinit runs.

for a middle

Explain what compinit does during its scan: reading the #compdef tag line of each underscore-prefixed file and building the command-to-function map, then caching the result in the dump file.

for a senior

Show how you would debug this on someone else's machine: check the fpath contents, check whether the command has an entry in the _comps association, then suspect a stale dump or a compinit -C fast path.

for a principal

Own the packaging side of it: decide whether your tooling installs into a directory that is already on fpath everywhere, or whether every user must edit their config, and understand the startup-time cost of a large fpath scan for the whole team.

## What compsys actually is zsh's modern completion system, usually called compsys, is not a builtin table of rules. It is a large collection of ordinary zsh **functions** that ship with zsh, plus whatever functions you or your packages add. When you press Tab, zsh works out the context (which command you are typing, which argument you are on) and calls the function registered for that command. Because these are just functions, getting a new completion to work is the same problem as getting any autoloadable function to work: zsh has to be able to find it, and it has to be told the function exists. ## fpath: the search path for functions `fpath` is an array parameter — the function equivalent of `PATH`. When a function is marked autoloadable, zsh does not read its body until the first call; at that point it searches each directory in `fpath` for a file named after the function and loads it. Completion functions live in files named with a leading underscore (`_mytool`, `_git`, `_ssh`), which is a naming convention, not a requirement of the shell. Distributions and package managers install their completion files into a directory that is on `fpath` by default, typically a `site-functions` directory under the zsh share path. A tool that installs somewhere else — say into `~/.local/share/zsh/completions` — needs that directory added yourself: ```bash fpath=(~/.local/share/zsh/completions $fpath) ``` ## compinit: what initialisation does Adding to `fpath` alone changes nothing for completion. The registration step is `compinit`: ```bash autoload -Uz compinit compinit ``` `autoload -Uz compinit` marks the `compinit` function itself as autoloadable (`-U` suppresses alias expansion while loading, `-z` forces zsh-style autoloading — both are defensive flags you should just always use). Calling `compinit` then walks every directory in `fpath`, looks at each file whose name starts with `_`, and reads its **first line**, the `#compdef` tag line: ```bash #compdef mytool ``` That line is a comment as far as the shell is concerned, but compinit parses it to build the mapping from command name to completion function. It also defines the `compdef` function, installs the completion widgets on the keymap, and sets up the machinery the completion functions rely on. ## Why the order in .zshrc matters The scan happens once, when `compinit` is called. Appending a directory to `fpath` *after* that call means compinit never saw it, so the file is never registered — the single most common reason a freshly installed completion appears to do nothing. Put every `fpath` modification above the `compinit` call. ## The dump file Scanning every file in every `fpath` directory on each shell start would be slow, so compinit writes what it learned to a dump file, by default `${ZDOTDIR:-$HOME}/.zcompdump`, and reuses it on subsequent starts. compinit does try to notice when the set of completion files has changed and rebuild, but the check is not perfect, and it is skipped entirely if you call `compinit -C` for faster startup. The standard cure when a new completion refuses to appear is: ```bash rm -f ~/.zcompdump && compinit ``` Or just start a fresh shell after removing the dump. You can also point compinit at an explicit dump file with `compinit -d <file>`, which is useful when several zsh configurations share a home directory. ## What is not the problem Completion files do not need the execute bit — they are read by the shell, not executed as programs. They should not be sourced from `.zshrc`; sourcing `_mytool` directly would run its body outside a completion context and typically error. And the leading underscore matters: compinit only considers files that look like completion functions. ## Checking your work After re-running `compinit`, `echo $_comps[mytool]` prints the function name registered for that command (`_comps` is the association compinit builds), and `which _mytool` shows whether the function loaded. If `_comps[mytool]` is empty, the file is either not on `fpath`, missing or misspelling its `#compdef` line, or masked by a stale dump.

  • What is `~/.zcompdump` and when do you have to remove it?
    It is the cache compinit writes after scanning `fpath`, so later shells can skip the scan. compinit tries to detect changes and rebuild it, but the check is not exhaustive and `compinit -C` skips it altogether. If you install a completion file and the shell still ignores it after restart, delete the dump and re-run `compinit`.
  • What does the `#compdef` line at the top of a completion file do?
    It is a comment to the shell but a directive to compinit: during its scan, compinit reads that first line to learn which command or commands the function completes, and records the mapping. A completion file with no `#compdef` line is loadable but never bound to any command, so Tab falls back to default completion.
  • Why is `autoload -Uz compinit` written with those two flags rather than a bare `autoload compinit`?
    `-U` loads the function with aliases suppressed, so a user alias cannot change the meaning of a word inside shipped zsh code. `-z` forces zsh-style autoloading regardless of the `KSH_AUTOLOAD` option. Together they make loading deterministic no matter what the surrounding configuration does.

saying these in an interview costs you the question

  • Thinks the completion file must be executable
  • Sources the _mytool file directly from .zshrc
  • Adds to fpath after calling compinit
  • Believes compinit re-scans fpath on every prompt
  • Confuses fpath with PATH

context

open as a page

zsh's completion system is configured with `zstyle` on context strings such as `':completion:*:*:git:*'`. What is a zstyle context, how does zsh decide which of several matching zstyle lines applies, and what would you set to get case-insensitive matching and an arrow-key menu?

level: middleimportance: must knowfreq 55%

basics

~20 s

A zstyle context is a colon-separated string describing the exact situation being completed, and zstyle lines are patterns matched against it, with the most specific matching pattern winning. Case-insensitive matching comes from the matcher-list style; the arrow-key menu comes from the menu style set to select.

open as a page

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%

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.

open as a page

Every new zsh session on a machine prints `zsh compinit: insecure directories, run compaudit for list` and then asks whether to continue. What is compinit checking, why does it care, and how should you resolve it?

level: middleimportance: should knowfreq 46%

basics

~20 s

compinit refuses to load completion functions from directories or files that are writable by group or others, or not owned by root or by you, because those functions are shell code that runs as you. Run compaudit to list the offenders and tighten their ownership and permissions.

open as a page

In zsh, pressing Tab after a command that must query a slow source — a package index or a remote inventory — freezes the prompt for a second or two each time. How would you find out where the time goes, and what does zsh's completion system offer to fix it?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Time the completion by running the underlying query by hand, then check whether the completion function supports zsh's completion cache. Turning on the use-cache style, with cache-path pointing at a writable directory, makes such functions store and re-read candidate lists instead of re-querying on every Tab.

open as a page

You own a CLI that everyone on your team uses daily, and you are deciding how its zsh completion should be produced and delivered. How would you choose between a hand-written completion function, one generated from the CLI's own argument parser, and `compdef _gnu_generic <cmd>` — and what do you owe users beyond the initial install?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Choose by how much the completion must know and who will maintain it: generated completions stay in sync with the parser, hand-written ones give the best experience but drift, and _gnu_generic costs nothing but only completes option names. Delivery and staleness matter as much as the choice.

open as a page