LCM Logo
Programming

Rust CLIs with clap

clap is the de facto standard for parsing command-line arguments in Rust. You declare your interface as a struct with attributes, and clap derives the parser, validation, --help text, --version, and error messages from it. This guide covers the derive API, which is the recommended starting point; the lower-level builder API exists for cases needing runtime-determined arguments. It assumes you know the language basics from the Rust fundamentals guide — structs, enums, Option, and Result all feature heavily here, because a clap CLI is a struct.

The canonical reference is the derive tutorial on docs.rs; sections below link into it. Current version at the time of writing: clap 4.6.

Setup

Add clap with the derive feature flag (the derive macros are opt-in):

shell
cargo add clap --features derive

A first CLI

Derive Parser on a struct; each field becomes an argument. Doc comments (///) become the help text. #[command(version, about)] fills --version and the description from Cargo.toml, so they never drift from your package metadata.

src/main.rs
use clap::Parser;

/// Greet someone from the command line
#[derive(Parser)]
#[command(version, about)]
struct Cli {
    /// Name of the person to greet
    name: String,
}

fn main() {
    let cli = Cli::parse();
    println!("Hello, {}!", cli.name);
}

That's a complete program: cargo run -- Ada prints the greeting, cargo run -- --help prints generated usage, and a missing NAME produces a clear error and nonzero exit code — all for free. (The -- separates cargo's arguments from your program's.)

Configuring the command

Any Command builder method can be used as a #[command(...)] attribute — the attribute list is not limited to a fixed set. A realistic top-level configuration:

src/main.rs
#[derive(Parser)]
#[command(
    name = "rtemis",
    version,
    about = "The rtemis command-line interface.",
    before_help = logo(),
    arg_required_else_help = true
)]
struct Cli {
    // ...
}

Taking these in turn: name sets the program name shown in usage and version output (default: the package name from Cargo.toml); bare version and about pull from Cargo.toml, though about here overrides with an explicit string; before_help prints text above the help output — and because attribute values are arbitrary expressions, it can call a function like logo() that returns a banner String; arg_required_else_help = true prints help instead of an error when the program is run with no arguments — the right default for a subcommand-style tool.

Other commonly reached-for methods include after_help (text below the help, e.g. examples), long_version (extended --version output with build info), and propagate_version (make --version work inside every subcommand).

Positionals, options, and flags

By default a field is a positional argument (tutorial: adding arguments). Add #[arg(short, long)] to make it a named option (-n / --name, inferred from the field name). A bool field with short/long becomes a flag that is false unless passed. Field types drive everything else: Option<T> makes an argument optional, Vec<T> accepts repeated values, and default_value_t supplies a default.

src/main.rs
use std::path::PathBuf;
use clap::Parser;

/// Search files for a pattern
#[derive(Parser)]
#[command(version, about)]
struct Cli {
    /// Pattern to search for
    pattern: String,

    /// Files to search (one or more)
    paths: Vec<PathBuf>,

    /// Maximum number of matches to print
    #[arg(short = 'm', long, default_value_t = 10)]
    max_count: usize,

    /// Write output to a file instead of stdout
    #[arg(short, long, value_name = "FILE")]
    output: Option<PathBuf>,

    /// Ignore case when matching
    #[arg(short, long)]
    ignore_case: bool,
}

fn main() {
    let cli = Cli::parse();
    println!("searching for {:?} in {} file(s)", cli.pattern, cli.paths.len());
}

This parses invocations like grepish TODO src/main.rs src/lib.rs -i -m 3 --output hits.txt. Note PathBuf rather than String for paths — clap parses into any type that implements FromStr, and the help shows <FILE> thanks to value_name.

A repeatable flag like -v -v -v for verbosity uses a u8 field with #[arg(short, long, action = clap::ArgAction::Count)] — the field holds how many times it appeared.

Validated and enumerated values

Because clap parses into typed fields, much validation is automatic: passing abc to a usize field fails with a clear error before your code runs. Beyond that, constrain values in the attribute (tutorial: validation).

For a fixed set of choices, derive ValueEnum — invalid input gets an error listing the valid values, and --help documents them:

rust
use clap::{Parser, ValueEnum};

#[derive(Copy, Clone, ValueEnum)]
enum Format {
    Plain,
    Json,
    Csv,
}

#[derive(Parser)]
struct Cli {
    /// Output format
    #[arg(long, value_enum, default_value_t = Format::Plain)]
    format: Format,
}

For numeric ranges, use value_parser; for anything else, any function fn(&str) -> Result<T, String> works as a custom parser:

rust
#[derive(clap::Parser)]
struct Cli {
    /// Network port to use
    #[arg(long, value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
}

Passing --port 0 exits with error: invalid value '0' for '--port <PORT>': 0 is not in 1..=65535.

Subcommands

Tools like git and cargo split functionality into subcommands (git commit, cargo build). Model them as an enum deriving Subcommand, where each variant carries its own arguments (tutorial: subcommands). Dispatch is an exhaustive match, so adding a variant forces you to handle it.

src/main.rs
use clap::{Parser, Subcommand};

/// Manage a todo list
#[derive(Parser)]
#[command(version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Add a new task
    Add {
        /// Task description
        description: String,
        /// Mark as high priority
        #[arg(short, long)]
        priority: bool,
    },
    /// List all tasks
    List,
    /// Mark a task as done
    Done {
        /// Task number from `list`
        id: usize,
    },
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Add { description, priority } => {
            println!("adding{}: {description}", if priority { " (high)" } else { "" });
        }
        Commands::List => println!("listing tasks"),
        Commands::Done { id } => println!("completing task {id}"),
    }
}

Running with no subcommand prints help; todo add --priority "ship guide" and todo done 3 both parse into their variant. For larger CLIs, give each variant a dedicated #[derive(Args)] struct (Add(AddArgs)) and keep each subcommand's arguments in its own module.

Errors and exit codes

Cli::parse() handles bad input for you: it prints the error and exits with code 2. Your own runtime failures should flow through Result, exactly as in the error handling section of the fundamentals guide — return Result from main and let ? propagate:

src/main.rs
use clap::Parser;

#[derive(Parser)]
struct Cli {
    path: std::path::PathBuf,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    let content = std::fs::read_to_string(&cli.path)?; // I/O error propagates
    println!("{} lines", content.lines().count());
    Ok(())
}

Many real CLIs use the anyhow crate here — its anyhow::Result and .context("while reading config")? produce friendlier error chains with no boilerplate.

Testing your interface

clap reports most definition mistakes (conflicting flags, invalid defaults) as debug assertions at parse time. One unit test catches them all without running every subcommand:

src/main.rs
#[test]
fn verify_cli() {
    use clap::CommandFactory;
    Cli::command().debug_assert();
}

Resources

The derive tutorial is the canonical walkthrough, and the derive reference documents every attribute. The cookbook has complete application-style examples, including a git clone and a pacman clone.

Beyond parsing, the Rust CLI book covers the wider ecosystem: anyhow for errors, indicatif for progress bars, assert_cmd for end-to-end testing, and clap_complete for generating shell completions from the same struct you already wrote.

On this page