#Dev

Crafting Cross-Shell Tab Completions: Bridging the Bash-Zsh Divide

LavX Team
3 min read

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:

  1. Descriptions in both shells using Bash’s display quirks
  2. Discoverability for completed commands via phantom entries
  3. 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

Source: Mill Build Tool Blog

Comments

Loading comments...