Introduction to Command Line Arguments in Rust

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.

Indigo Curnick
October 28, 2024
Articles

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.

Named Parameters

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.

Auto-Documentation

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.

Optional Parameters

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>

Derive Conflicts

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.

Flags

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.

Using 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.

Conflicting Flags

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

Using Subcommands

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.

Subscribe To Our Newsletter - Sleek X Webflow Template

Subscribe to our newsletter

Sign up at Naurt for product updates, and stay in the loop!

Thanks for subscribing to our newsletter
Oops! Something went wrong while submitting the form.