Project: Command-Line Tool with clap
On the Command-Line Arguments page you read arguments by hand with std::env::args(). That works for one or two arguments. But real tools need options like --precision 3, short forms like -p 3, a --help page, good error messages, and subcommands like git commit and git push. Writing all of that by hand is a lot of boring, error-prone code.
clap (Command Line Argument Parser) is the crate almost every Rust CLI uses for this. You describe your arguments as a normal Rust struct, and clap writes the parser, the help text and the error messages for you. In this project you’ll build convert, a small unit converter with two subcommands: temp for temperatures and length for lengths.
What you’ll build
Section titled “What you’ll build”Here’s the finished tool in a terminal. Every output on this page comes from actually running the program.
$ cargo run -q -- temp 100 --from celsius --to fahrenheit100 °C = 212.00 °F$ cargo run -q -- temp -40 -f c -t f-40 °C = -40.00 °F$ cargo run -q -- length 5 km mi5 km = 3.11 mi$ cargo run -q -- -p 0 length 1 mi ft1 mi = 5280 ft$ cargo run -q -- temp 20 --from rankine --to cerror: invalid value 'rankine' for '--from <FROM>' [possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.Everything after -- goes to your program instead of to Cargo, and -q (quiet) hides Cargo’s own “Compiling…” lines. That last error message? We didn’t write it. clap did.
Step 1: Create the project and add clap
Section titled “Step 1: Create the project and add clap”cargo new convertcd convertcargo add clap --features deriveThe derive feature is the important part. It turns on #[derive(Parser)], which is the friendly way to use clap. Your Cargo.toml now contains:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }(Your version number may be a little newer. Anything starting with 4 works for this page. See Cargo and Crates if cargo add is new to you.)
Step 2: Your first parser
Section titled “Step 2: Your first parser”Let’s start with a tool that only converts Celsius to Fahrenheit. You write a struct with one field per argument and put #[derive(Parser)] on it. Replace src/main.rs with:
use clap::Parser;
/// Convert a temperature from Celsius to Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// The temperature in Celsius celsius: f64,
/// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2)] precision: usize,
/// Also print the formula that was used #[arg(short, long)] verbose: bool,}
fn main() { let cli = Cli::parse(); let fahrenheit = cli.celsius * 9.0 / 5.0 + 32.0; println!("{} °C = {:.*} °F", cli.celsius, cli.precision, fahrenheit); if cli.verbose { println!("(used °F = °C × 9/5 + 32)"); }}$ cargo run -q -- 21.521.5 °C = 70.70 °F$ cargo run -q -- 100 --precision 1 --verbose100 °C = 212.0 °F(used °F = °C × 9/5 + 32)clap reads each field and decides what kind of argument it is:
celsiushas no#[arg(...)]options, so it’s a positional argument: the first plain value you type. Because its type isf64, clap also parses the text"21.5"into a number for you.#[arg(short, long)]makes an option with a short name and a long name taken from the field:-p/--precision.default_value_t = 2is used when you leave it out.- A
boolfield becomes a flag: it’strueif--verbose(or-v) is present,falseif not. - An
Option<T>field (not used here) would be an argument you may leave out entirely.
{:.*} in println! means “use the next argument as the number of decimal places”. That’s how --precision reaches the output.
Cli::parse() reads the real command line. The Rust Playground has no command line, so to try the parser there, use Cli::parse_from with a list of pretend arguments. The first item is always the program’s name, just like std::env::args():
use clap::Parser;
/// Convert a temperature from Celsius to Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// The temperature in Celsius celsius: f64,
/// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2)] precision: usize,
/// Also print the formula that was used #[arg(short, long)] verbose: bool,}
fn main() { // Pretend the user typed: convert 100 --precision 1 --verbose let cli = Cli::parse_from(["convert", "100", "--precision", "1", "--verbose"]); println!("{cli:?}");
let fahrenheit = cli.celsius * 9.0 / 5.0 + 32.0; println!("{} °C = {:.*} °F", cli.celsius, cli.precision, fahrenheit);}Output:
Cli { celsius: 100.0, precision: 1, verbose: true }100 °C = 212.0 °FTry changing the pretend arguments: remove "--verbose", or swap "--precision" for "-p".
Step 3: Help text from doc comments
Section titled “Step 3: Help text from doc comments”Did you notice the /// lines in the struct? Those are doc comments, and clap turns them into the help page. The comment above the struct describes the whole program; the comment above each field describes that argument. You get --help (and -h) without writing anything else:
$ cargo run -q -- --helpConvert a temperature from Celsius to Fahrenheit
Usage: convert [OPTIONS] <CELSIUS>
Arguments: <CELSIUS> The temperature in Celsius
Options: -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -v, --verbose Also print the formula that was used -h, --help Print help -V, --version Print version$ cargo run -q -- --versionconvert 1.0The #[command(name = "convert", version = "1.0")] line sets the name and version shown here. If you write just #[command(version)], clap takes the version from your Cargo.toml instead.
clap also explains mistakes for you:
$ cargo run -q --error: the following required arguments were not provided: <CELSIUS>
Usage: convert <CELSIUS>
For more information, try '--help'.$ cargo run -q -- abcerror: invalid value 'abc' for '<CELSIUS>': invalid float literal
For more information, try '--help'.Good help text is a gift to your future self. Write the doc comments as if someone is seeing your tool for the first time — because one day, that someone will be you.
Step 4: Subcommands
Section titled “Step 4: Subcommands”Now let’s support lengths too. Tools like git and cargo use subcommands (cargo build, cargo test) to group different jobs. In clap, a subcommand is an enum with #[derive(Subcommand)]. Each variant is one subcommand, and its fields are that subcommand’s arguments:
use clap::{Parser, Subcommand};
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2, global = true)] precision: usize,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Convert a temperature Temp { /// The temperature to convert value: f64,
/// The unit to convert from #[arg(short, long)] from: String,
/// The unit to convert to #[arg(short, long)] to: String, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: String,
/// The unit to convert to to: String, },}
fn main() { let cli = Cli::parse_from(["convert", "temp", "100", "--from", "c", "--to", "f"]); println!("{cli:?}");
let cli = Cli::parse_from(["convert", "length", "5", "km", "mi", "-p", "1"]); println!("{cli:?}");
// You find out which subcommand was used with a normal match. match cli.command { Command::Temp { value, from, to } => println!("temp: {value} {from} -> {to}"), Command::Length { value, from, to } => println!("length: {value} {from} -> {to}"), }}Output:
Cli { precision: 2, command: Temp { value: 100.0, from: "c", to: "f" } }Cli { precision: 1, command: Length { value: 5.0, from: "km", to: "mi" } }length: 5 km -> miA few details:
#[command(subcommand)]on thecommandfield tells clap “this field holds the chosen subcommand”.- The variant name
Tempbecomes the subcommandtemp(clap lowercases it for you). tempuses named options (--from c --to f) whilelengthuses positional arguments (5 km mi). Both styles are common; you get to see each one.global = truelets--precisionappear before or after the subcommand. Without it,-p 1would only be accepted beforelength.
The help page now lists the subcommands, and each subcommand has its own help:
$ cargo run -q -- --helpA small unit converter for temperatures and lengths
Usage: convert [OPTIONS] <COMMAND>
Commands: temp Convert a temperature length Convert a length help Print this message or the help of the given subcommand(s)
Options: -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -h, --help Print help -V, --version Print version$ cargo run -q -- temp --helpConvert a temperature
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> The temperature to convert
Options: -f, --from <FROM> The unit to convert from -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -t, --to <TO> The unit to convert to -h, --help Print helpBut there’s a problem. The units are plain Strings, so clap accepts anything:
$ cargo run -q -- temp 20 --from rankine --to cCli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }We’d have to check every unit by hand later. Let’s make clap do it.
Step 5: Enums and validation with ValueEnum
Section titled “Step 5: Enums and validation with ValueEnum”When an argument can only be one of a few words, describe those words as an enum and derive ValueEnum. clap then accepts only those words, lists them in the help, and rejects everything else. These enums replace the String fields from Step 4:
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}- Each variant becomes a lowercase word:
Celsius→celsius,Km→km. #[value(alias = "c")]also accepts the short formc, without showing it in the help.- clap needs
Clone. We addCopytoo, because these small enums are cheap to copy and that makesmatcheasier.
In the Command enum, change from: String to from: TempUnit (and LengthUnit for lengths). We also add two more rules:
/// How many digits to show after the decimal point#[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))]precision: u8,/// The temperature to convert#[arg(allow_negative_numbers = true)]value: f64,value_parser!(u8).range(0..=6)only accepts a precision from 0 to 6. More than that would just print noise.allow_negative_numbers = truelets you type-40. Without it, clap thinks-40is a short option called-4(see Common mistakes). Temperatures can be negative, so we allow it; lengths can’t, so we don’t.
Now clap catches bad input before your code ever runs:
$ cargo run -q -- temp 20 --from rankine --to cerror: invalid value 'rankine' for '--from <FROM>' [possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.$ cargo run -q -- length 3 yd merror: invalid value 'yd' for '<FROM>' [possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.$ cargo run -q -- -p 9 length 1 m cmerror: invalid value '9' for '--precision <PRECISION>': 9 is not in 0..=6
For more information, try '--help'.And the help page now shows the possible values:
$ cargo run -q -- temp --helpConvert a temperature
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> The temperature to convert
Options: -f, --from <FROM> The unit to convert from [possible values: celsius, fahrenheit, kelvin] -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -t, --to <TO> The unit to convert to [possible values: celsius, fahrenheit, kelvin] -h, --help Print helpThis is the big idea of clap: once parsing succeeds, your data is already valid. from is a real TempUnit, not a string that might be anything. The rest of your program can trust it.
Step 6: Errors and exit codes
Section titled “Step 6: Errors and exit codes”Some mistakes clap can’t catch, because they depend on the meaning of the values. -500 °C is a perfectly good number, but it’s colder than absolute zero (-273.15 °C), so it’s not a real temperature. That check belongs in our own code.
A tidy pattern is to put the real work in a run function that returns a Result, and let main decide what to print and which exit code to return. Programs report success or failure to the shell with a number: 0 means success, anything else means failure. Rust’s std::process::ExitCode lets main return one:
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS // exit code 0 } Err(message) => { eprintln!("error: {message}"); // errors go to stderr ExitCode::FAILURE // exit code 1 } }}clap follows the same rules. When parsing fails, Cli::parse() prints the error to stderr and exits with code 2, the usual code for “you used the command wrong”. --help and --version exit with 0. In a terminal, echo $? shows the last exit code:
$ cargo run -q -- temp -500 -f c -t kerror: -500 °C is colder than absolute zero$ echo $?1$ cargo run -q -- length 3 yd merror: invalid value 'yd' for '<FROM>' [possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.$ echo $?2Why does this matter? Scripts and other programs check exit codes to know if your tool worked. The Environment and Processes page covers exit codes and stderr in more detail. In bigger tools you’d probably use anyhow for run’s error type instead of String, so you get context and error chains for free.
Step 7: The complete program
Section titled “Step 7: The complete program”Here is everything together. Replace src/main.rs with this and try the commands from the top of the page.
use clap::{Parser, Subcommand, ValueEnum};use std::process::ExitCode;
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))] precision: u8,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Convert a temperature Temp { /// The temperature to convert #[arg(allow_negative_numbers = true)] value: f64,
/// The unit to convert from #[arg(short, long)] from: TempUnit,
/// The unit to convert to #[arg(short, long)] to: TempUnit, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: LengthUnit,
/// The unit to convert to to: LengthUnit, },}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}
impl TempUnit { fn symbol(self) -> &'static str { match self { TempUnit::Celsius => "°C", TempUnit::Fahrenheit => "°F", TempUnit::Kelvin => "K", } }}
impl LengthUnit { // How many meters one of this unit is. fn meters(self) -> f64 { match self { LengthUnit::Mm => 0.001, LengthUnit::Cm => 0.01, LengthUnit::M => 1.0, LengthUnit::Km => 1000.0, LengthUnit::In => 0.0254, LengthUnit::Ft => 0.3048, LengthUnit::Mi => 1609.344, } }
fn name(self) -> &'static str { match self { LengthUnit::Mm => "mm", LengthUnit::Cm => "cm", LengthUnit::M => "m", LengthUnit::Km => "km", LengthUnit::In => "in", LengthUnit::Ft => "ft", LengthUnit::Mi => "mi", } }}
fn convert_temp(value: f64, from: TempUnit, to: TempUnit) -> Result<f64, String> { // Step 1: turn whatever we got into Celsius. let celsius = match from { TempUnit::Celsius => value, TempUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0, TempUnit::Kelvin => value - 273.15, }; if celsius < -273.15 { return Err(format!("{value} {} is colder than absolute zero", from.symbol())); } // Step 2: turn Celsius into the unit we want. Ok(match to { TempUnit::Celsius => celsius, TempUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0, TempUnit::Kelvin => celsius + 273.15, })}
fn convert_length(value: f64, from: LengthUnit, to: LengthUnit) -> f64 { value * from.meters() / to.meters()}
fn run(cli: &Cli) -> Result<String, String> { let p = cli.precision as usize; match cli.command { Command::Temp { value, from, to } => { let result = convert_temp(value, from, to)?; Ok(format!("{value} {} = {result:.p$} {}", from.symbol(), to.symbol())) } Command::Length { value, from, to } => { let result = convert_length(value, from, to); Ok(format!("{value} {} = {result:.p$} {}", from.name(), to.name())) } }}
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS } Err(message) => { eprintln!("error: {message}"); ExitCode::FAILURE } }}Two small tricks in run: {result:.p$} means “print result with p decimal places”, and the ? after convert_temp(...) returns early with the error if the temperature is impossible.
Step 8: Testing your parser with try_parse_from
Section titled “Step 8: Testing your parser with try_parse_from”Cli::parse() exits the program when something is wrong. That’s what you want for users, but not in a test. Cli::try_parse_from(...) does the same parsing on a list of arguments you choose, and returns a Result instead of exiting. That makes it perfect for checking your parser — and it runs in the Playground:
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))] precision: u8,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Convert a temperature Temp { /// The temperature to convert #[arg(allow_negative_numbers = true)] value: f64,
/// The unit to convert from #[arg(short, long)] from: TempUnit,
/// The unit to convert to #[arg(short, long)] to: TempUnit, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: LengthUnit,
/// The unit to convert to to: LengthUnit, },}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}
fn check(line: &str) { println!("$ {line}"); // split_whitespace gives us the arguments, program name first. match Cli::try_parse_from(line.split_whitespace()) { Ok(cli) => println!(" ok: {cli:?}"), Err(err) => { // Only show the first line of clap's message. let first_line = err.to_string().lines().next().unwrap_or("").to_string(); println!(" {:?}: {first_line}", err.kind()); } }}
fn main() { // Panics if the parser itself is set up wrong (for example, two // options with the same short name). Cheap insurance. Cli::command().debug_assert();
check("convert temp -40 -f c -t fahrenheit"); check("convert length 5 km mi -p 1"); check("convert temp 20 --from rankine --to c"); check("convert -p 9 length 1 m cm"); check("convert temp 10 --to c"); check("convert --version");}Output:
$ convert temp -40 -f c -t fahrenheit ok: Cli { precision: 2, command: Temp { value: -40.0, from: Celsius, to: Fahrenheit } }$ convert length 5 km mi -p 1 ok: Cli { precision: 1, command: Length { value: 5.0, from: Km, to: Mi } }$ convert temp 20 --from rankine --to c InvalidValue: error: invalid value 'rankine' for '--from <FROM>'$ convert -p 9 length 1 m cm ValueValidation: error: invalid value '9' for '--precision <PRECISION>': 9 is not in 0..=6$ convert temp 10 --to c MissingRequiredArgument: error: the following required arguments were not provided:$ convert --version DisplayVersion: convert 1.0Notice that --version comes back as an Err too. To clap, “show the version and stop” isn’t a successful parse, so it’s reported with the kind DisplayVersion (and --help with DisplayHelp). Cli::parse() handles those for you and exits with code 0.
In your real project, the same checks go into tests. Add this to the bottom of src/main.rs:
#[cfg(test)]mod tests { use super::*; use clap::CommandFactory;
#[test] fn cli_is_set_up_correctly() { Cli::command().debug_assert(); }
#[test] fn parses_a_temperature() { let cli = Cli::try_parse_from(["convert", "temp", "-40", "-f", "c", "-t", "f"]).unwrap(); assert_eq!(cli.precision, 2); match cli.command { Command::Temp { value, from, to } => { assert_eq!(value, -40.0); assert_eq!(from, TempUnit::Celsius); assert_eq!(to, TempUnit::Fahrenheit); } other => panic!("expected temp, got {other:?}"), } }
#[test] fn rejects_an_unknown_unit() { let result = Cli::try_parse_from(["convert", "length", "3", "yd", "m"]); let err = result.unwrap_err(); assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue); }
#[test] fn minus_forty_is_the_same_in_both() { let f = convert_temp(-40.0, TempUnit::Celsius, TempUnit::Fahrenheit).unwrap(); assert_eq!(f, -40.0); }}$ cargo test -q
running 4 tests....test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sCommon mistakes
Section titled “Common mistakes”Forgetting the derive feature. If you write cargo add clap without --features derive, the derive macros don’t exist:
error: cannot find derive macro `Parser` in this scope --> src/main.rs:4:10 |4 | #[derive(Parser, Debug)] | ^^^^^^ |note: `Parser` is imported here, but it is only a trait, without a derive macro --> src/main.rs:1:5 |1 | use clap::Parser; | ^^^^^^^^^^^^
error: cannot find attribute `command` in this scopeFix: run cargo add clap --features derive, or add features = ["derive"] in Cargo.toml by hand.
Negative numbers look like options. With the Step 2 program, try a cold day:
$ cargo run -q -- -40error: unexpected argument '-4' found
tip: to pass '-4' as a value, use '-- -4'
Usage: convert [OPTIONS] <CELSIUS>
For more information, try '--help'.clap sees - and thinks -4 is a short option. Fix: add #[arg(allow_negative_numbers = true)] to the field, like we did in Step 5. (Users can also type -- -40; everything after a lone -- is treated as a plain value.)
Calling Cli::parse() in the Playground. The Playground runs your program with no arguments, so parse() prints an error (or, for a tool with subcommands, the help page) and exits with code 2 before your code does anything. Use Cli::parse_from([...]) or Cli::try_parse_from([...]) there, and in tests.
Printing errors with println!. Error messages belong on stderr (eprintln!), not stdout. If someone runs convert ... > result.txt, the errors still show up on screen instead of hiding in the file.
Ideas to extend it
Section titled “Ideas to extend it”- More units. Add yards (
yd) and nautical miles toLengthUnit. The compiler will point at everymatchyou need to update. - A
weightsubcommand for kilograms, pounds and ounces. - A
--jsonflag that prints the result as JSON using serde_json. - Read values from stdin when no value is given, so you can pipe numbers in:
echo 100 | convert temp -f c -t f. AnOption<f64>field tells you whether a value was typed. - Better errors with anyhow. Change
runto returnanyhow::Result<String>and useanyhow::bail!. See anyhow and thiserror. - Shell completions. The clap_complete crate generates tab-completion scripts for bash, zsh, fish and PowerShell from your
Clistruct.
- Add clap with
cargo add clap --features derive, then put#[derive(Parser)]on a struct. Each field is one argument. - Plain fields are positional,
#[arg(short, long)]makes options,boolmakes flags, anddefault_value_tsets defaults.///doc comments become the--helptext. #[derive(Subcommand)]on an enum gives you subcommands;#[derive(ValueEnum)]limits an argument to a fixed set of words.value_parser!(...).range(...)checks number ranges.- After parsing succeeds, the data is already valid. Put the remaining checks in a
runfunction that returnsResult, and return anExitCodefrommain. - clap exits with code 2 for usage errors and 0 for
--help/--version. Your own failures should use a non-zero code too, and print to stderr. Cli::try_parse_fromparses a list you choose and returns aResult— perfect for tests and the Playground.
Quick check
Section titled “Quick check”Quick check
1. Which cargo add command enables #[derive(Parser)]?
2. Where does clap get the description of each option in --help?
3. What does Cli::try_parse_from(["convert", "--version"]) return?
4. A user types an invalid unit and Cli::parse() fails. Which exit code does clap use?
5. Why does convert -40 fail in the Step 2 program?
Score: 0 / 5
Read more
Section titled “Read more”- clap on docs.rs — the full API documentation, with examples for every attribute.
- The clap derive tutorial — a step-by-step official tutorial covering arguments, options, subcommands and validation.
- The clap derive reference — every
#[command(...)],#[arg(...)]and#[value(...)]option explained. - Command Line Applications in Rust — a free book on building real CLI tools: output, errors, testing and packaging.
- Exit codes — Command Line Applications in Rust — what exit codes mean and how to use them well.