Skip to content

Completions

Completion support is opt-in: add completion to the root attribute and enable the completions cargo feature (forgetting the feature is a compile error that names it):

toml
[dependencies]
usage = { package = "usage-rs", version = "6", features = ["completions"] }
rust
#[derive(Cli)]
#[usage(bin = "ex", completion)]
struct Ex { /* … */ }

This generates script methods and wires the runtime protocol into parse():

rust
// the script a user installs into their shell
pub fn completion_script(shell: usage::complete::Shell) -> String;

// register an alias while still invoking this binary for answers
pub fn completion_script_for_alias(alias: &str, shell: usage::complete::Shell) -> String;

// where that script goes, and where it went — see "Installing the script" below
pub fn completion_install_plan(shell, env) -> Result<Plan, install::Error>;
pub fn install_completion(shell, env, on_foreign) -> Result<Installed, install::Error>;

// and the same pair for an alias
pub fn completion_install_plan_for_alias(alias, shell, env) -> Result<Plan, install::Error>;
pub fn install_completion_for_alias(alias, shell, env, on_foreign) -> Result<Installed, …>;

// answer a runtime completion request, if argv is one
pub fn completion_request(argv: &[OsString]) -> Option<String>;

Shell covers Bash, Elvish, Zsh, Fish, Nu, and PowerShell — clap's native shell set plus a usage-native Nushell target.

How it works

The installed script calls your binary back at completion time with a hidden __complete_word__ request describing the line and cursor. The request is recognized before any parsing, so it never appears in your grammar, help, or spec. parse() intercepts it automatically; with parse_from, call completion_request first and print whatever it returns.

A typical way to expose the scripts:

rust
#[derive(Args)]
struct Completion {
    /// Which shell to generate for
    shell: String,
}

// in your run function:
let shell = usage::complete::Shell::from_name(&completion.shell)
    .expect("a supported shell name");
print!("{}", Ex::completion_script(shell));

Shell aliases are explicit because each shell stores and expands them differently. To complete m exactly like mise, install Ex::completion_script_for_alias("m", shell). The generated script registers m, but its callback executes mise; it does not depend on alias expansion in the completion subprocess. Embedders can make the same distinction with usage::script::script_for(real_binary, registered_name, shell).

Candidates come from the same tables the parser uses: subcommands and their visible aliases, flags in scope at the cursor (globals included, hidden entries excluded), choices and ValueEnum words for a pending value, and negation spellings.

Installing the script

A script your user still has to redirect by hand is only half of shipping one, so #[usage(completion)] also generates the pair that puts it where the shell will look:

rust
use usage::install::{Env, OnForeign};

// Where it would go, and what else the user must do. Touches no filesystem.
let plan = Ex::completion_install_plan(shell, &Env::from_process())?;

// The same thing, written.
let done = Ex::install_completion(shell, &Env::from_process(), OnForeign::Refuse)?;
println!("installed to {}", done.plan.path.display());
if let Some(line) = done.plan.loading.instruction() {
    println!("add this to your shell's startup file, once:\n{line}");
}

Env is the environment described rather than read at the point of use, the way usage-config describes one: Env::from_process() is what a CLI passes, while a test builds Env::new(Platform::Linux, …) and asks where a script would go on a machine it is not running on. plan() is also the whole of a --dry-run — there is no flag for one, because a plan is what a preview prints.

Where each shell keeps a user's own scripts, and whether it finds one without being told:

ShellDirectoryLoads by itself
bash$BASH_COMPLETION_USER_DIR/completions, else $XDG_DATA_HOME/bash-completion/completionsyes, via bash-completion
Elvish$XDG_CONFIG_HOME/elvish/completionsno — needs sourcing
fish$XDG_CONFIG_HOME/fish/completionsyes
nushell$NU_VENDOR_AUTOLOAD_DIR, else the nushell config directoryonly in a vendor directory
zsh$XDG_DATA_HOME/zsh/site-functions, as _<name>no — needs fpath+=
PowerShell$XDG_CONFIG_HOME/powershell/completions, %LOCALAPPDATA%\PowerShell\completions on Windowsno — needs dot-sourcing

