blog

Parsing CLI Arguments in Rust with clap's Derive API

Turn a plain struct into a full argument parser, complete with flags, options, help text and --version, in about a dozen lines.

  • Rust
  • CLI
  • clap

Every command-line tool starts the same way: it needs to read some arguments. You can loop over std::env::args() by hand, but you'll be rewriting help text, validation and error messages within the hour. The clap crate handles all of that, and its derive API lets you describe your whole CLI as a plain struct.

Set it up

cargo new linecount
cd linecount
cargo add clap --features derive

Describe the arguments as a struct

use std::path::PathBuf;

use clap::Parser;

/// A tiny example CLI
#[derive(Parser, Debug)]
#[command(version)]
struct Cli {
    /// File to read
    path: PathBuf,

    /// Print extra detail
    #[arg(short, long)]
    verbose: bool,

    /// Stop after this many lines
    #[arg(short = 'n', long, default_value_t = 1000)]
    limit: usize,
}

fn main() {
    let cli = Cli::parse();

    if cli.verbose {
        eprintln!("verbose mode on");
    }
    println!("would read up to {} lines from {}", cli.limit, cli.path.display());
}

That's the whole parser. A few things are happening here:

  • A field with no attribute (path) becomes a required positional argument.
  • A bool field becomes a flag. short and long give you -v and --verbose.
  • The field's type is the validation. Pass --limit abc and clap rejects it with a clear error, because abc isn't a usize.
  • Doc comments (///) become the help text, so your documentation and your --help never drift apart.

Try it

cargo run -- --help now prints something like this, with no extra code:

A tiny example CLI

Usage: linecount [OPTIONS] 

Arguments:
    File to read

Options:
  -v, --verbose        Print extra detail
  -n, --limit   Stop after this many lines [default: 1000]
  -h, --help           Print help
  -V, --version        Print version

Cli::parse() also handles the failure path for you: on bad input it prints the error and exits with status 2, the conventional code for a usage error. If you'd rather handle that yourself, Cli::try_parse() returns a Result instead.

From here, the natural next steps are subcommands (put #[derive(Subcommand)] on an enum) and value validation with value_parser. Both are just more attributes on the same struct.