Writing Your Own Simple Tab-Completions for Bash and Zsh :: The Mill Build Tool
The last quality of life feature we will add is the ability to show completion
descriptions when tabbing on a complete word:
For example, the Mill build tool does this so if you’re not sure what a flag or command
does, you can press <TAB>
on it to see more details:

Tab-completion is a common way to explore unfamiliar APIs, and just because someone
finished writing a flat or command doesn’t mean they aren’t curious about what
it does! But while Zsh tab-completion displays descriptions when multiple
options match the prefix, and we managed to hack Bash tab-completion to do the same
thing, neither displays any information if the word you are tab-completing is already
complete.
This behavior can be annoying, if the user wants to see the description, they will
need to first:
-
Delete enough characters to make the token match multiple completions
-
Press
<TAB>
-
Visually scan the multiple completions printed to find the word description
they care about -
Type back in all the missing characters so they can run the command
To solve this, we can hack Bash and Zsh to print tab-completion descriptions even
if the token is already a complete word. We do this by checking if the token
is a complete word, and if so adding a second “dummy” completion: this makes
the tab-completion ambiguous, which cases Bash and Zsh to print out the completions
and descriptions for the user to see.
Doing this in _complete_foo_bash
looks like the following:
_complete_foo_bash() {
local IFS=$'\n'
local raw=($(_generate_foo_completions "$COMP_CWORD" "${COMP_WORDS[@]}"))
local trimmed=()
trimmed+=( "${raw[@]}" )
if (( ${#raw[@]} == 1 )); then
trimmed+=( "${raw[0]%%:*}" )
fi
COMPREPLY=( "${trimmed[@]}" )
}
Instead of checking the length of raw
to decide whether we add a trimmed
and non-trimmed lines to trimmed
, we now instead always add the non-trimmed lines
that contain the completion descriptions, and in the case where there’s only
one line we then add an additional word-only completion with the description
trimmed off.
This means that all completions are ambiguous and will print the description –
even completions with a single real choice – but the additional trimmed line
when there is only 1 real choice ensures that the description text never gets
inserted into the user’s command
In Zsh, this can be similarly done via:
_complete_foo_zsh() {
local -a raw trimmed
local IFS=$'\n'
raw=($(_generate_foo_completions "$CURRENT" "${words[@]}"))
for d in $raw; do trimmed+=( "${d%%:*}" ); done
if (( ${#raw} == 1 )); then
trimmed+=( "${raw[1]}" )
raw+=( "${trimmed[1]}" )
fi
compadd -d raw -- $trimmed
}
The change here is similar to the Bash snippet above: when the number of completions is 1,
we add an additional completion to make it ambiguous so Zsh prints the description. But
because Zsh expects to pass two parallel arrays of descriptions and tokens to compadd
,
our if
block needs to append items to both trimmed
and raw
.
Using this, it now looks like
$ foo apple<TAB>
apple apple: a common fruit
Although the UI is not quite perfect – the word apple
gets duplicated twice –
this nevertheless achieves the original goal of letting users <TAB>
on an
already-completed flag or command to see the description or documentation for that word.