Rust Clap journey – a little further

At Qxf2 we are always on the lookout to go wider and deeper into evolving our technical toolbelt. One such initiative was to explore popular Rust crates and to come up with code samples showing implementation of important features. Clap is a popular crate in Rust, that aids in developing CLI applications seamlessly. In our opening blog, we gave you a soft introduction into creating your first CLI app using Clap, arguments grouping and finally parsing them for further processing. Let us take the Rust Clap journey – a little further. In this blog, we will help you dive further into Clap, by showing you Clap’s implementation of optional arguments, subcommands, and more.


Configuring short and long versions of command-line arguments

First, let’s look at how we can configure the long and short versions of command line arguments.
short_long_args.rs

use clap::Parser;
///Fetch login details
#[derive(Parser, Debug)]
struct Args {
    #[arg(short = 'u', long ="username")]
    user_name: String,
    #[arg(short = 'p', long = "password")]
    password: String,
}
fn main() {
    let args = Args::parse();
    println!("{:?}", args);
}

To start with, we have imported Clap’s Parser trait and then defined the command-line arguments using Clap’s Args struct. Clap’s documentation neatly explains Args as an abstract representation of a command line argument. It is used to set all the options and relationships that define a valid argument for the program.

Breaking down the code:

We have used Args to define two command-line arguments namely username and password.

#[arg(short = 'u', long ="username")]

#[arg] is an attribute from Clap that is used to provides metadata about how to handle command-line arguments. short and long is used to specify short and long versions of the argument. So now when we call the binary, we can use either `-u` or `--username` to pass value to username argument. By default, arguments defined are mandatory unless the default_value parameter is set, which we will discuss in a bit.

let args = Args::parse();
println!("{:?}", args);

Args::parse() is a method provided by the Parser trait to parse command-line arguments. We are then printing the Args struct fields with values to ensure they are parsed correctly.

Run code:
Executing short_long_args.rs
Executing short_long_args.rs

Defining mandatory and optional command-line arguments

Next, let’s focus on configuring mandatory and optional command-line arguments as they are vital for building any CLI app.
optional_args.rs

use clap::Parser;
use std::env;
///Login validation
#[derive(Parser, Debug)]
struct Args {
    /// User Name - optional argument
    #[arg(short = 'u', long ="username", default_value = "Qxf2")]
    user_name: String,
    /// Password - mandatory argument
    #[arg(short = 'p', long = "password", required = true)]
    password: String,
}
fn main() {
    let args = Args::parse();
    let expected_password = match env::var("APP_PASSWORD") {
        Ok(val) => val,
        Err(_) => {
            println!("Password not set as an environment variable!");
            return;
        }
    };
    if args.password == expected_password{
        println!("Welcome to {}!", args.user_name);
    }
    else {
        println!("Password mismatch for user: {}! \nLogin failed!", args.user_name);
    }
}
Breaking down the code:

We have extended the previous example. This time we’ve also defined mandatory and optional arguments and performed validation. “///” marks the start of documentation comments. Documentation comments make help feature of clap more customizable as well as improves code readability.

#[arg(short = 'u', long ="username", default_value = "Qxf2")]

Setting default_value parameter in #[arg] attribute makes a command-line argument optional. Argument “username” is an optional argument as it takes a default value. Whereas in the following code,

#[arg(short = 'p', long = "password", required = true)]

Argument “password” is a mandatory argument to be passed from the command-line.

