Zsh custom completions are a surprisingly powerful way to supercharge your command-line productivity, turning mundane commands into interactive explorers.
Let’s see it in action. Imagine you’re writing a script that manages projects, and you want a completion for myproj list that shows only projects starting with "feat-".
First, create a completion definition file. Zsh looks for these in directories listed in your $fpath. A common place is ~/.zsh/completion/. So, create that directory if it doesn’t exist:
mkdir -p ~/.zsh/completion
Now, create a file named _myproj inside this directory:
# ~/.zsh/completion/_myproj
#compdef myproj
_myproj() {
local context="$1"
local curcontext="$curcontext"
case $context in
myproj)
_arguments \
'1: :->command' \
'*:: :->args'
;;
myproj\ list)
_arguments \
'1: :->project'
;;
esac
case $state in
command)
local commands=("list" "add" "remove")
_describe 'command' commands
;;
project)
# This is where the magic happens for our specific case
local projects=$(ls | grep '^feat-' | sed 's/^feat-//')
_describe 'project' projects
;;
args)
# Fallback for other commands or arguments
_arguments
;;
esac
}
After saving this file, you need to tell Zsh to load it. Add this to your ~/.zshrc:
fpath=(~/.zsh/completion $fpath)
autoload -U compinit && compinit
Now, open a new Zsh session or run compinit in your current one. Try typing myproj list (with a space after list) and press Tab. You should see a list of directories in your current directory that start with feat-.
The mental model for Zsh completions revolves around a hierarchical state machine. When you type a command and press Tab, Zsh invokes the corresponding completion function (e.g., _myproj for myproj). This function is responsible for figuring out what the user might want to type next.
It does this by inspecting the current context ($curcontext) and the arguments already provided. The core of a completion function is often a case statement that branches based on the current state or the command being completed.
Inside these branches, you use helper functions like _arguments and _describe.
_argumentsis used to define the general structure of arguments for a command. It takes pairs ofspec:message:state. Thespecdefines the argument pattern (e.g.,1:for the first positional argument,*: :->state_namefor any number of arguments transitioning to a named state)._describeis used to present a list of possible completions to the user. It takes a descriptive name (e.g., 'command', 'project') and an array of possible completions.
The case $state in ... esac block is where you define what happens when a specific state (like project in our example) is entered. This is where you generate the actual list of options.
In our _myproj example, when the project state is active (meaning the user typed myproj list ), we execute ls | grep '^feat-' | sed 's/^feat-//'. This command lists all files/directories in the current directory, filters them to only those starting with feat-, and then removes the feat- prefix to give us clean project names. These names are then passed to _describe to be presented as completions.
The real power comes from the fact that you can generate these completion options dynamically. You’re not limited to static lists. You can query APIs, parse output from other commands, or even read from configuration files. The _arguments function also handles more complex scenarios like optional arguments, flag parsing, and recursive completion definitions.
One thing that often trips people up is how Zsh handles multiple completion functions for the same command. If you have a system-wide completion for git and a custom one in ~/.zsh/completion/_git, Zsh will load them both. The order in your $fpath matters, and often, more specific completions (like those you define) should come earlier in the path to override system defaults if necessary. You can explicitly control which completion function is used with compdef myproj _myproj.
The next hurdle you’ll likely encounter is handling completions for commands that have a very large number of options, or options that depend on previous selections, requiring more sophisticated state management within your completion functions.