Learn how to harness the power of the Rust programming language with clap, a command-line argument parser that brings efficiency and clarity to CLI development. This blog delves into creating versatile and structured CLI tools with clap, covering topics from basic setup to advanced features like optional parameters, flags, and subcommands, enabling developers to build robust command-line applications with ease.
The best way to use command line arguments in Rust beyond the absolute most basic implementation is using clap
- command line argument parser. With clap
we will be able to parse command line arguments into a strongly typed Rust structure.
clap
comes with two different modes. A derive mode which allows us to define the CLI structure with macros, or a builder version which allows us to define it with a builder function. In this tutorial, we’ll be using the derive version. Therefore, add clap
to your Rust project with
$ cargo add clap --features derive
Let’s start with the most basic implementation. To do that we’ll begin with a struct
called Cli
and annotating it with Parser
.
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 name: String,
7}
8
9fn main() {
10 let cli = Cli::parse();
11
12 println!("Name: {}", cli.name);
13}
14
Now try running with cargo run -- <your name>
. You should see Name: <your name>
be printed out. Quite nifty - that was really easy! Also notice how we derived version
and about
? Try running with cargo run -- --help
or cargo run -- --version
. We already have docs written.
This is pretty good already, but let’s try pushing it further. Let’s add a second parameter.
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 name: String,
7 city: String,
8}
9
10fn main() {
11 let cli = Cli::parse();
12
13 println!("Name: {} | City: {}", cli.name, cli.city);
14}
Now when we run with cargo run -- <your name> <your city>
we see Name: <your name> | City: <your city>
. This is okay but what if we forget which way around it goes? If we instead ran cargo run -- <your city> <your name>
by mistake we’d see Name: <your city> | City: <your name>
! There would be no way to catch this error! Can we do anything about this?
Absolutely, and it’s very simple too. Just derive arg
on the fields
1#[derive(Parser)]
2#[command(version, about, long_about = None)]
3struct Cli {
4 #[arg(short, long)]
5 name: String,
6 #[arg(short, long)]
7 city: String,
8}
Now we can run with cargo run -- --name <your name> --city <your city>
(the long
version) or cargo run -- -n <your name> -c <your city>
(the short
version). This really helps users out with remembering what goes where.
clap
will handle documentation for us. This allows us to tell users of the program about the parameter and how we expect it to be used. All we have to do is give the fields docs with Rust’s triple forward slash comments like so
1#[derive(Parser)]
2#[command(version, about, long_about = None)]
3struct Cli {
4 /// Your name
5 #[arg(short, long)]
6 name: String,
7 /// The city you live in
8 #[arg(short, long)]
9 city: String,
10}
Try running with cargo run -- -h
now and see how much more useful the help is.
For the purpose of economy, for the rest of this article, these documentation comments will be left out, but know you can always add them later to your own program.
Adding optional parameters in clap
is extremely easy - simply make the type Option
! Thanks to Rust’s robust type system this just works. For example
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10}
11
12fn main() {
13 let cli = Cli::parse();
14
15 if cli.city.is_some() {
16 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
17 } else {
18 println!("Name: {}", cli.name);
19 }
20}
Now when we run with cargo run -- -n <your name>
we get Name: <your name>
and if we run with cargp run -- -n <your name> -c <your city>
we get Name: <your name> | City: <your city>
Sometimes you can have conflicts. One current downside of clap
is these conflicts only reveal themselves at runtime not at compile time. So it’s important to look out for them! Let’s see an example
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10 #[arg(short, long)]
11 note: Option<String>,
12}
13
14fn main() {
15 let cli = Cli::parse();
16
17 if cli.city.is_some() {
18 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
19 } else {
20 println!("Name: {}", cli.name);
21 }
22}
What happens if we run this with cargo run -- --help
? Unfortunately we get a panic
Command clap_test: Short option names must be unique for each argument, but '-n' is in use by both 'name' and 'note'
There is a way around this though. Obviously we can pick words that don’t have conflicting first letters, but if they do, we can customise it in the macro. Consider this
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10 #[arg(short = 'o', long)]
11 note: Option<String>,
12}
13
14fn main() {
15 let cli = Cli::parse();
16
17 if cli.city.is_some() {
18 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
19 } else {
20 println!("Name: {}", cli.name);
21 }
22
23 if cli.note.is_some() {
24 println!("Notes: {}", cli.note.as_ref().unwrap());
25 }
26}
Now we are using -o
for note
and -n
for name
.
Any good CLI parser should support flags which take no arguments, and again, clap
makes this very easy. Let’s add a verbosity flag
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10 #[arg(short = 'o', long)]
11 note: Option<String>,
12 #[arg(short, long)]
13 verbose: bool,
14}
15
16fn main() {
17 let cli = Cli::parse();
18
19 if cli.verbose {
20 println!("Verbose mode enabled");
21 }
22
23 if cli.city.is_some() {
24 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
25 } else {
26 println!("Name: {}", cli.name);
27 }
28
29 if cli.note.is_some() {
30 println!("Notes: {}", cli.note.as_ref().unwrap());
31 }
32}
33
That’s literally it. The bool
will be false if not present, or true
if it was specified.
Enum
We can also use enum
with clap
arguments. Let’s say we want to customise the verbosity even more than just on or off - we want to have three levels: high, medium and low. A perfect use case for an enum
1use clap::{Parser, ValueEnum};
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10 #[arg(short, long, value_enum)]
11 verbosity: Verbosity,
12}
13
14#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
15enum Verbosity {
16 High,
17 Medium,
18 Low,
19}
20
21fn main() {
22 let cli = Cli::parse();
23
24 println!("Running with verbosity level: {:?}", cli.verbosity);
25
26 if cli.city.is_some() {
27 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
28 } else {
29 println!("Name: {}", cli.name);
30 }
31}
You can try running this program with cargo run -- -n <your name> -v low
.
Verbosity is the kind of thing we might like to have be optional in the CLI and just use a default value for though. One option would be to make it optional and then in the code check if is it Some
or None
. But that does complicate the code - if we want to make the default low then low is the same as None
. A little messy. Instead, let’s use optional values. We first derive default
for Verbosity
and then annotate it with default_value_t
.
1use clap::{Parser, ValueEnum};
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10 #[arg(short, long, value_enum, default_value_t)]
11 verbosity: Verbosity,
12}
13
14#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
15enum Verbosity {
16 High,
17 Medium,
18 Low,
19}
20
21impl Default for Verbosity {
22 fn default() -> Self {
23 return Self::Low;
24 }
25}
26
27fn main() {
28 let cli = Cli::parse();
29
30 println!("Running with verbosity level: {:?}", cli.verbosity);
31
32 if cli.city.is_some() {
33 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
34 } else {
35 println!("Name: {}", cli.name);
36 }
37}
Now if we run with cargo run -- -n <your name>
we see the verbosity level was set to low.
Sometimes we don’t want multiple flags to be present at the same time. In this example well allow the users to supply either an age or an email, but not both. For that, we can use the conflicts_with
parameter
1use clap::Parser;
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[arg(short, long)]
9 city: Option<String>,
10 #[arg(short = 'o', long)]
11 note: Option<String>,
12 #[arg(short, long)]
13 verbose: bool,
14 #[arg(short, long, conflicts_with = "email")]
15 age: Option<i32>,
16 #[arg(short, long, conflicts_with = "age")]
17 email: Option<String>,
18}
19
20fn main() {
21 let cli = Cli::parse();
22
23 if cli.verbose {
24 println!("Verbose mode enabled");
25 }
26
27 if cli.city.is_some() {
28 println!("Name: {} | City: {}", cli.name, cli.city.as_ref().unwrap());
29 } else {
30 println!("Name: {}", cli.name);
31 }
32
33 if cli.note.is_some() {
34 println!("Notes: {}", cli.note.as_ref().unwrap());
35 }
36
37 if cli.age.is_some() {
38 println!("Your age is: {}", cli.age.as_ref().unwrap());
39 } else {
40 // Given the program structure, this can never fail
41 // but in a real application, you should probably implement better error handling
42 println!("Your email is: {}", cli.email.as_ref().unwrap());
43 }
44}
45
If you play around with this, you’ll see that the user can supply an email or an age, but not both. However, they can also supply neither of them. What if we want to ensure that they supply at least one? In this case we can use Args and flatten those Args.
1use clap::{Args, Parser};
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[arg(short, long)]
7 name: String,
8 #[command(flatten)]
9 args: Details,
10}
11
12#[derive(Args)]
13#[group(required = true, multiple = false)]
14struct Details {
15 #[arg(short, long)]
16 age: Option<i32>,
17 #[arg(short, long)]
18 email: Option<String>,
19}
20
21fn main() {
22 let cli = Cli::parse();
23
24 println!("Your name is: {}", cli.name);
25
26 if cli.args.age.is_some() {
27 println!("Your age is: {}", cli.args.age.as_ref().unwrap());
28 } else {
29 println!("Your email is: {}", cli.args.email.as_ref().unwrap());
30 }
31}
32
Subcommands allow us to make much more complex command line arguments. Let’s say we’re making a command line argument tool that allows us to make users, or make companies in some database. By using subcommands with an enum
we can make sure that only the user attributes are present in user mode, and only company attributes are present in company mode.
Let’s say that users need a name and an email, and companies need a sector and a tax ID. We can model this like so
1use clap::{Parser, Subcommand};
2
3#[derive(Parser)]
4#[command(version, about, long_about = None)]
5struct Cli {
6 #[command(subcommand)]
7 command: Commands,
8}
9
10#[derive(Subcommand)]
11enum Commands {
12 #[command(name = "--user")]
13 User {
14 #[arg(short, long)]
15 name: String,
16 #[arg(short, long)]
17 email: String,
18 },
19 #[command(name = "--company")]
20 Company {
21 #[arg(short, long)]
22 sector: String,
23 #[arg(short, long = "tax-id")]
24 tax_id: String,
25 },
26}
27
28fn main() {
29 let cli = Cli::parse();
30
31 match &cli.command {
32 Commands::User { name, email } => {
33 println!("Made user with Name: {}, Email: {}", name, email)
34 }
35 Commands::Company { sector, tax_id } => {
36 println!("Made company with Sector: {}, Tax ID: {}", sector, tax_id)
37 }
38 };
39}
40
Try running this program with cargo run -- --company -s software --tax-id AA123
to see how it works, also try running it with cargo run -- --company -s software --tax-id AA123 -n Bob
. You’ll see how clap
keeps the different arguments separated for us.
As you can see, working with clap
is really straightforward. We can easily make strongly typed CLI arguments with a pretty straightforward syntax. clap
has loads of features, more than I can cover in this short article, but you can always find more extensive docs here.