Discover how to implement rich tab-completions that work seamlessly across both Bash and Zsh, complete with command descriptions—a feature previously thought impossible in Bash. Li Haoyi's ingenious approach transforms CLI usability by leveraging shell quirks to deliver professional-grade completions.
For CLI tool developers, few experiences are more frustrating than writing separate tab-completion scripts for Bash and Zsh users. The fragmentation between these dominant shells creates maintenance nightmares and limits user experience—especially for descriptive completions that help users understand commands. Li Haoyi’s breakthrough approach solves this elegantly, enabling rich, cross-shell completions with minimal code.
{{IMAGE:1}} Descriptive tab-completions in action (Source: Mill Build Tool)
Why Cross-Shell Completions Matter
Half your users run Bash (typically on Linux), while the other half use Zsh (especially on macOS). Zsh natively supports command descriptions during tab-completion, but Bash lacks this capability. Traditional solutions force developers to maintain two separate completion systems—until now. Haoyi’s method unifies the workflow using a single generator function and shell-specific wrappers.
The Foundation: Basic Completion
At its core, tab-completion involves a handler function that suggests completions based on partially typed words. Here’s the unified generator:
_generate_foo_completions() {
local idx=$1; shift
local words=( "$@" )
local current_word=${words[idx]}
# Example completion candidates with descriptions
local array=(
"run: Execute a task"
"build: Compile artifacts"
"test: Run test suite"
)
for elem in "${array[@]}"; do
if [[ $elem == "$current_word"* ]]; then echo "$elem"; fi
done
}
Shell-specific handlers then process this output. For Bash:
_complete_foo_bash() {
local raw=($(_generate_foo_completions "$COMP_CWORD" "${COMP_WORDS[@]}"))
COMPREPLY=( "${raw[@]}" )
}
complete -F _complete_foo_bash foo
For Zsh:
_complete_foo_zsh() {
local -a raw
raw=($(_generate_foo_completions "$CURRENT" "${words[@]}"))
compadd -- $raw
}
compdef _complete_foo_zsh foo
The Bash Description Hack
Bash doesn’t natively display completion descriptions, but Haoyi exploits a clever loophole: When multiple completions exist, Bash shows full suggestions without inserting descriptions into the command line. By tactically omitting the colon-prefixed descriptions only when a single match exists, we achieve descriptive previews:
_complete_foo_bash() {
local IFS=$'\n'
local raw=($(_generate_foo_completions "$COMP_CWORD" "${COMP_WORDS[@]}"))
local trimmed=()
if (( ${#raw[@]} == 1 )); then
# Single match: Trim description for insertion
trimmed=( "${raw[0]%%:*}" )
else
# Multiple matches: Show descriptions
trimmed=( "${raw[@]}" )
fi
COMPREPLY=( "${trimmed[@]}" )
}
Now Bash users see:
$ foo <TAB>
build: Compile artifacts run: Execute a task test: Run test suite
Single-Completion Discovery
What if users want documentation for a fully typed command? Normally, tabbing on foo build does nothing. Haoyi’s solution: Inject a phantom duplicate completion to force description display:
# In Bash handler
if (( ${#raw[@]} == 1 )); then
trimmed+=( "${raw[0]%%:*}" ) # Add descriptionless version
fi
{{IMAGE:3}} Single-command description trigger via (Source: Mill Build Tool)
Zsh requires parallel array manipulation:
# In Zsh handler
if (( ${#raw} == 1 )); then
trimmed+=( "${raw[1]}" ) # Add bare command
raw+=( "${trimmed[1]}" ) # Add descriptive version
fi
Now tabbing on completed commands shows documentation:
$ foo build<TAB>
build build: Compile artifacts
Unified Solution
Combining these techniques yields robust cross-shell completions:
# Unified generator and handlers (simplified)
# ... [Full code from Haoyi's final example] ...
Key achievements:
- Descriptions in both shells using Bash’s display quirks
- Discoverability for completed commands via phantom entries
- Single codebase for all completion logic
Why This Matters
Tab-completion isn’t just about saving keystrokes—it’s fundamental to CLI discoverability. Haoyi’s approach democratizes rich completions for all shell users while eliminating dual-maintenance burdens. For tools like Mill, Maven, or custom CLIs, this technique elevates user experience from functional to exceptional.
"These 50 lines of shell script replaced 300 lines of fragmented completion code in Mill. Now our users explore flags like they browse GUI menus." — Li Haoyi
Comments
Please log in or register to join the discussion