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?
answer
- completion functions are just autoloadable functions
- there is a search path for functions
- the array is scanned once, at init
- fpath entries must precede compinit
- a stale dump file can mask a new file
basics
~20 szsh 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 szsh'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
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.
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.
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.
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