DEV Community

Cover image for Building a Rust CLI With clap: Arguments, Errors, and Help That Works
tdk-cli
tdk-cli

Posted on

Building a Rust CLI With clap: Arguments, Errors, and Help That Works

A command-line tool lives or dies on its arguments. A flag that silently ignores bad input, a help screen that does not match the code, or an error message that hides the cause, will each cost users more time than any feature saves. clap is the crate most Rust developers use to avoid those failures, because it generates parsing, validation, and help from a single typed definition.

Declaring arguments as types

clap's derive API describes the command as a struct:

use clap::Parser;

#[derive(Parser)]
#[command(name = "report", version, about = "Summarise a CSV file")]
struct Cli {
    /// Path to the CSV file to read
    input: std::path::PathBuf,

    /// Only include rows from this year or later
    #[arg(long, value_name = "YEAR")]
    since: Option<u32>,

    /// Print results as JSON instead of a table
    #[arg(long)]
    json: bool,
}

fn main() {
    let cli = Cli::parse();
    println!("{:?}", cli.input);
}
Enter fullscreen mode Exit fullscreen mode

Each field becomes an argument. The type decides how it is parsed: PathBuf for a file path, Option<u32> for an optional number, bool for a switch. A value that does not parse as a u32 is rejected with an error before your code runs.

The doc comments become the help text, so the help screen always matches the code. There is nothing separate to keep in sync.

Subcommands

Larger tools need subcommands, such as report summary and report export. clap models them as an enum, and each variant carries its own arguments. The dispatch in main becomes a match, which the compiler checks for completeness.

Errors and exit codes

Parsing errors are the first thing a user sees, so they should say what went wrong. clap prints the offending argument and suggests the correct form. Your own errors deserve the same care. Return a result from main, map each failure to an exit code, and print a single line that names the file or value at fault.

Use a distinct exit code for usage errors and another for runtime failures, so scripts can tell them apart.

Testing the interface

Test the command line as a user runs it. The assert_cmd crate runs your binary with arguments and checks the exit code and output. Pair it with predicates for matching output. Keep one test per important flag, and one per common failure: a missing file, a malformed number, a conflicting pair of flags.

clap also offers a debug assertion that checks the whole command definition for mistakes such as duplicate flags. Run it in a test so a bad definition fails the build, not a user's terminal.

Distribution

A Rust CLI compiles to a single binary. Build a release binary, strip it if size matters, and publish it through your package manager or as a GitHub release. Consider shell completions, which clap can generate, so users get tab completion without extra work.

When not to use clap

For a one-argument script, reading std::env::args directly is enough. clap pays off once a tool has several flags, subcommands, or help text that other people will read.

TDK CLI is itself a Node-based command-line tool, so its argument design is outside this article's scope. If you want to run a Rust CLI alongside services in a local stack, package it as a container and register it with a bring-your-own resource, or keep it outside the stack entirely.

Top comments (0)