let expected_password = match env::var("APP_PASSWORD") {
       Ok(val) => val,
       Err(_) => {
           println!("Password not set as an environment variable!");
           return;
       }

std::env is a module in Rust’s standard library (std) that has functions and types used to work with the environment variables. We are initializing string variable “expected_password” with the value defined in the environment variable “APP_PASSWORD”. If we have set “APP_PASSWORD” then “expected_password” will be initialized with that value. If not, it will be handled in the error block.

if args.password == expected_password{
        println!("Welcome to {}!", args.user_name);
    }
    else {
        println!("Password mismatch for user: {}! \nLogin failed!", args.user_name);
    }

We are then comparing password passed from command-line argument with that set in the environment variable and informing user of the validation outcome accordingly.

Run command and expected outcome:

Let us explore the help for this program:

$ cargo run --bin optional_args -- --help
Login validation
 
Usage: optional_args.exe [OPTIONS] --password <PASSWORD>
 
Options:
  -u, --username <USER_NAME>  User Name - optional argument [default: Qxf2]
  -p, --password <PASSWORD>   Password - mandatory argument
  -h, --help                  Print help

If we run this login validation code without setting environment variable “APP_PASSWORD”:

$ cargo run --bin optional_args -- --password 123
Password not set as an environment variable!

Let us set the environment variable “APP_PASSWORD”,

export APP_PASSWORD=TestersAreSharp#$2024

and run the login validation code:

$ cargo run --bin optional_args -- --password 123
Password mismatch for user: Qxf2!
Login failed!
$ cargo run --bin optional_args -- --password TestersAreSharp#$2024
Welcome to Qxf2!
$ cargo run --bin optional_args -- -u Qxf2 -p TestersAreSharp#$2024
Welcome to Qxf2!

Defining positional argument and boolean options

As we are aware, positional argument is a type of command-line argument that is identified based on its position after the command. For example, in the copy command `cp source.txt destination.txt`, source.txt and destination.txt are positional arguments indexed at position 1 and 2 respectively. Order is important for positional arguments and they do not require any flag or keyword. In the case of boolean options(otherwise called as flags), their presence would simply mean true. If not mentioned on the command line, it would mean they are ignored for that run. As flags, they are used for turning on or off certain features in a program. Let us see an example to implement positional arguments and boolean options.
positional_args_boolean_options.rs

use clap::{Command, Arg};
fn main(){
    // Build the CLI app using builder pattern
    let cmd = Command::new("NumberProgram")
    .version("1.0")
    .about("Program to show usage of positional argument and boolean option using clap")
    .arg(
        Arg::new("input_number")
        .help("a number")
        .index(1)
        .value_parser(clap::value_parser!(i32))
        .required(true)
    )
    .arg(
        Arg::new("square")
        .help("Boolean option to get the square of the input number")
        .short('s')
        .long("square")
        .required(false)
        .num_args(0)
    );
    // Runtime argument parsing
    let matches = cmd.get_matches();
    let input_number = matches.get_one("input_number");
    //Extract the value passed for the positional argument and display
    let number = match input_number {
        Some(input_number) => input_number,
        None => &0,
    };
    println!("Number passed via CLI : {} ", number);
    //If boolean option is passed then print square
    if let Some(true) = matches.get_one("square"){
        println!("Square of {} : {} ", number, number*number);
    }
}
Breaking down code:

We have constructed a command “NumberProgram” that takes two arguments, using the builder pattern of Clap.

.arg(
        Arg::new("input_number")
        .help("a number")
        .index(1)
        .value_parser(clap::value_parser!(i32))
        .required(true)
    )

First argument “input_number” is a positional argument which should be passed at index 1, accepts integer type and is a mandatory argument.

 .arg(
        Arg::new("square")
        .help("Boolean option to get the square of the input number")
        .short('s')
        .long("square")
        .required(false)
        .num_args(0)
    )

Second argument “square” is a flag or boolean option and this has been specified by mentioning required attribute as false. Also, the short and long versions of specifying this argument has been defined. num_args(0) would mean that this argument does not take any value. Rest of the code is simple. The positional arguments if integer, is displayed and If boolean option “-s” or “–square” is passed, then square of the number passed is displayed as well.

Run command and expected outcome:
$ cargo run --bin positional_args_boolean_options -- --help
Program to show usage of positional argument and boolean option using clap
 
Usage: positional_args_boolean_options.exe [OPTIONS] <input_number>
 
Arguments:
  <input_number>  a number
 
Options:
  -s, --square   Boolean option to get the square of the input number
  -h, --help     Print help
  -V, --version  Print version
$ cargo run --bin positional_args_boolean_options -- 5
Number passed via CLI : 5
$ cargo run --bin positional_args_boolean_options -- 5 --square
Number passed via CLI : 5
Square of 5 : 25

Implementing subcommands

Subcommands are keywords that are used to invoke a new set of options. It is one of the most important features of a command-line parser. Let us now see how to implement subcommands using Clap crate. We will be demonstrating subcommands taking Git operations as an example.
subcommands.rs

use clap::{Command, Arg};
fn main() {
    let app_m = Command::new("MyGitApp")
        .subcommand(Command::new("clone").about("Clone a repository").arg(
            Arg::new("name")
                .help("repo name")
                .short('n')
                .long("name")
                .num_args(1)
        ))
        .subcommand(Command::new("push").about("Push changes to a repository").arg(
            Arg::new("branch")
                .help("branch name")
                .short('b')
                .long("branch")
                .num_args(1)
        ))
        .subcommand(Command::new("commit").about("Commit changes to a repository").arg(
            Arg::new("message")
                .help("commit message")
                .short('m')
                .long("message")
                .num_args(1)
        ))
        .get_matches();
 
    match app_m.subcommand() {
        Some(("clone", sub_m)) => {
            // Handle clone subcommand
            if let Some(name) = sub_m.get_one::<String>("name") {
                println!("Cloning repository with name: {}", name);
            }
            else {
                println!("Please provide name of repository to clone");
            }
        }
        Some(("push", sub_m)) => {
            // Handle push subcommand
            if let Some(branch) = sub_m.get_one::<String>("branch") {
                println!("Pushing changes to branch: {}", branch);
            }
            else {
                println!("Please provide the branch to push");
            }
        }
        Some(("commit", sub_m)) => {
            // Handle commit subcommand
            if let Some(message) = sub_m.get_one::<String>("message") {
                println!("Committing changes with message: {}", message);
            }
            else {
                println!("Please provide a commit message");
            }
        }
        _ => {
            // Either no subcommand or invalid subcommand
            println!("Invalid subcommand or no subcommand provided");
        }
    }
}
Breaking down code:

We have constructed three subcommands for the main command “MyGitApp” using Clap’s builder pattern. The subcommands are “clone”, “push” and “commit” and they take one argument each. At any point in time, only one subcommand can be passed as an input argument from the command line. get_matches() method is used to parse the command-line arguments.

 match app_m.subcommand() {
        Some(("clone", sub_m)) => {
            // Handle clone subcommand
            if let Some(name) = sub_m.get_one::<String>("name") {
                println!("Cloning repository with name: {}", name);
            }
            else {
                println!("Please provide name of repository to clone");
            }
        }

We then use Rust’s match construct to compare and identify the chosen subcommand. If subcommand “clone” is passed from command-line, then it requires one mandatory argument to be passed as mandated by the num_args() attribute while constructing the subcommand. If no arguments are passed with “clone” subcommand then an invalid message is displayed.

Run command and expected outcome:
$ cargo run --bin subcommands -- -h
Usage: subcommands.exe [COMMAND]
 
Commands:
  clone   Clone a repository
  push    Push changes to a repository
  commit  Commit changes to a repository
  help    Print this message or the help of the given subcommand(s)
 
Options:
  -h, --help  Print help
$ cargo run --bin subcommands -- push --help
Push changes to a repository
 
Usage: subcommands.exe push [OPTIONS]
 
Options:
  -b, --branch <branch>  branch name
  -h, --help             Print help
$ cargo run --bin subcommands -- push -b fix_issue#45
Pushing changes to branch: fix_issue#45
$ cargo run --bin subcommands -- clone --name qxf2/qxf2-page-object-model
Cloning repository with name: qxf2/qxf2-page-object-model
$ cargo run --bin subcommands -- commit -m "Fixed issue#45"
Committing changes with message: Fixed issue#45

We have briefed you with examples highlighting the implementation of certain important features of Clap. We believe that we have taken your Rust Clap journey – a little further. Hope you found it useful and it gave you a head-start into implementing your CLI applications in Rust. Happy building and testing!


Hire Quality Engineers (QE) from Qxf2

Hire QEs from Qxf2 to experience a better form of testing. We employ experienced, technical and proactive testers. Most engineers we work with call us back when they switch jobs. They enjoy the fact that Qxf2 guides their testing efforts the right way instead of simply taking direction and executing surface level tests. As you can see from this post (and blog, really), we work on a number of technical techniques that help us understand the technology that our clients work with. This sort of insight leads to better testing and improved collaboration with developers. If you are looking for technical testers to join your team, get in touch.


One thought on “%1$s”

Leave a Reply

Your email address will not be published. Required fields are marked *