Writing Zsh Tab Completions with compsys: A Practical Guide
Learn how to create custom Zsh completions using the compsys framework, from a simple function to styling and caching, with a concrete example you can try today.
23 Feb 2026, 05:03 UTC

Why you might want a custom completion
When you repeatedly type a command with many options—such as a personal script that processes files, runs tests, or talks to an API—you often find yourself checking the help page just to remember the exact flag names. Zsh’s programmable completion system (compsys) lets you teach the shell to suggest those flags automatically when you press Tab. This article walks through creating a basic completion, enhancing it with styles, and discusses the trade‑offs you’ll encounter.
1. Core concepts: completion functions and compdef
A completion is just a shell function that prints possible matches to stdout. The compdef builtin associates that function with a command name. Inside the function you typically use _arguments to describe the command’s interface: each option, its description, and what kind of argument it expects.
For example, a tiny completion that offers only --help and --version looks like this:
_mytool() {
# Describe the options we support
_arguments \\
'--help[Show help message]' \\
'--version[Print version information]'
}
# Tell zsh to use _mytool for the command "mytool"
compdef _mytool mytool
Place the function definition in your ~/.zshrc (or a file you source from it) and then reload the shell with source ~/.zshrc. After that, typing mytool followed by Tab should display the two options.
2. A more realistic example: a file‑processing script
Suppose you have a script mytool that accepts:
--input <file>– path to an input file--output <file>– path for the result--verbose– toggle extra logging--format <json|yaml>– output format
The completion function can express all of these:
_mytool() {
local -a format_opts
format_opts=(json yaml)
_arguments \\
'--input[Path to input file]:_files' \\
'--output[Path to output file]:_files' \\
'--verbose[Enable verbose logging]' \\
'--format[Choose output format]:->format'
# Handle the dynamic --format argument
case $state in
format)
_describe -t formats 'output format' format_opts
;;
esac
}
compdef _mytool mytool
Here we use the helper _files to complete file paths for --input and --output. The --format option uses a dynamic case block to offer the two static choices.
After reloading your zshrc, try:
mytool --i<Tab>
You should see --input appear, and after selecting it you can press Tab again to browse the filesystem for a file.
3. Tuning behavior with zstyle
Completion styles let you adjust appearance and interaction without rewriting the function. Commonly useful styles include:
menu select– makes the completion list navigable with arrow keys.group-name– adds headings to separate options from file arguments.cache-policy– controls when cached results are considered stale.
Add these lines to your zshrc (after the compdef line):
# Use a selectable menu for mytool completions
zstyle ':completion:*:mytool:*' menu select
# Group options separately from filenames
zstyle ':completion:*:mytool:*' group-name ''
# Enable caching; invalidate after 5 seconds of inactivity
zstyle ':completion:*:mytool:*' cache-path ~/.zsh/cache/mytool
zstyle ':completion:*:mytool:*' cache-policy _mytool_cache_policy
_mytool_cache_policy() {
# Return true (0) if the cache is still fresh
[[ -f $1 ]] && [[ $(($(date +%s) - $(stat -c %Y $1))) -lt 5 ]] && return 0
return 1
}
With menu select active, pressing Tab once shows the list; a second Tab lets you move the highlight with ← → ↑ ↓ and accept with Enter. The cache reduces latency when the completion function does expensive work (e.g., querying a remote API).
4. Trade‑offs and maintenance considerations
Writing completions gives users a smoother experience, but there are costs:
- Verbosity: Even a modest interface can require dozens of lines to describe every option, its description, and any dynamic behavior.
- Version differences: Features like
_arguments’s->notation or certainzstylenames appeared in zsh 5.0. Older releases may need fallback completions or conditional checks (if (( $+functions[_arguments] )); then …). - Cache invalidation: If you cache results based on files that change infrequently, you must ensure the cache is cleared when the underlying data updates; otherwise completions become stale.
To verify that caching is working, you can time a completion before and after touching the source file:
# First run (no cache)
time (mytool --<Tab>)
# Second run (should be faster if cache is used)
time (mytool --<Tab>)
If the second invocation feels noticeably quicker, the cache is active. Remember that the cache lives only for the current shell session unless you persist it to disk.
Actionable next steps
Start small: pick one command you use daily, write a completion that covers its most common flags, and source it in your zshrc. Test the basic tab behavior, then add a menu select style if the list grows beyond a few items. As you become comfortable, explore the extensive zshcompsys man page for advanced helpers like _values, _alternative, and _arguments’s * pattern for ambiguous options.
By investing a few minutes in a completion function, you turn repetitive typing into a quick, discoverable interaction—making your shell workflow feel more responsive and less error‑prone.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.