skip to content

You ship a command-line tool called `deploytool` and want `deploytool <TAB>` to complete its subcommands in bash. Which bash builtins and shell variables implement that, and where does the completion script get installed?

level: middleimportance: nice to knowfreq 30%

answer

  1. one builtin registers, another generates
  2. the function fills an array
  3. index of the word being completed
  4. filter by the current prefix
  5. named after the command, loaded lazily

basics

~20 s

Bash's complete builtin registers how a command completes. A fixed word list is complete -W "start stop status" deploytool; anything context-sensitive uses complete -F _deploytool deploytool, where the function fills the COMPREPLY array, usually from compgen. Install it in the bash-completion completions directory.

solid answer

~50 s

Programmable completion is the `complete` builtin. The simplest form attaches a fixed word list: `complete -W "start stop status" deploytool`. For anything that depends on position or on earlier words you register a function instead — `complete -F _deploytool deploytool`. When the user presses Tab, bash calls that function with `COMP_WORDS` (the command line split into words) and `COMP_CWORD` (the index of the word being completed) set, and the function must assign the candidate list to the `COMPREPLY` array. `compgen` is the generator you build it from: `compgen -W "start stop status" -- "$cur"` filters a word list by the current prefix, and `compgen -f`/`-d`/`-c` generate filenames, directories and command names. `compopt -o nospace` adjusts behaviour for the completion in progress. For distribution, install the script as a file named after the command in the bash-completion `completions` directory, where it is loaded lazily on first Tab.

code

bash · 15 lines
bash
_deploytool() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local prev=${COMP_WORDS[COMP_CWORD-1]}

    if [ "$COMP_CWORD" -eq 1 ]; then
        COMPREPLY=( $(compgen -W "start stop status restart" -- "$cur") )
        return
    fi

    case $prev in
        --env) COMPREPLY=( $(compgen -W "dev staging prod" -- "$cur") ) ;;
        *)     COMPREPLY=( $(compgen -W "--env --dry-run" -- "$cur") ) ;;
    esac
}
complete -F _deploytool deploytool

go deeper

for a junior

Know that Tab completion for a command is configurable and that complete -W "a b c" mytool gives a working word list in one line. Recognise this as bash-side configuration, not something the tool does by itself.

for a middle

Describe the function protocol precisely: complete -F, the COMP_WORDS and COMP_CWORD inputs, filling COMPREPLY, and using compgen -W with the current prefix to filter. Name where the script is installed.

for a senior

Weigh generating completions from the tool against hand-maintaining them, keep the function fast enough that Tab never blocks, and know the packaging difference between the lazy completions directory and the eagerly sourced legacy one.

for a principal

Treat completion as part of the product's usability contract: decide whether the CLI generates its own completion scripts, how they ship for each supported shell, and who owns the drift when flags change.

## The pieces Bash's completion system has three parts: the `complete` builtin that registers a *completion spec* for a command name, the `compgen` builtin that generates candidate words, and a set of `COMP_*` variables that carry the context into your function. ## Simple cases without a function ```bash complete -W "start stop status restart" deploytool # fixed word list complete -d cd_to_project # directories only complete -c which # command names complete -F _deploytool deploytool # call a function ``` `complete -p deploytool` prints the spec currently registered, and `complete -r deploytool` removes it. Useful `-o` modifiers include `-o default` (fall back to bash's normal filename completion if the spec produces nothing), `-o nospace` (do not append a trailing space, for completing a path prefix) and `-o filenames` (treat results as filenames so bash escapes them and marks directories). ## The function protocol When bash calls a `-F` function it sets: - `COMP_WORDS` — an array of the words on the current command line. - `COMP_CWORD` — the index into that array of the word the cursor is in. - `COMP_LINE` and `COMP_POINT` — the raw line and cursor offset, for the rare case you need to re-parse yourself. The function's return value carries no candidates; its job is to assign them into the `COMPREPLY` array. Bash takes it from there — it inserts the single candidate, or lists them on a second Tab. ```bash _deploytool() { local cur prev cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=( $(compgen -W "start stop status restart" -- "$cur") ) return fi case $prev in start|restart) COMPREPLY=( $(compgen -W "--env --dry-run" -- "$cur") ) ;; --env) COMPREPLY=( $(compgen -W "dev staging prod" -- "$cur") ) ;; *) COMPREPLY=( $(compgen -f -- "$cur") ) ;; esac } complete -F _deploytool deploytool ``` The `-- "$cur"` argument to `compgen` is what filters the list down to what the user has already typed; forgetting it is the most common beginner bug, because bash then offers every candidate regardless of prefix. ## compgen as a generator `compgen` shares `complete`'s generator options but writes matches to standard output instead of registering anything, so it is also useful on its own — `compgen -c ssh` lists commands starting with `ssh`, and `compgen -W "$(deploytool list-envs)" -- "$cur"` builds candidates from the tool's own output. Calling out to the tool on every Tab is convenient but costs a process per completion, so cache anything slow. `compopt` changes options for the completion in progress, for example `compopt -o nospace` when you are completing a `key=` prefix the user will keep typing. ## Where the script lives Completion specs are per-shell state, so they must be registered in every session — which is what the `bash-completion` package automates. Two locations matter: - `/usr/share/bash-completion/completions/` — install a file **named exactly after the command** (`deploytool`). bash-completion registers a lazy loader, so the file is sourced only the first time someone completes that command. This is the modern, fast path and where a distribution package should put the file. - `/etc/bash_completion.d/` — the legacy directory, whose contents are sourced eagerly at shell startup. It still works, but every file there is a tax on every shell start. For a single user, sourcing the file from `~/.bashrc` is fine. Note that none of this happens unless bash-completion itself is loaded; a minimal container or a stripped `~/.bashrc` simply gets bash's default filename completion. ## Generated completions Many CLI frameworks emit a completion script for you — the usual pattern is a subcommand that prints one to standard output, which the user then evaluates or redirects into the completions directory. Generating beats hand-maintaining, because a hand-written script drifts out of date the moment a flag is added. ## What can go wrong - Unquoted expansion in `COMPREPLY=( $(compgen ...) )` splits candidates containing spaces; use `mapfile -t COMPREPLY < <(compgen ...)` when candidates may contain whitespace. - A completion function that prints to standard output corrupts the display — keep it silent. - A slow function makes Tab feel broken, because the user is blocked while it runs.

  • What is the difference between `complete` and `compgen`?
    `complete` registers a completion spec against a command name — it says *how* that command completes. `compgen` takes the same generator options but simply writes matching words to standard output, so it is the engine you call from inside a completion function, or from the prompt to see what a rule would produce.
  • Why is a file in /usr/share/bash-completion/completions cheaper than one in /etc/bash_completion.d?
    Files in the completions directory are registered with a lazy loader keyed on the command name, so nothing is read until someone actually presses Tab for that command. Everything in the legacy `/etc/bash_completion.d` directory is sourced eagerly at shell startup, so each file there adds latency to every new shell.
  • Your completion function offers every subcommand no matter what the user has typed. What did you forget?
    Almost certainly the `-- "$cur"` argument on `compgen`, which filters the generated word list against the prefix already typed. Without it `compgen -W` prints the whole list and bash shows all of it. Take `cur` from `${COMP_WORDS[COMP_CWORD]}` and pass it through.

saying these in an interview costs you the question

  • Says the completion function should echo candidates instead of setting COMPREPLY
  • Thinks a spec registered in one shell applies to other open shells
  • Confuses compgen with complete
  • Omits the current prefix so every candidate is always offered
  • Believes bash completes subcommands automatically once a tool has --help

context