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?
answer
- ask what happens when a flag is added
- one option reads the live help text
- one is derived from the parser itself
- drift is the real failure mode
- installation and staleness are half the job
basics
~20 sChoose 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.
solid answer
~50 sTreat it as a maintenance decision rather than a quality one. `compdef _gnu_generic mycli` is free — it parses the command's `--help` at completion time, so it never drifts — but it only produces option names, offers nothing for values or subcommands, and spawns a process per Tab. A hand-written `_arguments` function gives the best result: descriptions per flag, per-subcommand argument completion, real value completion from your own data. Its cost is drift, because nothing forces it to change when the CLI does. Generating the function from the parser you already have removes the drift at the price of whatever the generator can express. Beyond the choice, delivery is the part teams get wrong: install into a directory that is already on `fpath` rather than asking every user to edit their config, remember that a stale `~/.zcompdump` can hide a freshly installed file, and keep the function cheap, because it runs synchronously in front of a human waiting at a prompt.
go deeper
Know the three shapes this can take — a hand-written function, a generated one, and _gnu_generic parsing --help — and that the file has to end up somewhere zsh already looks.
Compare them concretely: what each can complete, whether it can drift from the CLI, and what the per-completion cost is when it shells out to the tool.
Bring the operational half: install path versus user-edited fpath, the stale dump file that makes a correct install look broken, and bounding any expensive work so Tab never hangs.
Own the tradeoff and the lifecycle — pick the cheapest option that clears the experience bar, tie the completion's version to the binary's, name an owner for anything hand-maintained, and accept that failures here are silent rather than reported.
## The real axis is maintenance, not capability Everyone reaches for the richest option first. The question that decides it is: *what happens when the CLI adds a flag next quarter?* Completions are documentation that nobody reads until it is wrong, and a completion that offers a removed subcommand is worse than no completion at all, because it teaches confidently wrong things. ## The three approaches **`compdef _gnu_generic mycli`.** `_gnu_generic` runs the command's `--help` and parses option names out of it. Maintenance is zero and drift is impossible, because it reads the live binary. In exchange: option names only — no values, no subcommand awareness, no descriptions beyond what the help text yields — a subprocess on every completion attempt, and total dependence on the help output looking conventional. Good for internal tools where the win is not retyping `--namespace`. **A hand-written `_arguments` function.** The full experience: exclusion groups so alternative spellings stop being offered, per-flag descriptions, `_describe` output pairing each subcommand with its purpose, value completion from your own inventory. This is what makes the shipped completions for mature tools feel good. The cost is that it is a second implementation of your CLI's grammar, maintained by hand, in a language most of the team does not write. It drifts the first time someone ships a flag in a hurry. **Generated from the parser.** If your CLI's argument parser already knows every flag, subcommand and value type, emitting a completion function from it removes the drift by construction — the completion is derived from the same source of truth as the `--help` output. The limits are the generator's expressiveness: dynamic candidates that require calling back into the tool, or context-sensitive values, are usually where generated output stops and hand-written stubs start. A common compromise is generated structure plus a small hand-maintained overlay for the few dynamic cases. ## What you owe users after the choice **Delivery.** A completion nobody installs helps nobody. Installing the `_mycli` file into a directory that is already on `fpath` for your platform's zsh means the completion appears the next time compinit rebuilds. The alternative — instructions telling every user to add a directory to `fpath` *before* their `compinit` call and re-run it — has a real drop-off rate, and produces support requests from people who appended it afterwards. **Staleness of the registration.** compinit caches its scan in `~/.zcompdump`. Its change detection is not exhaustive, and any user running `compinit -C` for startup speed has opted out of detection entirely. So your install notes should say plainly what to do when the completion does not appear: remove the dump and re-run `compinit`, or open a new shell. **Latency budget.** The function runs synchronously while a human waits. A completion that shells out to your CLI, or worse over the network, turns Tab into a stall. If candidates are expensive, use the completion cache — `use-cache` with a sane `cache-policy` — or generate from a local file your tooling refreshes out of band. Whatever you do, bound it: an unbounded network call inside a completion function is how you hang someone's terminal. **Versioning.** The completion should ship with the binary and in the same package, so an upgrade replaces both together. A completion delivered separately becomes a second version to reason about, and users hit the exact drift you were trying to design away. **Correctness under the shell's rules.** The function runs in the user's interactive shell with their configuration. Declare your locals, do not leave parameters behind, do not assume options are set a particular way, and never write outside a cache path you were given. A completion function is code you are asking colleagues to run in their shell on every Tab; the bar is the bar for anything else you ship. ## How to actually decide Start from the cheapest thing that clears the bar. If the CLI is a handful of flags, `_gnu_generic` and move on. If it has subcommands with distinct arguments, generation is worth the build step. Hand-write only the parts generation cannot express, and treat those as code with an owner — because the failure mode of every one of these options is the same, and it is silence: nobody files a bug against a completion, they just stop pressing Tab.
- What does `_gnu_generic` actually give you, and what does it cost?It parses the command's `--help` output at completion time to offer option names, so it can never drift from the binary and needs no maintenance. The costs are that it completes only option names — no values, subcommands or curated descriptions — that it spawns a process per completion, and that unconventional help output yields nothing useful.
- Why does installing into a directory already on fpath matter more than it sounds?Because the alternative depends on every user editing their config correctly, and specifically placing the fpath line before their compinit call. That instruction has a real failure rate, and each failure looks like your tool being broken. Landing in a directory zsh already scans makes the completion appear without any user action.
- How do you keep a completion function from stalling someone's prompt?Do not put an unbounded call inside it. If candidates need an expensive query, use zsh's completion cache with an explicit cache-policy, or read a local file that your tooling refreshes out of band. Any live call needs a short timeout — a completion function runs synchronously with a human waiting on it.
- Why ship the completion in the same package as the binary?So an upgrade moves both together and the completion cannot describe a version the user does not have. Delivered separately, the completion becomes an independently versioned artifact and reintroduces exactly the drift you were trying to design away — with the added confusion that users cannot tell which one is stale.
saying these in an interview costs you the question
- Treats richness as the only axis and ignores maintenance
- Assumes users will happily edit fpath by hand
- Ships the completion separately from the binary
- Puts an unbounded network call inside the function
- Never plans for the stale zcompdump support ticket