Command-line tools with clap
clap builds the whole command line from a struct: flags, arguments, subcommands, --help and error messages. Your doc comments become the help text.
RememberDescribe your command line as a struct, derive Parser, and call Cli::parse().
todo-cli/ ├── src/ │ └── main.rs the program on this page ├── Cargo.lock created by Cargo └── Cargo.toml dependencies go here
[package]name = "todo-cli"version = "0.1.0"edition = "2024"
[dependencies]clap = { version = "4.6.7", features = ["derive"] }use clap::{Parser, Subcommand};
/// A tiny to-do list#[derive(Parser)]#[command(version)]struct Cli { /// Print more detail (allowed before or after the subcommand) #[arg(short, long, global = true)] verbose: bool,
#[command(subcommand)] command: Command,}
#[derive(Subcommand)]enum Command { /// Add a task Add { /// What to do text: String, /// Priority from 1 (high) to 3 (low) #[arg(short, long, default_value_t = 2)] priority: u8, }, /// List tasks List,}
fn main() { let cli = Cli::parse(); match cli.command { Command::Add { text, priority } => { println!("added {text:?} with priority {priority}"); if cli.verbose { println!("(verbose mode is on)"); } } Command::List => println!("nothing to list yet"), }}Here’s what you get without writing any help text by hand. After --, the arguments go to your program instead of to Cargo:
$ cargo run -- add "learn clap" -p 1 --verbose added "learn clap" with priority 1 (verbose mode is on) $ cargo run -- add "learn clap" -p abc error: invalid value 'abc' for '--priority <PRIORITY>': invalid digit found in string For more information, try '--help'. $ cargo run -- --help A tiny to-do list Usage: todo-cli [OPTIONS] <COMMAND> Commands: add Add a task list List tasks help Print this message or the help of the given subcommand(s) Options: -v, --verbose Print more detail (allowed before or after the subcommand) -h, --help Print help -V, --version Print version $ cargo run -- add --help Add a task Usage: todo-cli add [OPTIONS] <TEXT> Arguments: <TEXT> What to do Options: -p, --priority <PRIORITY> Priority from 1 (high) to 3 (low) [default: 2] -v, --verbose Print more detail (allowed before or after the subcommand) -h, --help Print help $ cargo run -- add error: the following required arguments were not provided: <TEXT> Usage: todo-cli add <TEXT> For more information, try '--help'.
The same tool in Go, with the standard flag package
Section titled “The same tool in Go, with the standard flag package”package main
import ( "flag" "fmt" "os")
func main() { verbose := flag.Bool("verbose", false, "print more detail") flag.Parse() if flag.NArg() < 1 { fmt.Fprintln(os.Stderr, "usage: todo [-verbose] add|list ...") os.Exit(2) }
switch flag.Arg(0) { case "add": addCmd := flag.NewFlagSet("add", flag.ExitOnError) priority := addCmd.Int("priority", 2, "priority from 1 (high) to 3 (low)") addCmd.Parse(flag.Args()[1:]) if addCmd.NArg() < 1 { fmt.Fprintln(os.Stderr, "usage: todo add [-priority N] TEXT") os.Exit(2) } fmt.Printf("added %q with priority %d\n", addCmd.Arg(0), *priority) if *verbose { fmt.Println("(verbose mode is on)") } case "list": fmt.Println("nothing to list yet") default: fmt.Fprintln(os.Stderr, "unknown command:", flag.Arg(0)) os.Exit(2) }}added "learn flags" with priority 1 (verbose mode is on)$ ./todo -h
Usage of ./todo:
-verbose
print more detail$ ./todo addusage: todo add [-priority N] TEXT$ ./todo add -priority 1 "learn flags" -verbose
added "learn flags" with priority 1
The last command puts -verbose after the text. Go’s flag package stops reading flags at the first argument that is not a flag (docs), so it did not complain and did not turn verbose mode on.
- Doc comments (
///) become the help text. The two--helpruns above are built from them, so the description sits next to the code it describes. - Types do the checking.
priority: u8is why-p abcis rejected with a clear message, and why-p 300is too (try it below). Au8allows 0 to 255, so “between 1 and 3” is still a rule you write yourself.default_value_t = 2shows up as[default: 2]in the help. - Subcommands are an enum, so a
matchwithout a_arm must cover every one. Add aDone { id: u32 }variant and leavemainalone, and the build fails (Enums explains why):
error[E0004]: non-exhaustive patterns: `Command::Done { .. }` not covered --> src/main.rs:36:11 | 36 | match cli.command { | ^^^^^^^^^^^ pattern `Command::Done { .. }` not covered | note: `Command` defined here --> src/main.rs:16:6 | 16 | enum Command { | ^^^^^^^ ... 28 | Done { | ---- not covered = note: the matched value is of type `Command` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 43 ~ Command::List => println!("nothing to list yet"), 44 ~ Command::Done { .. } => todo!(),
- Global flags. A flag on the top-level struct has to come before the subcommand unless it is marked
global = true, which lets--verbosego anywhere. The first run on this page uses it afteradd. The session below takes, global = trueout of a copy and runs it again: afteraddit is refused, and beforeaddit works. Go’sflag(above) and Python’sargparse(in the table below) also need it first.
$ cargo run -- add "learn clap" --verbose # with global = true added "learn clap" with priority 2 (verbose mode is on) $ cargo run -- add "learn clap" --verbose # after deleting ", global = true" error: unexpected argument '--verbose' found tip: to pass '--verbose' as a value, use '-- --verbose' Usage: todo-cli add <TEXT> For more information, try '--help'. $ cargo run -- --verbose add "learn clap" # still without global = true added "learn clap" with priority 2 (verbose mode is on)
- Exit codes. In these runs a usage error exited with code 2 and
--helpwith 0. The clap docs forError::exitsay the same: status 2 when it prints to stderr, 0 when it prints to stdout.
The same job in your language
Section titled “The same job in your language”Where the image has a parser that comes with the language or its runtime, I wrote the same to-do tool with it. The other cells only name the usual libraries; I did not run those.
| Language | Built in, and run for this page | Libraries people use (named, not run) |
|---|---|---|
| Rust | std::env::args is only the raw list of strings (Arguments) |
clap |
| Go | flag (the program above) |
cobra |
| Python | argparse |
Typer, Click |
| TypeScript | util.parseArgs, built into Node (the version is the first line of the runs below) |
commander, yargs |
| Java | nothing run | picocli |
| Kotlin | nothing run | Clikt |
| C# | nothing run | System.CommandLine (a NuGet package, per Microsoft’s docs) |
| C | getopt (POSIX, from the C library; run with glibc, the GNU C library) |
none named |
| C++ | nothing run | CLI11, cxxopts |
What the runs showed:
$ node --version && python3 --version v24.22.1 Python 3.13.5 $ python3 todo.py --verbose add "learn argparse" -p 1 # Python, argparse added "learn argparse" with priority 1 (verbose mode is on) $ python3 todo.py add "learn argparse" --verbose # Python usage: todo.py [-h] [-v] {add,list} ... todo.py: error: unrecognized arguments: --verbose $ python3 todo.py add "learn argparse" -p abc # Python usage: todo.py add [-h] [-p PRIORITY] text todo.py add: error: argument -p/--priority: invalid int value: 'abc' $ node todo.ts add "learn parseArgs" --verbose -p 1 # TypeScript, node:util parseArgs added "learn parseArgs" with priority 1 (verbose mode is on) $ node todo.ts add "learn parseArgs" -p abc # TypeScript invalid priority: abc $ gcc -Wall -o todo todo.c # C, getopt $ ./todo -v add "learn getopt" -p 1 # C added "learn getopt" with priority 1 (verbose mode is on) $ ./todo add "learn getopt" -v # C (glibc) added "learn getopt" with priority 2 (verbose mode is on) $ ./todo add "learn getopt" -p abc # C invalid priority: abc
argparsechecks the number for you (type=int), like clap, and its subparsers also need--verbosebeforeadd.parseArgsandgetopthand back text. The number check and the “invalid priority” message are code I wrote. Neither knows about subcommands, so--verboseand-vwere accepted after the text. Forgetoptthat is glibc’s default (glibc is the GNU C library): it moves non-option arguments to the end as it scans, andPOSIXLY_CORRECTor a leading+in the option string stops at the first one (man page).
Try it yourself
Section titled “Try it yourself”Run each command in the project. The session below was run in the image.
cargo run -- --version. It printstodo-cli 0.1.0, from#[command(version)]and the version inCargo.toml.cargo run -- add "learn clap" -p 300. Look for300 is not in 0..=255: theu8type is the rule.cargo run -- ad "learn clap". clap rejects the unknown subcommand and adds a tip thataddis similar.cargo run -- add "-draft". The text starts with a dash, so clap reads-das a flag and says to use-- -d; with--before it, the same text is accepted as the task.
$ cargo run -- --version todo-cli 0.1.0 $ cargo run -- add "learn clap" -p 300 error: invalid value '300' for '--priority <PRIORITY>': 300 is not in 0..=255 For more information, try '--help'. $ cargo run -- ad "learn clap" error: unrecognized subcommand 'ad' tip: a similar subcommand exists: 'add' Usage: todo-cli [OPTIONS] <COMMAND> For more information, try '--help'. $ cargo run -- add "-draft" error: unexpected argument '-d' found tip: to pass '-d' as a value, use '-- -d' Usage: todo-cli add [OPTIONS] <TEXT> For more information, try '--help'. $ cargo run -- add -- "-draft" added "-draft" with priority 2
Read more
Section titled “Read more”- clap’s derive tutorial and derive reference, for the version this page pins (4.6.7)
- The Rust Book, chapter 12.1: reading arguments by hand with
std::env::args - Command Line Applications in Rust: a tutorial book whose argument-parsing chapter uses clap
- Arguments, Environment, Time and Processes: what
stdgives you without clap
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.