Input & UX

Installing Shell Completion for bash, zsh, fish

Generate and install shell completion scripts for a Python CLI on bash, zsh, and fish, load them per-user or system-wide, and fix completions that never fire.

Updated

You have completion working in Python — subcommands and options complete when you drive the protocol by hand. This guide is the other half: getting that completion to fire automatically in a real shell. Each shell loads completion scripts from its own place and in its own way, so "install completion" means three different recipes for bash, zsh, and fish. This walks through generating the script and placing it for each shell, choosing between eager and sourced loading, and fixing the classic failure where Tab does nothing at all. Enabling completion in your code is the sibling guide; here we assume that already works and focus on the shell.

TL;DR

  • Generate the script. Click: _YOURCLI_COMPLETE=bash_source yourcli (swap bashzsh/fish). Typer: yourcli --show-completion, or let yourcli --install-completion do it for you.
  • bash: source the script from ~/.bashrc, or drop it in a bash-completion directory. Requires the bash-completion package.
  • zsh: the script must be on fpath and compinit must run — set both in ~/.zshrc.
  • fish: put a file named yourcli.fish in ~/.config/fish/completions/; fish autoloads it, no sourcing needed.
  • Nothing happens? Start a fresh shell, check the command is on PATH, and in zsh run rehash. Ninety percent of "completion is broken" is one of these.

Generate the completion script

Every install starts by generating the shell script your CLI already knows how to emit. With Click you set the completion trigger to the *_source variant for the target shell and capture stdout:

$ _YOURCLI_COMPLETE=bash_source yourcli   # prints the bash completion script
$ _YOURCLI_COMPLETE=zsh_source yourcli    # zsh version
$ _YOURCLI_COMPLETE=fish_source yourcli   # fish version

The variable name is derived from your console-script name: uppercased, hyphens to underscores, _COMPLETE appended (my-tool_MY_TOOL_COMPLETE). With Typer you do not need to remember the variable — the app prints the script for you:

$ yourcli --show-completion    # prints the script for your current shell
$ yourcli --install-completion # generates AND installs it in the right place

--install-completion is the fast path for end users; the manual placement below is what you reach for when you want the script checked into a package, deployed system-wide, or loaded a particular way.

Confirm which shell you are actually in before you generate — the login shell in your terminal is not always what you assume. echo "$0" or ps -p $$ -o comm= prints the running shell, and you want the script for that shell, not for whatever chsh says your default is. Generating a zsh script and sourcing it from a bash .bashrc is a surprisingly common way to get silent, do-nothing completion.

bash

bash completion relies on the bash-completion package (preinstalled on most Linux distributions; brew install bash-completion@2 on macOS). There are two good placements.

Sourced from .bashrc (simplest, per-user). Generate the script once into a file and source it on shell start. Generating to a file rather than eval-ing on every startup keeps your shell fast — you are not launching Python each time a shell opens.

$ _YOURCLI_COMPLETE=bash_source yourcli > ~/.yourcli-complete.bash
# ~/.bashrc
source ~/.yourcli-complete.bash

Dropped in a completion directory (per-user or system). bash-completion autoloads files from its user directory, so no source line is needed:

$ mkdir -p ~/.local/share/bash-completion/completions
$ _YOURCLI_COMPLETE=bash_source yourcli > ~/.local/share/bash-completion/completions/yourcli

