Zsh & Bash Completions¶
How tab completions are organized in this dotfiles project — for both upstream tools and in-house CLIs (fleet, mlf, pqsum, mi-router, x).
How autocomplete works (mechanics)¶
Tab completion is the same idea on both shells but the plumbing differs.
zsh — compinit + fpath + autoload _<tool> functions¶
compinitscans every directory in$fpathfor files starting with_and registers them as completion candidates. Run by oh-my-zsh once at startup.$fpathis zsh's function search path. Order in this repo (dot_zshrc.tmpl:67-70):- The first line of each file matters:
#compdef <name>tells zsh which command to bind to. If you write#compdef fleet, thenfleet<TAB>invokes the file's_fleetfunction. - Autoload vs eager-load: a file in
$fpathis parsed the first time the user actually TABs the command (lazy). Files sourced explicitly at startup (e.g. viaload_modular_dirin this repo) callcompdef _<func> <cmd>directly — parsed eagerly, no need to be in$fpath. _argumentsDSL (zsh-only) lets you describe options + positional args declaratively:'--hosts=[help text]:hosts:_fleet_hosts_csv'defines the flag, hint, and a custom completer function. Seedot_config/zsh/tools/45_fleet_completion.zsh.
bash — bash-completion v2 + complete -F + COMPREPLY¶
- bash-completion v2 is sourced at shell startup by
dot_config/bash/03_completion.bash. It auto-loads completion files on first TAB. - The user dir is
${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions/— drop a file named after the command (e.g.mi-router) here and v2 lazy-loads it on TAB. complete -F _func cmdregisters a bash function as the completer forcmd. The function reads$COMP_WORDS/$COMP_CWORDand writes candidates to$COMPREPLY._init_completion+compgenare bash-completion v2 helpers —_init_completionpopulatescur/prev/words/cword;compgen -W "list" -- "$cur"filters a wordlist by the current prefix.- Eager vs lazy mirrors zsh: a file in the user dir is lazy; a file sourced via
load_modular_dir(this repo'sdot_config/bash/) callscomplete -Fimmediately.
Three generation strategies in this repo¶
| Strategy | Where the file lives | Trigger | Best for |
|---|---|---|---|
| A. Lazy autoload | zsh ~/.zfunc/_<tool> / bash ${XDG_DATA_HOME}/bash-completion/completions/<tool> |
First TAB on the command | Tools that ship --print-completion <shell> or similar (tyro, click, clap-derive — including this repo's mi-router) |
| B. Eager-load at startup | dot_config/zsh/tools/<NN>_<tool>_completion.zsh + dot_config/bash/<NN>_<tool>_completion.bash |
Sourced via load_modular_dir after compinit / bash-completion v2 init |
Hand-written completion (this repo's fleet, mlf, pqsum, x) |
| C. Cached eval at startup | ${XDG_CACHE_HOME}/<shell>/<tool>_completion.<ext> |
Sourced via shell init script (dot_config/shell/<NN>_<tool>.sh) |
Slow generators whose output has side effects beyond compdef (e.g. thefuck --alias outputs an alias line — needs to land in the running shell's namespace, not just a deferred _<tool> function) |
Cache invalidation in classes A and C uses binary-mtime check:
This catchesbrew upgrade, uv tool upgrade, mise install <NEW> — anything that bumps the binary's mtime. Manual <tool>-update-completion helpers (in dot_config/{shell,zsh}/10_aliases.zsh) cover edge cases (gem-only upgrades, in-place replacements, cache corruption).
Architecture¶
compinit runs once (inside oh-my-zsh.sh)
│
├── ~/.zfunc/ ← user-generated completions (NOT tracked by chezmoi)
├── ~/.oh-my-zsh/custom/plugins/ ← community completions (184 files, cloned by ansible)
│ └── zsh-completions/src/
├── ~/.docker/completions/ ← Docker-shipped completions
├── $(brew --prefix)/share/zsh/ ← Homebrew-managed completions (via `brew completions link`)
│ └── site-functions/
└── $ZSH/completions/ ← oh-my-zsh built-in completions
All fpath additions are in dot_zshrc.tmpl before source "$ZSH/oh-my-zsh.sh", so compinit runs exactly once and picks up everything.
Completion Categories¶
Tools fall into four categories:
A. Self-generating (tool completion zsh > ~/.zfunc/_tool)¶
Most modern CLI tools can output their own completion script. Auto-generated for both shells by the chezmoi apply hook (run_after_50_generate_completions.sh.tmpl → scripts/generate_completions.sh); see Generating Completions After Fresh Install for opt-out + manual rerun.
| Tool | Command |
|---|---|
uv |
uv generate-shell-completion zsh |
mise |
mise completion zsh |
chezmoi |
chezmoi completion zsh |
just |
just --completions zsh |
bat |
bat --completion zsh |
delta |
delta --generate-completion zsh |
starship |
starship completions zsh |
rg |
rg --generate complete-zsh |
fd |
fd --gen-completions zsh |
zellij |
zellij setup --generate-completion zsh |
sesh |
sesh completion zsh |
pueue |
pueue completions zsh |
docker |
docker completion zsh |
gh |
gh completion -s zsh |
opencode |
opencode completion zsh |
bw |
bw completion --shell zsh |
Usage pattern:
# Generate once (or after upgrade)
mkdir -p ~/.zfunc
uv generate-shell-completion zsh > ~/.zfunc/_uv
mise completion zsh > ~/.zfunc/_mise
B. Pre-bundled (no action needed)¶
Completions ship with the package or via the zsh-completions community plugin.
| Tool | Source |
|---|---|
eza |
Homebrew formula installs _eza into site-functions |
git |
oh-my-zsh git plugin |
yazi |
Ships _yazi in release assets (also in zsh-completions) |
lazygit |
No completion support; not needed (TUI app) |
btop |
No completion support; not needed (TUI app) |
| Many others | zsh-completions plugin covers 184 tools |
C. Eval/source at startup (shell integration)¶
These tools inject completions as part of broader shell integration. Most live in the POSIX dot_config/shell/ layer (sourced by both zsh and bash); zsh-only ones live in dot_config/zsh/tools/.
| Tool | Where | How | Cached? |
|---|---|---|---|
fzf |
dot_config/shell/10_fzf.sh |
eval "$(fzf --zsh)" (zsh) / eval "$(fzf --bash)" (bash) |
— |
zoxide |
dot_config/shell/20_zoxide.sh |
eval "$(zoxide init <shell>)" |
— |
direnv |
dot_config/shell/30_direnv.sh |
eval "$(direnv hook <shell>)" |
— |
marimo |
dot_config/shell/29_marimo.sh |
_MARIMO_COMPLETE=<shell>_source marimo |
✅ ${XDG_CACHE_HOME}/{zsh,bash}/marimo_completion.{zsh,bash} — refresh via marimo-update-completion |
thefuck |
dot_config/shell/27_thefuck.sh |
thefuck --alias |
✅ ${XDG_CACHE_HOME}/{zsh,bash}/thefuck_alias.{zsh,bash} — refresh via thefuck-update-completion |
try-cli |
dot_config/zsh/tools/32_try.zsh |
ruby try.rb init (zsh-only) |
✅ ${XDG_CACHE_HOME}/zsh/try_init.zsh — refresh via try-update-completion |
Cached entries auto-invalidate when the tool's binary mtime is newer than the cache file (
[ "$(command -v <tool>)" -nt "$cache" ]) — catchesmise install <NEW>,brew upgrade,uv tool upgrade. Manual refresh helpers handle edge cases: gem-only upgrades that don't bump the ruby binary, in-place tool replacements, or cache corruption. The pattern mirrorsdot_config/zsh/tools/95_bitwarden.zsh(the canonical example).
D. No completion support¶
claude, gemini, agy/agyc (Antigravity CLI), btop, lazygit -- no generation command available.
E. Python tools via shtab / tyro / click / argcomplete¶
Python CLI frameworks can generate completions. Pick by what your script uses:
- tyro (this repo's
mi-router+ everyscripts/fleet/*.pyandscripts/mlf/*.pysubcommand):<tool> --tyro-write-completion zsh <path>. Native flag — auto-added when you calltyro.cli(...). tyro emits#compdef <full-binary-path>for zsh, so post-process the first line to the short name (seedot_config/shell/47_mi_router.shfor the canonical pattern). Bash output already bindsbasename, no fixup needed. - shtab:
shtab --shell=zsh my_module.parser > ~/.zfunc/_my_tool. Decorate-an-argparse.ArgumentParserstyle; works for both shells. - argcomplete:
register-python-argcomplete tool_name(eval-at-startup style, category C). Requires the script to callargcomplete.autocomplete(parser)beforeparse_args(). Cross-shell. - click (used by
marimo,thefuck,mlflow,litellm, etc.):_TOOL_COMPLETE=zsh_source tool > ~/.zfunc/_tool. The completion script may include side effects (aliases / functions) — if so, treat as category C and cache via${XDG_CACHE_HOME}/<shell>/<tool>_completion.<ext>. See the canonical pattern atdot_config/shell/29_marimo.sh.
TAB display itself, not just completion data: see
oh-my-zsh-plugins.md"TAB display upgrades — alternatives evaluated" for fzf-tab (recommended) and rejected alternatives (zsh-autocomplete / carapace-bin / inshellisense).
F. In-house CLIs (this repo)¶
| CLI | Source | Framework | Strategy | Files |
|---|---|---|---|---|
mi-router |
dot_dotfiles/bin/executable_mi-router |
tyro | A (lazy autoload) | dot_config/shell/47_mi_router.sh (gen + mtime check); refresh helper mi-router-update-completion |
reyee |
dot_dotfiles/bin/executable_reyee |
tyro | A (lazy autoload) | dot_config/shell/48_reyee.sh (gen + mtime check); refresh helper reyee-update-completion |
ping-monitor |
dot_dotfiles/bin/executable_ping-monitor |
bash hand-rolled | B | dot_config/zsh/tools/50_ping_monitor_completion.zsh + dot_config/bash/50_ping_monitor_completion.bash |
fleet |
dot_dotfiles/bin/executable_fleet + scripts/fleet/*.py |
hand-rolled umbrella + tyro/argparse subs | B (eager hand-written) | dot_config/zsh/tools/45_fleet_completion.zsh + dot_config/bash/45_fleet_completion.bash |
mlf |
dot_dotfiles/bin/executable_mlf + scripts/mlf/*.py |
hand-rolled umbrella + tyro subs | B | dot_config/zsh/tools/46_mlf_completion.zsh + dot_config/bash/46_mlf_completion.bash |
pqsum |
dot_dotfiles/bin/executable_pqsum |
argparse | B | dot_config/zsh/tools/48_pqsum_completion.zsh + dot_config/bash/48_pqsum_completion.bash |
x |
dot_dotfiles/bin/executable_x |
bash hand-rolled | B | dot_config/zsh/tools/49_x_completion.zsh + dot_config/bash/49_x_completion.bash |
dotcfg |
dot_dotfiles/bin/executable_dotcfg (thin shell → scripts/init/dotfiles_init.py reconfigure) |
bash wrapper | B | dot_config/zsh/tools/51_dotcfg_completion.zsh + dot_config/bash/51_dotcfg_completion.bash (keys mirror PROMPTS) |
agent-warmup |
dot_dotfiles/bin/executable_agent-warmup |
argparse (subcommands) | B | dot_config/zsh/tools/52_agent_warmup_completion.zsh + dot_config/bash/52_agent_warmup_completion.bash |
aiblock |
scripts/aiblock.py |
questionary (interactive TUI) | — (no CLI args) | (not needed) |
Dynamic candidates wired up:
fleethost names —_fleet_hosts_one/_fleet_hosts_csvshell out tofleet hosts --list. Used for the positionalfleet hosts NAME,fleet chezmoi diff HOST,fleet chezmoi tail HOST, and the--hosts=foo,bar,bazCSV flag everywhere.pqsum -g <GROUP>—_pqsumshells out topueue status --json | jq '.groups | keys[]'.mlfids (run-id, experiment-id, model-name) — not auto-completed (would round-trip MLflow tracking server, possibly auth-required). Usetv mlflowfor fuzzy id picking instead.
Adding a new in-house CLI (decision flow):
- Does it use tyro? → Use Strategy A. Mirror
dot_config/shell/47_mi_router.sh. Add a<tool>-update-completionhelper todot_config/shell/10_aliases.sh. Don't forget to rewrite#compdefin zsh post-gen. - Does it use argparse? → Two sub-options:
- Add
argcomplete.autocomplete(parser)to the script + Strategy C cached eval at startup. - Hand-write Strategy B completion (smaller / no Python dependency at every shell startup).
- Hand-rolled / shell script → Strategy B hand-written. One zsh file in
dot_config/zsh/tools/<NN>_<name>_completion.zsh, one bash file indot_config/bash/<NN>_<name>_completion.bash. Mirror_fleet's structure: top-level dispatcher → per-subcommand functions → dynamic helpers. - Update this table +
docs/shells/aliases.mdin the same commit (per the cross-file maintenance rule inCLAUDE.md).
Why ~/.zfunc/ is NOT Tracked by Chezmoi¶
Decision: ~/.zfunc/ is not version-controlled. Rationale:
- Completions are tool-version-dependent. A completion script generated by
uv 0.6may not matchuv 0.7. Tracking stale files causes subtle breakage. - Not all users install the same tools. A
_pueuecompletion file is useless (and throws warnings) if pueue isn't installed. - Regeneration is trivial. One command per tool.
- Brew-installed tools auto-manage their completions via site-functions. Duplicating them in
~/.zfunc/creates conflicts.
What we DO track¶
fpath+=~/.zfuncindot_zshrc.tmpl(ensures the directory is in the search path)zsh-completionsplugin cloned by ansible (community completions)~/.docker/completionsfpath entry- Tool-specific
eval/sourceintegrations indot_config/zsh/tools/
Generating Completions After Fresh Install¶
Automatic — no manual step needed. Every chezmoi apply runs .chezmoiscripts/global/run_after_50_generate_completions.sh.tmpl, which calls scripts/generate_completions.sh and regenerates completion for the 14 self-generating tools listed in Section A (chezmoi, mise, uv, just, starship, gh, docker, rg, fd, bat, delta, zellij, pueue, opencode).
The hook is idempotent — it stat-checks each tool's binary mtime against the existing completion file and skips if the cache is fresh. Cost when nothing changed: ~21ms total (presence checks + 28 stat calls). After an ansible-playbook / brew upgrade / mise install <NEW> that bumps a binary's mtime: ~140ms one-shot to regenerate the affected completions.
Output paths:
- zsh: ~/.zfunc/_<tool> — lazy-loaded by compinit on first TAB (in $fpath via dot_zshrc.tmpl:68).
- bash: ${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions/<tool> — lazy-loaded by bash-completion v2 on first TAB.
Manual rerun¶
just completions-refresh # force regen all (--force)
bash scripts/generate_completions.sh --force # same, no `just`
bash scripts/generate_completions.sh # mtime-checked (same as the chezmoi hook)
Use just completions-refresh after a tool upgrade outside the chezmoi flow (e.g. cargo install <tool>), or to recover from a corrupted completion file.
Opt out¶
Existing completion files stay as-is; you can still run just completions-refresh manually.
Add a new tool to the auto-gen inventory¶
Edit the regen <tool> "<zsh-args>" "<bash-args>" block in scripts/generate_completions.sh. The arguments are whatever follows the tool name to produce the per-shell completion script — check <tool> completion --help or <tool> --help | grep -i complet. Examples already in the script cover click (_TOOL_COMPLETE=), clap (--generate complete-<shell>), tyro (--tyro-write-completion <shell>-equivalent), cobra (completion <shell>), and ad-hoc styles (--completions <shell>).
What about tools NOT in the auto-gen inventory?¶
sesh/tv/worktrunk: shell-startup mtime check (each tool has its owndot_config/zsh/tools/<NN>_<tool>.zshfile that runs the version check on every shell start — these tools are typicallycargo install-ed outside chezmoi, so they need the per-startup probe to catch user upgrades).bw/marimo/thefuck/try-cli: cached eval at startup (their completion script has side effects beyondcompdef, so the file issource'd into the running shell rather than autoloaded).mi-router: tyro self-gen, lives indot_config/shell/47_mi_router.sh(not the bulk script — has its own#compdefrewrite for tyro's full-path output quirk).fleet/mlf/pqsum/x/dotcfg/agent-warmup: hand-written completion indot_config/{zsh/tools,bash}/45-52_*_completion.*(in-house CLIs without a built-in completion generator).dotcfg's--setkey list mirrors the PROMPTS table inscripts/init/dotfiles_init.py.
Priority / Override Order¶
When the same _tool file exists in multiple fpath directories, the first match wins:
1. ~/.zfunc/ (highest - your overrides)
2. ~/.oh-my-zsh/custom/plugins/zsh-completions/src/
3. ~/.docker/completions/
4. $(brew --prefix)/share/zsh/site-functions/ (Homebrew-managed)
5. /usr/share/zsh/*/functions/ (system, lowest)
Current dot_zshrc.tmpl order:
fpath+=~/.zfunc # appended (lowest of custom)
fpath=(zsh-completions/src $fpath) # prepended (higher than ~/.zfunc)
fpath=(~/.docker/completions $fpath) # prepended (highest of custom)
If you want ~/.zfunc/ to override community completions, change to prepend:
FAQ¶
Q: Should I generate completions for tools that zsh-completions already covers?
A: Only if you want the latest version. The community plugin may lag behind. For fast-moving tools (uv, mise), generating your own is worthwhile.
Q: A completion file throws errors after I uninstall a tool. What do I do?
A: Remove the file: rm ~/.zfunc/_toolname. Stale _tool files in fpath are harmless (silently ignored) unless they have syntax errors.
Q: How do I debug completion issues?
A: Run echo $fpath | tr ' ' '\n' to see all fpath entries. Use which _toolname to see which completion file is being used. Force rebuild with rm -f ~/.zcompdump && compinit.