Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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 to FILENAME by the value_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 then log_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 the log_to_file is False would not be a problem. The code just would not use it.

  • ArgPredicate

  • default_value_if

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 the mode.
  • 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

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), or 3.. (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
}