For a system-wide install (a package's postinstall step, say), write to /usr/share/bash-completion/completions/yourcli instead. Either way, open a new shell to pick it up.

zsh

zsh needs two things to be true: your completion script must be on fpath, and the completion system (compinit) must have been initialised. Miss either and Tab stays silent. The robust per-user setup:

$ mkdir -p ~/.zfunc
$ _YOURCLI_COMPLETE=zsh_source yourcli > ~/.zfunc/_yourcli
# ~/.zshrc  — order matters: fpath BEFORE compinit
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit

The leading underscore in the filename (_yourcli) is a zsh convention for completion functions — keep it. If you use a framework like Oh My Zsh that already calls compinit, just make sure your fpath= line runs before it; putting the fpath addition near the top of .zshrc is the safe choice. After editing, start a new shell or run exec zsh.

fish

fish is the easiest of the three: it autoloads any file in its completions directory, matched by command name. No sourcing, no init call.

$ mkdir -p ~/.config/fish/completions
$ _YOURCLI_COMPLETE=fish_source yourcli > ~/.config/fish/completions/yourcli.fish

The filename must match the command (yourcli.fish). fish picks it up in new shells automatically, and often in the current one too because it loads completions lazily on first use. For a system-wide install, write to /usr/share/fish/vendor_completions.d/yourcli.fish.

Eager (eval) vs sourced-from-file

You will see two styles in the wild and it is worth knowing the trade-off.

  • Eager / eval on every startupeval "$(_YOURCLI_COMPLETE=bash_source yourcli)" directly in .bashrc. Always current, because it regenerates the script each time. The cost: it launches your Python program on every new shell, which adds startup latency you will feel if the tool is heavy. This is where a fast CLI pays off — see CLI startup performance and lazy loading.
  • Sourced from a generated file — generate once to a file, then source or autoload that file. Zero Python at shell start, so it is the better default. The catch: the script is a snapshot. If you add subcommands or rename options, the shape of completion can go stale until you regenerate. (Dynamic value callbacks still run your program at Tab-time, so those stay fresh regardless.)

For most users, generate to a file. Reserve eval for a machine where you are actively developing the CLI and want the completion structure to track your code without regenerating.

Troubleshooting: completion never fires

When Tab does nothing, work down this list — the fixes are almost always mundane.

  1. You did not start a fresh shell. Completion registration happens at shell startup. After installing, open a new terminal or run exec bash / exec zsh / start a new fish. This is the single most common cause.
  2. The command is not on PATH. The generated script invokes yourcli to compute candidates. If yourcli is not on the PATH of the shell where you press Tab, nothing comes back. Confirm with command -v yourcli. Tools installed with pipx or uv tool put a launcher on PATH for you — see installing and distributing CLIs with pipx.
  3. zsh needs a rehash. zsh caches the commands on PATH. Right after installing a new CLI, zsh may not "see" it until you run rehash (or open a new shell). If the completion function itself is not found, re-check that fpath is set before compinit.
  4. Stale zsh completion cache. zsh caches completion metadata in ~/.zcompdump. After changing a completion script, remove it and reinitialise: rm -f ~/.zcompdump && compinit.
  5. bash-completion is not installed. The generated bash script depends on the bash-completion runtime. On a minimal system it may be missing; install the bash-completion package.
  6. It works in one shell but not another. Each shell loads a different script from a different place, so a working bash setup tells you nothing about zsh. Install completion once per shell you actually use, and test each in its own fresh session.
  7. The Python side is actually broken. Rule this out fast by driving the protocol directly, with no shell in the loop:
$ _YOURCLI_COMPLETE=bash_complete COMP_WORDS="yourcli " COMP_CWORD=1 yourcli
plain,deploy
plain,status

If that prints candidates, your program is fine and the problem is placement or loading. If it prints nothing, fix completion in your code first — that is the enabling guide.

Production notes

  • Ship the script, don't make users generate it. When you package the CLI, include a generated completion file per shell and document one-line install steps, or lean on Typer's --install-completion. Regenerate the files as part of your release so their structure tracks the code.
  • The script name must match the console-script name. Both the completion trigger variable and the filename derive from the installed command. Renaming the entry point invalidates a shipped script; keep them in step with your entry points.
  • System vs user directories. System paths (/usr/share/...) suit OS packages; user paths (~/.local/share/..., ~/.zfunc, ~/.config/fish/completions) suit pip/pipx installs where you cannot write to system locations.
  • Test in a clean shell. Completion bugs hide behind an already-configured environment. Verify in env -i bash --noprofile --norc (then source only your script) or a fresh container so you catch missing dependencies your own dotfiles paper over.
  • Pin versions. The generated script format is tied to the framework — pin click>=8.1 / typer>=0.12 and regenerate scripts when you upgrade.