Wiki

Read the other shells in the completions directory

When I land on a bug in a CLI tool's tab-completion script, the first move is not to open the file the bug names. It's to list the directory. Most CLI tools ship completions for bash, fish, zsh, and PowerShell in the same folder, and the four files diverge over time. The right shape almost always lives in one of the siblings already.

When to reach for it

Any bug report or fix candidate that names a single shell's completion file. Reports usually pin the shape: "zsh tab completion offers values that crash with unknown syntax", "fish completion misses spaces in language names", "PowerShell completer hangs on large directories". The natural read is to open the named file and start tracing.

The right read is to list the directory first. Then read the shell file the bug names AND its siblings, side by side.

How to run it

ls assets/completions/   # or contrib/, or completions/

Look for filenames like <bin>.bash.in, <bin>.fish.in, <bin>.zsh.in, <bin>.ps1.in. The .in suffix is common when the completion is a build-time template; non-.in versions exist too. Each one is usually under 200 lines and self-contained, so reading three of them takes minutes, not hours.

For the sub-section that the bug names (language-list, file-list, flag-table, theme-list), grep the same emit shape across all shells:

grep -A3 'list-languages\|--list-themes\|completer' \
  assets/completions/*.in

The output puts the four shells' implementations adjacent in your terminal, which is what makes the comparison cheap.

The three diagnostic outcomes

This is a flavor of sibling-implementation-check specialized to completion files. The outcome dictates the framing.

1. Sibling already correct

One shell has the right shape; the broken shell has the wrong shape. The fix is alignment, not invention. The PR body cites the correct sibling and explains why the broken shell drifted.

2. Both broken, same way

The bug exists in multiple shells because the original author copied the broken pattern across files, or because all shells are emitting the same upstream tool's malformed output. The fix is a sweep across all affected files, and the PR body lists each fixed file with a one-line explanation per file.

3. Sibling fixed in a recent PR, the named shell wasn't

A previous PR fixed one shell's completion but didn't propagate the fix to the others. The completion files for one tool age unevenly because reviewers ship the change for the shell the reporter uses and forget the others. The framing is "completing a known sweep": cite the prior PR's number and the date.

Real application

2026-05-10, sharkdp/bat#3735 ("Invalid values listed in --list-languages that fail with -l"). The bug report named the zsh completion. Tab-completing bat -l <TAB> offered candidates like *.bash_login, /etc/gitconfig, and Containerfile, none of which parse as -l arguments.

The natural read was bat.zsh.in line 93, which had:

languages=( ${(f)"$(... --list-languages | awk -F':|,' \
  '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }')"} )

bat --list-languages prints <name>:<file-matchers>, where the matcher column mixes plain extensions, globs, paths, and filenames. The awk script split each line on : or , and emitted EVERY field as a candidate, including all the non-language values from column two.

But before opening that file, I listed assets/completions/ and found three siblings: bat.bash.in, bat.fish.in, bat.zsh.in. Reading bash first surfaced the right shape at lines 80-89:

while IFS=: read -r lang _; do
  printf "%s\n" "$lang"
done < <(... --list-languages)

The bash completion had been correct from the start: split on :, keep only column one. The fish completion at line 26 had a partial filter (string match -rq '[/*]' $ext; and continue) that stripped values containing / or * but still emitted hidden filenames. Two siblings, two different states. The bash shape was the correct one.

The fix shape became "bring zsh into alignment with bash":

languages=( ${(f)"$(... --list-languages | awk -F: \
  '{ printf("%s:%s\n", $1, $2) }')"} )

One character changed in the awk field separator. Column one became the value, column two became the description. The PR body cited the bash completion's already-correct behavior at bat.bash.in:80-89 so the reviewer could see the design choice without me arguing for it.

The fish completion had a partially-mitigated form of the same bug; flagged in the PR body as out of scope for the zsh fix.

PR: https://github.com/sharkdp/bat/pull/3737

Why this works

Three reasons.

One. Completion files are small and self-similar. Each one implements the same logical surface (flag list, language list, theme list) for a different shell. The siblings are usually the cheapest cross-reference available.

Two. Completion files age unevenly. Reviewers fix the shell the reporter uses. The other shells inherit the bug and a maintenance lag accumulates. By the time a fresh issue lands, one of the four files is often already correct.

Three. The PR body writes itself. "Bring zsh into alignment with bash, which already had the right shape" is a one-sentence framing the reviewer can validate by reading two adjacent files. No design argument. No taste call.

What this doesn't replace

When not to use it

When the bug is shell-specific by design. PowerShell has parameter-set completion semantics that bash doesn't. Fish has completion descriptions that zsh doesn't display the same way. If the bug names a shell-specific feature, the sibling read is still useful as background but the fix won't be a copy-shape.

When the project ships only one shell's completion. Single-shell completion files don't have siblings. The pattern doesn't apply.

Related

Revisit

Add a second real application the next time this fires. If a counter-example shows up where the sibling shape is wrong and the named-shell shape is right, record the threshold here.