Where the answer is no, Loading::Manual carries the exact line and the file it belongs in. Printing it is the caller's job.

A file already at the target is read before anything is written, which is what separates an upgrade from a theft. Identical bytes are Wrote::Unchanged and nothing is written at all; a file carrying any @generated by usage stamp is Wrote::Updated — the family, so a script usage g completion --install wrote counts as much as one a binary wrote for itself; anything else is Error::Foreign naming the path, unless the caller passes OnForeign::Overwrite. So re-running an install after an upgrade needs no flag, while a script somebody wrote by hand survives one.

What installing never does, on purpose:

  • No startup file is edited. Not .zshrc, not $PROFILE. Writing the script again is a no-op, so an upgrade can re-run an install as often as it likes; appending a line to .zshrc again is not, and a tool that owns a user's dotfiles has no undo to offer.
  • No shell detection. You name the shell. $SHELL is the login shell, not necessarily the one running, and a guess made here would be a guess owned here.

Completing values

Three ways to say what a value can be:

Value choices and custom completers work for both detached values (--format j) and attached long values (--format=j). Attached candidates retain the flag prefix because shells replace the whole word, so the latter completes to a word such as --format=json.

rust
// a fixed set of words
#[usage(long, choices("json", "table"))]
format: Option<String>,

// paths — the shell's native file completion takes over
#[usage(long, value_hint = usage::ValueHint::FilePath)]
file: Option<PathBuf>,

// filtered paths — directories remain available for traversal
#[usage(
    long,
    value_hint = usage::ValueHint::FilePath,
    extensions("toml", "yaml")
)]
manifest: Option<PathBuf>,

// anything you can compute
#[usage(arg, name = "TASK", complete = tasks_in_file)]
task: Option<String>,

ValueHint carries clap's full stable vocabulary. Path, executable, and command hints delegate to the shell; username and hostname hints use system candidates; Other, URL, and email values suppress the shell's misleading filename fallback. Every hint emits a portable complete type into KDL so external consumers preserve the same policy.

A custom completer is a plain function, referenced by path — a typo is a compile error, not a silent dead completer:

rust
fn tasks_in_file(
    partial: &<Tasks as usage::spec::CommandArgs>::Partial,
    _ctx: &usage::complete::CompleteCtx<'_>,
) -> Vec<usage::complete::Candidate<'static>> {
    // Partial string fields hold the bytes as typed — a word that is not valid UTF-8 is
    // still a word somebody wrote.
    let file = partial
        .file
        .as_deref()
        .map(|bytes| String::from_utf8_lossy(bytes).into_owned());
    let file = file.as_deref().unwrap_or("tasks.toml");
    read_tasks(file)
        .map(|t| usage::complete::Candidate::described(t.name, t.about))
        .collect()
}

The first parameter is the partial parse of the completer's own command — flags the user has already typed are available, so a --file flag can steer what gets completed. Build candidates with Candidate::new(value) or Candidate::described(value, description); shells that display descriptions show them, shells that don't get the value alone. Chain .displayed(label) when a short insertion needs a more explanatory presentation; zsh and PowerShell keep that label separate from the text inserted into the command line, while other shells display the value. Chain .with_kind(CandidateKind::Command) (or Flag, File, or Directory) when a runtime candidate has a more specific role. PowerShell maps it to the corresponding native completion result type; shells without typed candidates keep the same value and description.

Tracing an answer

Completion decisions are available as structured diagnostic data. This uses the same split, parser walk, and completion tables as the runtime request:

rust
let line = "ex build --out ";
let split = usage::complete::split(line, line.len(), usage::complete::Shell::Zsh);
let trace = usage::complete::trace(Ex::spec(), &split);

assert_eq!(trace.awaiting_value, Some("out"));
eprintln!("{trace}");

The trace includes the shell-split words and prefix, selected command path, cursor owner, flag and separator state, candidates, and native shell fallback. Applications can expose its Display form from a diagnostic command or render the public fields themselves.

MIT LicenseCopyright © 2026jdx.dev