When Tab returns silence
You are building a CLI tool. You type my-cli -- and hit Tab. The shell stares back at you. You type my-cli --in and hit Tab. Still nothing. You know the flag exists. You just forgot the exact spelling. Your users feel the same way. Every time they hit Tab and get silence, they lose momentum. They have to interrupt their flow, check the help text, and retype. Shell completions fix this. They turn your CLI from a memorization test into a guided conversation.
How completion works under the hood
Shell completion works like a handshake between your program and the shell. When the user hits Tab, the shell pauses. It runs your program with a special hidden flag, usually __complete. It passes the current command line and the cursor position as arguments. Your program parses that input, figures out what token is being completed, and prints a list of valid options. The shell reads that list and displays it to the user. If the user picks an option, the shell fills it in and waits for the next input.
You do not write the UI logic. You define your command structure, and a generator translates that structure into a shell-specific script. The script handles the hidden flag, parses the arguments, and formats the output for Bash, Zsh, Fish, PowerShell, or Elvish. You provide the rules. The generator writes the glue.
The clap_complete crate provides this generator. It reads a clap::Command definition and emits the script. The crate is the companion to clap. If you are using clap for argument parsing, clap_complete is the standard way to add completions.
Minimal example
Add clap and clap_complete to your dependencies. The clap crate handles the command definition. clap_complete handles the generation.
// Cargo.toml
// clap handles argument parsing. clap_complete generates the scripts.
[dependencies]
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
Define your command and generate the script. The generate function takes the shell type, the command definition, the binary name, and a writer. It walks the command tree and emits shell syntax.
use clap::{Command, Arg, value_parser};
use clap_complete::{generate, shells::Bash};
use std::io;
fn build_cli() -> Command {
Command::new("my-cli")
.version("0.1.0")
.arg(
Arg::new("input")
.short('i')
.long("input")
.value_parser(value_parser!(String))
.help("Sets the input file")
)
}
fn main() {
let mut cmd = build_cli();
// Generate the bash script and write it to stdout.
// The binary name "my-cli" must match the installed binary name.
generate(Bash, &mut cmd, "my-cli", &mut io::stdout());
}
Run the program and redirect the output to a file.
cargo run > my-cli.bash
Source the script to activate completions in the current session.
source my-cli.bash
Now typing my-cli -- and hitting Tab shows --input. Typing my-cli --in and hitting Tab completes to --input. The shell reads the script, finds the completion function, and calls your binary with the hidden flag when Tab is pressed.
The community convention is to generate completions during the build process, not at runtime. Runtime generation adds a dependency to every user's install and slows down the binary. Build-time generation produces a static script that installs once and runs instantly. Ship the scripts with your release.
Realistic example with derive and subcommands
Most Rust CLIs use the clap derive macro. The macro reduces boilerplate and keeps the command definition close to the data structures. clap_complete works seamlessly with the derive macro. It reads the generated Command and produces completions for subcommands, flags, and arguments.
use clap::{Parser, Subcommand, ValueEnum, ValueHint};
use clap_complete::{generate, shells::Bash};
use std::io;
#[derive(Parser, Debug)]
#[command(name = "my-cli", version, about = "A tool for managing tasks")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Run a task
Run {
/// The target file to process
#[arg(short, long, value_hint = ValueHint::FilePath)]
file: String,
/// Verbosity level
#[arg(short, long, value_enum)]
verbose: Option<Verbosity>,
},
/// List available tasks
List,
}
#[derive(ValueEnum, Clone, Debug)]
enum Verbosity {
Quiet,
Normal,
Loud,
}
fn main() {
let mut cmd = Cli::command();
// Generate completions for the derived command.
// Subcommands and enums are handled automatically.
generate(Bash, &mut cmd, "my-cli", &mut io::stdout());
}
The #[command(subcommand)] attribute tells clap to treat the Commands enum as a set of subcommands. clap_complete detects this and generates nested completion logic. Typing my-cli run -- and hitting Tab shows --file and --verbose. Typing my-cli run --verbose= and hitting Tab shows quiet, normal, and loud.
The value_hint = ValueHint::FilePath attribute adds a hint for the shell. If the user types my-cli run --file= and hits Tab, the shell can fall back to completing file paths in the current directory. This works in Bash and Zsh when the script includes the fallback logic. The hint does not change the Rust code. It only affects the generated shell script.
Subcommands just work. The derive macro handles the hierarchy. You do not need to write special logic for subcommands.
Installation and distribution
Users expect to install completions easily. The standard pattern is to provide completion scripts in the release assets. Users download the script and place it in the shell's completion directory.
For Bash, the directory is usually /usr/share/bash-completion/completions/ for system-wide installation or ~/.local/share/bash-completion/completions/ for user installation. For Zsh, the directory is ~/.zsh/completions/ or a path in $fpath. For Fish, the directory is ~/.config/fish/completions/.
The community convention is to include a completions directory in the repository and the release tarball. The directory contains my-cli.bash, my-cli.zsh, my-cli.fish, and so on. Users copy the file to their shell's directory and reload the shell.
You can generate scripts for all shells in one step using generate_to. This function writes to a directory instead of stdout. It is useful for build scripts that produce all completion files.
use clap_complete::{generate_to, shells::*};
use std::fs;
fn generate_all_completions(cmd: &mut clap::Command) {
// Create the completions directory.
let mut out = fs::Dir::create("completions").unwrap();
// Generate scripts for each supported shell.
// The binary name must match the installed binary.
generate_to(Bash, cmd, "my-cli", &mut out).unwrap();
generate_to(Zsh, cmd, "my-cli", &mut out).unwrap();
generate_to(Fish, cmd, "my-cli", &mut out).unwrap();
generate_to(Elvish, cmd, "my-cli", &mut out).unwrap();
generate_to(PowerShell, cmd, "my-cli", &mut out).unwrap();
}
Ship the scripts. Do not make users run a command to generate completions. Static scripts win.
Pitfalls and compiler errors
If you pass an immutable reference to generate, the compiler rejects it with E0596 (cannot borrow as mutable). The generator modifies the command structure to attach completion metadata. You must pass &mut cmd.
// This fails with E0596.
let cmd = Cli::command();
generate(Bash, &cmd, "my-cli", &mut io::stdout());
// This works.
let mut cmd = Cli::command();
generate(Bash, &mut cmd, "my-cli", &mut io::stdout());
The binary name in generate must match the installed binary name exactly. If the script expects my-cli but the binary is my_cli, completions break. The shell calls the binary name from the script. If the name is wrong, the shell cannot find the program. Match the binary name exactly. The shell will not forgive a typo.
Dynamic completions are limited. clap_complete generates static scripts based on the command definition. It cannot list files in the current directory or query a database for values. You can use ValueHint for basic file completion, but complex dynamic behavior requires custom generators. Custom generators are advanced and rarely needed. Stick to static completions unless you have a specific requirement.
Generated scripts are human-readable but not meant to be edited. The scripts contain shell functions that handle quoting, escaping, and subcommand logic. Editing the script breaks the completion logic. Regenerate the script instead. Treat the script as generated code.
Decision matrix
Use clap_complete when you are using clap for argument parsing. Use generate_to when you want to produce scripts for all shells in a single directory during the build. Use generate with io::stdout() when you need a script for a specific shell on demand. Use ValueHint when you want the shell to fall back to file or directory completion for arguments. Use custom generators when you need completions that depend on runtime state, though this is rare and complex. Reach for clap_complete's built-in shell support before writing shell scripts by hand. The generated scripts handle edge cases like quoting and subcommands that manual scripts often miss.
Generate once, install everywhere. Static scripts win.