Skip to content

Rust Framework

usage-rs is a fast, typed framework for building complete command-line applications in Rust. Declare commands, flags, arguments, and settings with familiar structs and enums, and get first-class environment and config-file resolution, advanced shell completions, portable validation, negation flags, typed argument groups, categorized subcommands, and more.

In the mise-scale benchmark it parses hundreds of times faster than clap, with no third-party runtime crates and a 1.3 MB stripped binary versus clap's 3.1 MB. See the performance results and clap migration guide.

The same declaration also becomes a portable usage spec that the binary can print. usage-cli turns it into documentation, manpages, and completions — the same toolchain used across jdx's CLIs.

rust
use usage::Cli;

/// A tool that does things
#[derive(Cli)]
#[usage(bin = "ex", version = "1.0")]
struct Cli {
    /// How many jobs to run at once
    #[usage(short = 'j', long, env = "EX_JOBS", default = "4")]
    jobs: Option<String>,

    /// Print more
    #[usage(short = 'v', long, count)]
    verbose: u8,

    /// Colorize output
    #[usage(long, negate = "--no-color", default = "true")]
    color: bool,

    /// Files to process
    files: Vec<String>,
}

fn main() {
    let cli = Cli::parse();
    // cli.jobs, cli.verbose, cli.color, cli.files are ready to use
}

Doc comments are the help text: the first paragraph becomes the short help shown by -h, the whole comment becomes the long help shown by --help.

Parser overhead

What parsing mise use -g node@20 costs each framework, against a shadow of mise's CLI: 211 commands, 711 flags. The usage, clap, and bpaf programs are generated from the same checked-in spec.

usage-rs vs clap, bpaf

wall time, in-process parse throughput How this is measured Each parser runs repeatedly inside one process; process startup is excluded, so these bars are not full CLI invocation times. The chart reports the fastest per-parse time from many short rounds. Minima and their ratios drift a few percent between runs and machines, hence the ~. Instructions for one cold parse, which do not drift: 4,155 · 5.89M · 21.9M, agreeing across two machines to 0.15%.

usage-rs0.19 µs
clap480 µs· ~2,500× more
bpaf1,600 µs· ~8,200× more

usage-rs starts from compiler-emitted static tables and scans only the current command's flags plus inherited globals. clap and bpaf build a parser before they can use one Where their time goes Most of clap's is constructing and validating its command tree. bpaf's is larger because it assembles a combinator tree per run as well, which reusing the parser across parses only halves. . Heap allocations for a bare parse: zero, against clap's 6,560.

Methodology and raw numbers: tasks/perf-shadow.sh · time-sweep.rs

That speed moves work into compilation. For mise's 211 commands and 722 flags, a debug rebuild took about 10 seconds with usage-rs, 4 seconds with clap, and 2 seconds with bpaf (measurements).

Installation

One dependency. Add usage-rs to your Cargo.toml, aliased to usage:

toml
[dependencies]
usage = { package = "usage-rs", version = "6" }

Nothing third-party links into your binary — the only non-usage crates in the graph are the derive's compiler, which runs at build time (comparison with clap).

usage-rs is a facade. Applications should depend on it alone. The split underneath stays available for low-level adopters that want a thinner surface:

CrateRole
usage-rsThe one package an application depends on; re-exports the whole runtime
usage-deriveThe derive macros: Cli, Args, Subcommands, ValueEnum, ArgGroup, and Config (behind the config feature)
usage-argvThe zero-allocation, zero-dependency runtime the derive emits code against
usage-testTest helpers: what a command line parses to, what a page says, what a shell is offered
usage-configLayered settings resolution with provenance (Configuration)
usage-dynamicCommands discovered at runtime, merged into help and completion (Dynamic commands)

Cargo features

FeatureDefaultWhat it enables
specSpec metadata and to_kdl(); gates the derives
help-h / --help page rendering
diagnosticsclap-shaped error messages from render_failure
completionsShell completion scripts and the runtime completion protocol (complete is an alias of this feature)
validationPortable validate / validate_error expressions (Validation)
testusage::test: command output, parse, and help assertions (completion assertions want completions too)
configThe usage::Config derive and the resolver as usage::config (Configuration)
response-filesExplicit @file argument expansion as usage::response (Response files)

Parse entry points

#[derive(Cli)] generates these on your struct:

rust
// parse std::env::args; print help/version/errors and exit as appropriate
pub fn parse() -> Self;

// parse the given argv; hand errors (including help/version requests) back to you
pub fn parse_from<'v>(argv: &[&'v OsStr]) -> Result<Self, usage::Error<'static, 'v>>;

// the static parse tables and spec metadata
pub fn command() -> &'static usage::Command<'static>;
pub fn spec() -> &'static usage::spec::Spec<'static>;

// the usage spec as KDL
pub fn to_kdl() -> String;

parse() is the whole program shell: it prints the help page to stdout and exits 0 for -h/--help, prints {bin} {version} and exits 0 for --version, and prints a rendered failure to stderr and exits 2 — clap's exit status, so scripts that check for it keep working. parse_from gives you the same machinery without the process control; see Help, version, and errors for handling its Err variants.

For programs that parse more than once, see Updating an existing value.

Generated dispatch

#[usage(run)] generates the match that routes parsed subcommands to their Run implementations. Context and async variants are covered in Dispatch.

One declaration, every artifact

Because the derive also emits a usage spec, usage-cli can generate documentation, manpages, and shell completions from your CLI. Every binary answers __usage_spec__ with its own spec:

bash
mycli __usage_spec__ > mycli.usage.kdl
usage g markdown -f mycli.usage.kdl --out-dir docs
usage g manpage -f mycli.usage.kdl > mycli.1
usage g completion bash mycli --file mycli.usage.kdl

Cli::to_kdl() is the same document in-process, for a build script or a checked-in artifact.

See Spec output for the round-trip guarantees, what the emitted KDL looks like, and how to opt out of the endpoint.

Where to go next

  • Quickstart — a small CLI from declaration to generated docs, end to end
  • Args and flags — field types, attributes, env vars, defaults
  • Updating values — merge another command line into an existing value
  • Subcommands — command enums, nesting, flatten, value enums
  • Dynamic commands — commands discovered at runtime, in help and completion
  • DispatchRun, RunWith, the async pair, and the generated match
  • Validation — choices, groups, exclusive, delimiter, conflicts, portable validate
  • Help, version, and errors — what the parser renders and how to hook it
  • Completions — static scripts and runtime completion
  • Configuration — settings declared in code: usage::Config and layered resolution
  • Response files — opt-in, nested @file argument expansion
  • Testing — run commands or assert directly on parsing, help, and completions
  • Spec output — the emitted KDL and usage-cli integration
  • Migrating from clap — mechanical rewrites and intentional API breaks
  • Performance — what a parse costs, measured at mise's scale