CLI - Command line Interface with clap
In this book we are going to learn how to use Clap, the Command Line Argument Parser for Rust.
There are two main ways to use Clap. Using the manual Builder and the Derive mode. In these example we'll learn about using the Builder
First example: One parameter
In order to get started we need to add clap to the depenedencies. We can do so either by running cargo add clap
or by editing the Cargo.toml
file
manually adding the [dependencies]
section if it isn't there and the row with clap
.
The cargo add clap
command will use the latest version number of clap. By the time your read this book this number will probably have a higher value
than what you see here.
This is the Cargo.toml
in my example:
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = "4.5.29"
use clap::{Arg, Command}; fn main() { let command = Command::new("My app") .arg(Arg::new("name")); let matches = command.get_matches(); match matches.get_one::<String>("name") { Some(name) => println!("name: {:?}", name), None => println!("No name provided"), } }
$ cargo run -q
No name provided
$ cargo run -q Foo
name: "Foo"
$ cargo run -q "Foo Bar"
name: "Foo Bar"
cargo build --release
$ ./target/release/demo
No name provided
$ ./target/release/demo Foo
name: "Foo"
If the user does not understand what parameters are expected by this program she can run the command proving the --help
flag or
the shorter -h
flag and Clap will print some explanation on how to use this command line tool:
$ ./target/release/demo --help
Usage: demo [name]
Arguments:
[name]
Options:
-h, --help Print help
We can achieve the same even without using the compiled binary using cargo build
, but in that case we need to supply the --help
flag
after a --
.
cargo run -q -- --help
The above might look strange, but if forget to proveide the --
separator and we write this:
cargo run -q --help
then we'll see the help of the cargo command itself. That was not our intention.
Positional parameters
use clap::{Arg, Command}; fn main() { let command = Command::new("My app") .arg(Arg::new("fname")) .arg(Arg::new("lname")); let matches = command.get_matches(); match matches.get_one::<String>("fname") { Some(name) => println!("fname: {:?}", name), None => println!("No fname provided"), } match matches.get_one::<String>("lname") { Some(name) => println!("lname: {:?}", name), None => println!("No lname provided"), } }
Named parameter
use clap::{Arg, Command}; fn main() { let command = Command::new("My app") .arg(Arg::new("fname").long("fanme")) .arg(Arg::new("lname").long("lanme")); let matches = command.get_matches(); match matches.get_one::<String>("fname") { Some(name) => println!("fname: {:?}", name), None => println!("No fname provided"), } match matches.get_one::<String>("lname") { Some(name) => println!("lname: {:?}", name), None => println!("No lname provided"), } }
Clap - Command Line Argument Parser
-
Start by using
std::env::args
-
clap is the Command Line Argument Parser
-
Derive
-
Builder
-
Extension called clap_complete.
Clap Single positional argument
cargo add clap --features derive
[package]
name = "single-positional-argument"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
use clap::Parser; #[derive(Parser)] struct Cli { filename: String, } fn main() { let args = Cli::parse(); println!("filename: {}", args.filename); }
Clap Several positional arguments
use clap::Parser; #[derive(Parser)] struct Cli { host: String, #[arg(required = true)] files: Vec<String>, // Probably not a good idea to put a one-value item after a vector of items //test: String, //test: Vec<String>, } fn main() { let args = Cli::parse(); println!("host: {}", args.host); println!("files: {:?}", args.files); //println!("test: {}", args.test); }
Clap Single long argument
- arg
- long
[package]
name = "single-long-argument"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
use clap::Parser; #[derive(Parser)] struct Cli { #[arg(long)] host: String, } fn main() { let args = Cli::parse(); println!("host: {}", args.host); }
Clap Short arguments
-
short
-
We can allow to use
-x
short names (defaults to the first letter of the attribute) -
We can set the letter ourselves.
use clap::Parser; #[derive(Parser)] struct Cli { #[arg(short)] debug: bool, #[arg(short = 'H')] host: String, } fn main() { let args = Cli::parse(); println!("debug: {}", args.debug); println!("host: {}", args.host); }
Clap accepting string, number, bool - default values
- default_value
- default_value_t
use clap::Parser; #[derive(Parser)] struct Cli { #[arg(long, default_value = "127.0.0.1")] host: String, #[arg(long, default_value_t = 5000)] port: i32, #[arg(long, default_value_t = 0)] small: u8, #[arg(long, default_value_t = 0.0)] float: f32, #[arg(long, default_value_t = false)] debug: bool, #[arg(long, default_value = ".")] path: std::path::PathBuf, } fn main() { let args = Cli::parse(); println!("host: {}", args.host); println!("port {}", args.port); println!("small: {}", args.small); println!("float: {}", args.float); println!("debug: {}", args.debug); println!("path: {}", args.path.display()); // `PathBuf` cannot be formatted with the default formatter; call `.display()` on it }
Clap add help to each argument
use clap::Parser; #[derive(Parser)] struct Cli { #[arg(long, help = "The name of the test")] test: String, #[arg(long, default_value = "127.0.0.1", help = "The name of the host")] host: String, #[arg(long, help = "Debug our code")] debug: bool, } fn main() { let args = Cli::parse(); println!("host: {}", args.host); println!("test: {}", args.test); println!("debug: {}", args.debug); }
Clap show the version number from Cargo.toml
- version
use clap::Parser; #[derive(Parser)] #[command(version)] struct Cli {} fn main() { let _args = Cli::parse(); println!("Hello World!"); }
Clap Show the description of the crates using the about command
- description
- about
- command
[package]
name = "show-description"
version = "0.1.0"
edition = "2021"
description = "This is the description field of Cargo.toml"
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
use clap::Parser; #[derive(Parser)] #[command(about)] struct Cli {} fn main() { let _args = Cli::parse(); println!("Use the --help to see the description"); }
Clap Show the description written in the code
[package]
name = "show-description-from-code"
version = "0.1.0"
edition = "2021"
description = "This is the description in Cargo.toml"
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
use clap::Parser; #[derive(Parser)] #[command(about = "Hello from the code")] struct Cli {} fn main() { let _args = Cli::parse(); println!("Use --help to show the description from the code"); }
Clap show generated description
[package]
name = "show-generated-description"
version = "0.1.0"
edition = "2021"
description = "Description in Cargo.toml"
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
use clap::Parser; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Parser)] #[command(about = get_about())] struct Cli {} fn main() { let _args = Cli::parse(); println!("Use the --help flag"); } fn get_about() -> String { let start = SystemTime::now(); let since_the_epoch = start .duration_since(UNIX_EPOCH) .expect("Time went backwards"); format!( "The description generated at {}", since_the_epoch.as_nanos() ) }
Clap validate number range
- value_parser!
use clap::Parser; #[derive(Parser)] struct Cli { #[arg(long, value_parser = clap::value_parser!(u16).range(1024..10000))] port: u16, } fn main() { let cli = Cli::parse(); println!("PORT = {}", cli.port); }
Clap subcommands
use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] struct Cli { #[arg(long, default_value = ".")] root: PathBuf, #[command(subcommand)] command: Option<Commands>, } #[derive(Subcommand)] enum Commands { Web { #[arg(long)] outdir: PathBuf, }, Email { #[arg(long)] to: String, }, } fn main() { let args = Cli::parse(); println!("root: {:?}", args.root); match &args.command { Some(Commands::Web { outdir }) => { println!("outdir: {:?}", outdir); } Some(Commands::Email { to }) => { println!("to: {}", to); } None => { println!("There was no subcommand given"); } } }
Clap mutually exclusive
use clap::Parser; #[derive(Parser)] struct Cli { #[arg(long, group = "action")] more: bool, #[arg(long, group = "action")] less: bool, } fn main() { let cli = Cli::parse(); println!("more: {}", cli.more); println!("less: {}", cli.less); }
Clap mutually exclusive with group
use clap::{Args, Parser}; #[derive(Parser)] pub struct Cli { #[clap(flatten)] group: Group, } #[derive(Args)] #[group(required = true, multiple = false)] pub struct Group { #[clap(long)] more: bool, #[clap(long)] less: bool, } fn main() { let args = Cli::parse(); println!("less: {}", args.group.less); println!("more: {}", args.group.more); }
Clap example for CLI
- CLI
[package]
name = "clap-example"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.3", features = ["derive"] }
use clap::Parser; #[derive(Parser, Debug)] #[command(version)] struct Cli { #[arg(long, default_value = "127.0.0.1")] host: String, #[arg(long, default_value = "5000")] port: String, #[arg(long, default_value = "")] indexfile: String, #[arg(long, default_value = ".")] path: std::path::PathBuf, #[arg(long, default_value_t = 0, help = "A number mapped to i32")] limit: i32, #[arg(long, default_value_t = 0, help = "A number mapped to u8")] small: u8, #[arg(long, short, default_value_t = false, help = "Turn on debug mode")] debug: bool, } fn main() { let args = Cli::parse(); dbg!(&args.host); dbg!(&args); if args.debug { println!("In debug mode"); } }
Clap: improving the help with value_name
- value_name
By default the help (generated by providing -h
) includes the name of the field in angle brackets.
This is usually a repetition of the parameter name in upper case letters.
In many cases this gives good indication of the value the program is expecting, but sometimes you might want to give a better hint.
In such cases you can set the string that will appear inside the angle brackets using the value_name
parameter.
- In this example the input field has the default:
INPUT
while the out field was set toFILENAME
by thevalue_field.
use clap::Parser; #[derive(Parser, Debug)] struct Cli { #[arg(long)] input: String, #[arg(long, value_name = "FILENAME")] output: String, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
Usage: value-name --input <INPUT> --output <FILENAME>
Options:
--input <INPUT>
--output <FILENAME>
-h, --help Print help
Clap: one of a fixed list of values (enumerated)
- enum
- ValueEnum
Using Clap we can define a parameter to accept only values from a fixed list of possible values.
For this we create an enum
where all variants of the enum correspond to the values we would like to accept.
The enum
has to derive from clap::ValueEnum
and also from Clone
and Copy
.
We can also use the alias
to allow alternate values to be mapped onto specific variants of the enum.
use clap::Parser; use clap::ValueEnum; #[derive(Debug, Clone, Copy, ValueEnum)] enum Color { Red, #[value(alias = "Green", alias = "verde")] Green, Blue, } #[derive(Parser, Debug)] struct Cli { #[arg(long)] color: Color, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
cargo run -q -- --color blue
cargo run -q -- --color red
cargo run -q -- --color green
cargo run -q -- --color Green
cargo run -q -- --color verde
Clap and environment variables
- env
In order for this feature to work we need to enable both the derive
and the env
features of Clap so we start with:
cargo add clap -F derive -F env
If we don't enable the env
feature we'll get an error: no method named env
found for struct Arg
in the current scope with method not found in Arg
.
use clap::Parser; //use clap::ArgAction; //use clap::{builder::ArgPredicate, ArgAction, ValueEnum}; #[derive(Parser, Debug)] struct Cli { #[arg(long, env = "DEMO_HOSTNAME")] hostname: String, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
In this case we can pass the hostname either as a command line flag or by setting the appropriate environmnet variable.
cargo run -- --hostname localhost
DEMO_HOSTNAME=localhost cargo run
Setting the envronment can be done on the same row (on linux and macOS) as we see in our example, but it can be also set earlier.
Clap - set default value if other flag provided
-
default_value_if
-
ArgPredicate
-
Equals
-
OsStr
-
If
log_to_file
is true thenlog_file
will get a default value. -
Actually I am not sure this is such a good example for
default_value_if
as setting the default even if thelog_to_file
isFalse
would not be a problem. The code just would not use it.
use clap::builder::{ArgPredicate, OsStr}; use clap::Parser; #[derive(Debug, Parser)] struct Cli { #[arg(long)] log_to_file: bool, #[arg( long, default_value_if("log_to_file", ArgPredicate::Equals(OsStr::from("true")), "my.log") )] log_file: Option<String>, } fn main() { let args = Cli::parse(); println!("Args: {args:?}"); }
$ cargo run -q
Args: Cli { log_to_file: false, log_file: None }
$ cargo run -q -- --log-to-file
Args: Cli { log_to_file: true, log_file: Some("my.log") }
$ cargo run -q -- --log-to-file --log-file other.log
Args: Cli { log_to_file: true, log_file: Some("other.log") }
$ cargo run -q -- --log-file other.log
Args: Cli { log_to_file: false, log_file: Some("other.log") }
Clap - set default value if other argument provided
-
default_value_if
-
ArgPredicate
-
IsPresent
-
This is a similar example but now we say that if the user provides a name for the logfile then we'll automatically turn on logging to that file.
-
This might be more logical than the previous one, but I am not sure I like this either.
use clap::builder::ArgPredicate; use clap::Parser; #[derive(Debug, Parser)] struct Cli { #[arg(long, default_value_if("log_file", ArgPredicate::IsPresent, "true"))] log_to_file: bool, #[arg(long, default_value = "my.log")] log_file: String, } fn main() { let args = Cli::parse(); println!("Args: {args:?}"); }
$ cargo run -q
Args: Cli { log_to_file: false, log_file: "my.log" }
$ cargo run -q -- --log-to-file
Args: Cli { log_to_file: true, log_file: "my.log" }
$ cargo run -q -- --log-file other.log
Args: Cli { log_to_file: true, log_file: "other.log" }
$ cargo run -q -- --log-to-file --log-file other.log
Args: Cli { log_to_file: true, log_file: "other.log" }
Clap - set default value based on another flag
mode
is required argument.- The value of
log_level
is based on themode
. - We can also set the
log_level
manually.
$ cargo run -q -- --mode devel
Args: Cli { mode: Devel, log_level: 1 }
$ cargo run -q -- --mode test
Args: Cli { mode: Test, log_level: 2 }
$ cargo run -q -- --mode release
Args: Cli { mode: Release, log_level: 0 }
$ cargo run -q -- --mode release --log-level 10
Args: Cli { mode: Release, log_level: 10 }
use clap::builder::{ArgPredicate, OsStr}; use clap::Parser; use clap::ValueEnum; #[derive(Debug, Clone, Copy, ValueEnum)] enum Mode { Devel, Test, Release, } #[derive(Debug, Parser)] struct Cli { #[arg(long)] mode: Mode, #[arg( long, default_value_if("mode", ArgPredicate::Equals(OsStr::from("devel")), "1"), default_value_if("mode", ArgPredicate::Equals(OsStr::from("test")), "2"), default_value_t = 0 )] log_level: u8, } fn main() { let args = Cli::parse(); println!("Args: {args:?}"); }
Clap complete enum - name of shell
-
clap_complete
-
Shell
use clap::{value_parser, Parser}; use clap_complete::Shell; #[derive(Parser, Debug)] struct Cli { #[clap(long, value_parser=value_parser!(Shell))] shell: Shell, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
Repeat the same argument several times
-
Vec
-
required
-
If we set the element of the struct to be a vector then Clap will allow us to supply the same argument several times.
-
As a vector can also be emptty this will also work if we don't provide any
--animal
arguments. -
We could also include
required=true
to require at least one value.
use clap::Parser; #[derive(Parser, Debug)] struct Cli { #[arg(long)] animal: Vec<String>, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
$ cargo run -q
Cli { animal: [] }
$ cargo run -q -- --animal Cat
Cli { animal: ["Cat"] }
$ cargo run -q -- --animal Cat Dog
error: unexpected argument 'Dog' found
Usage: repeat-the-same-argument [OPTIONS]
For more information, try '--help'.
$ cargo run -q -- --animal Cat --animal Dog
Cli { animal: ["Cat", "Dog"] }
Limit the number of values for a vector argument
-
num_args
-
Vec
-
required
-
A different approach is to accept multiple values with a single mention of the argument. We can achieve this by setting the
num_args
. -
It can accept either a single number or a range such as
2..=3
(2 or 3),..=3
(same as 0..=3), or3..
(3 or more). -
We can also supply
required=true
to make sure the argument must be supplied. -
See num_args
use clap::Parser; #[derive(Parser, Debug)] struct Cli { #[arg(long, num_args=2..=3, required=true)] animal: Vec<String>, #[arg(long, num_args = 3)] sisters: Vec<String>, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
$ cargo run -q
error: the following required arguments were not provided:
--animal <ANIMAL> <ANIMAL>...
Usage: limit-number-of-args --animal <ANIMAL> <ANIMAL>...
For more information, try '--help'.
$ cargo run -q -- --animal Cat Dog
Cli { animal: ["Cat", "Dog"], sisters: [] }
$ cargo run -q -- --animal Cat Dog Crab
Cli { animal: ["Cat", "Dog", "Crab"], sisters: [] }
$ cargo run -q -- --animal Cat Dog Crab Snake
error: unexpected argument 'Snake' found
Usage: limit-number-of-args [OPTIONS] --animal <ANIMAL> <ANIMAL>...
For more information, try '--help'.
Clap: fixed list of valid values for generic type
-
value_parser
-
Besides creating an enum we could also just list the possible value of a generic type using the value_parse option.
use clap::Parser; #[derive(Parser, Debug)] struct Cli { #[arg(long, value_parser = ["cat", "dog", "crab"])] animal: String, } fn main() { let args = Cli::parse(); println!("{args:?}"); }
$ cargo run -q -- -h
Usage: value-parser-fixed-list --animal <ANIMAL>
Options:
--animal <ANIMAL> [possible values: cat, dog, crab]
-h, --help Print help
$ cargo run -q
error: the following required arguments were not provided:
--animal <ANIMAL>
Usage: value-parser-fixed-list --animal <ANIMAL>
For more information, try '--help'.
$ cargo run -q -- --animal cat
Cli { animal: "cat" }
$ cargo run -q -- --animal snake
error: invalid value 'snake' for '--animal <ANIMAL>'
[possible values: cat, dog, crab]
For more information, try '--help'.
Clap: arbitraty function to validate argument
- value_parser
use clap::Parser; use std::error::Error; #[derive(Parser, Debug)] struct Cli { #[arg(long, value_parser = parse_even_number)] number: u32, } fn parse_even_number(number: &str) -> Result<u32, Box<dyn Error + Send + Sync + 'static>> { let number = number.parse::<u32>()?; if number % 2 == 0 { return Ok(number); } Err(Box::<dyn Error + Send + Sync>::from( "An even number is expected", )) } fn main() { let args = Cli::parse(); println!("{args:?}"); }
$ cargo run -q -- --number 22
Cli { number: 22 }
$ cargo run -q -- --number 23
error: invalid value '23' for '--number <NUMBER>': An even number is expected
For more information, try '--help'.
$ cargo run -q -- --number ad
error: invalid value 'ad' for '--number <NUMBER>': invalid digit found in string
For more information, try '--help'.
Clap: default value only if the flag was provides
- TODO
./program --input 100 # set input to Some(100)
./program --input # set input to the dafult Some(42)
./program # input is None
<!-- mdbook-embedify [footer] -->
<style> footer { text-align: center; text-wrap: balance; margin-top: 5rem; display: flex; flex-direction: column; justify-content: center; align-items: center; } footer p { margin: 0; }</style><footer><p>Copyright © 2025 • Created with ❤️ by <a href="https://szabgab.com/">Gábor Szabó</a></p></footer>
Exercise: implement accepting the parameters of wc
You can get it either by typing "man wc" in your terminal or by visiting man wc.
Solution: implement accepting the parameters of wc
- TODO
use clap::Parser; // enum Total { // Auto, // Always, // Only, // Ever // } #[derive(Parser)] #[command(version)] struct Cli { #[arg(long, short = 'c')] bytes: bool, #[arg(long, short = 'm')] chars: bool, #[arg(long, short)] lines: bool, // --files0-from automatically mapped to this // We can use both // --files0-from file.txt // and // --files0-from=file.txt #[arg(long)] //, alias="files0-from")] files0_from: Option<String>, // --max-line-length is automatically mapped to this #[arg(long, short = 'L')] max_line_length: bool, #[arg(long, short)] words: bool, // #[arg(long)] // total: Total, } // TODO mutual exclusivity? fn main() { let args = Cli::parse(); if args.bytes { println!("print the byte counts"); } if args.chars { println!("print the character counts"); } if args.lines { println!("print the newline counts"); } if let Some(files0_from) = args.files0_from { if files0_from == "-" { println!("If F is - then read names from standard input"); } else { println!( "read input from the files specified by NUL-terminated names in file F={}", files0_from ); } } if args.max_line_length { println!("print the maximum display width"); } if args.words { println!("print the word counts"); } // --total=WHEN // when to print a line with total counts; WHEN can be: auto, always, only, never }