Writing Rust CLIs - Clap

Sangam BiradarSangam Biradar
10 min read

How echo works

purpose of this is to show you how to use arguments from the command line to change the behaviour of the program at runtime.

$ echo Hello
Hello

to start echo will prints its arguments to STDOUT

➜  rustlabs echo "welcome to  rustlabs "
welcome to  rustlabs 
➜  rustlabs echo welcome to  rustlabs  
welcome to rustlabs

if you want the spaces to be prevented I must enclose them in quotes.

man echo you will see more options around it .

ECHO(1)                                                          General Commands Manual                                                         ECHO(1)

NAME
     echo – write arguments to the standard output

SYNOPSIS
     echo [-n] [string ...]

DESCRIPTION
     The echo utility writes any specified operands, separated by single blank (‘ ’) characters and followed by a newline (‘\n’) character, to the
     standard output.

     The following option is available:

     -n    Do not print the trailing newline character.  This may also be achieved by appending ‘\c’ to the end of the string, as is done by iBCS2
           compatible systems.  Note that this option as well as the effect of ‘\c’ are implementation-defined in IEEE Std 1003.1-2001 (“POSIX.1”) as
           amended by Cor. 1-2002.  Applications aiming for maximum portability are strongly encouraged to use printf(1) to suppress the newline
           character.

     Some shells may provide a builtin echo command which is similar or identical to this utility.  Most notably, the builtin echo in sh(1) does not
     accept the -n option.  Consult the builtin(1) manual page.

EXIT STATUS
     The echo utility exits 0 on success, and >0 if an error occurs.

SEE ALSO
     builtin(1), csh(1), printf(1), sh(1)

STANDARDS
     The echo utility conforms to IEEE Std 1003.1-2001 (“POSIX.1”) as amended by Cor. 1-2002.

macOS 13.1                                                           April 12, 2003                                                           macOS 13.1

by default the text that echo prints on the command line are terminated by a new line character. if you see the above manual the program has a single -n option to omit the final newline.

➜  rustlabs echo -n Hello > hello-n
➜  rustlabs echo Hello > hello-a

the diff tool will display the difference between the two files .

rustlabs echo Hello > hello-a 
➜  rustlabs diff hello-n hello-a    
1c1
< Hello
\ No newline at end of file
---
> Hello

getting started

lets a new directory with the name echor using cargo :

$ cargo new echor
     Created binary (application) `echor` package

change the new directory to see the structure

➜  rustlabs cd echor 
➜  echor git:(master) ✗ tree
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

use cargo to run the program

➜  echor git:(master) ✗ cargo run
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
    Finished dev [unoptimized + debuginfo] target(s) in 0.84s
     Running `target/debug/echor`
Hello, world!

rust will start the program executing the main function in src/main.rs all function in src/main all functions return a value and the return type may be indicated with an arrow and the type such as -> u32 to say the function returns an unsigned 32-bit integer . the lack of any return type for the main implies that the function returns what calls the unit type /

println! the macro will automatically append a new line to the output, which is a feature you'll need to control when the user requests no terminating newline.

the unit type is like an empty value and is signified with a set of empty parentheses () the documentation says this "is used when there is no other meaningful value that could be returned " its not quite like a null pointer or undefined value in other languages


Accessing the command-line arguments

getting the command-line arguments to print. you can use std::env::args here std::env to interact with the environment which is where the program will find the arguments if you look at the documentation for a function, you will see it returns something of the type args

pub fn args() -> args

edit src/main.rs to print the arguments. you can call the function by using the full path followed by an empty set of parentheses

fn main() {
    println!(std::env::args());
}

execute the program using cargo run

echor git:(master) ✗ cargo run
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
error: format argument must be a string literal
 --> src/main.rs:2:14
  |
2 |     println!(std::env::args());
  |              ^^^^^^^^^^^^^^^^
  |
help: you might be missing a string literal to format with
  |
2 |     println!("{}", std::env::args());
  |              +++++

error: could not compile `echor` due to previous error

here you can't directly print the value that is returned from the function but it also suggests how to fix the problem. it wants you to first provide a literal string that has a set of curly braces {} that will serve as a placeholder for printed value so change the code

fn main() {
    println!("{}",std::env::args());
}

execute cargo run

➜  echor git:(master) ✗ cargo run
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
error[E0277]: `Args` doesn't implement `std::fmt::Display`
 --> src/main.rs:2:19
  |
2 |     println!("{}",std::env::args());
  |                   ^^^^^^^^^^^^^^^^ `Args` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Args`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `echor` due to previous error

there is a lot of information in that compiler message. the trait std::fmt::Display not being implemented for Args . A trait in rust is a way to define the behaviour of an object abstractly. if an object implements the Display trait, then it can be formatted for user-facing out.

The compiler suggests you should use {:?} instead of {} for placeholder . this is an instruction to print a Debug a version of the structure, which will format the output in debugging context

fn main() {
    println!("{:?}",std::env::args());
}

execute cargo run


➜  echor git:(master) ✗ cargo run
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
    Finished dev [unoptimized + debuginfo] target(s) in 0.44s
     Running `target/debug/echor`
Args { inner: ["target/debug/echor"] }

if you are unfamiliar with command line arguments, it's common for the first value to be the path of the program itself.

➜  echor git:(master) ✗ cargo run hello world
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/echor hello world`
Args { inner: ["target/debug/echor", "hello", "world"] }

let's see with -n flag

  echor git:(master) ✗ cargo run hello world -n
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/echor hello world -n`
Args { inner: ["target/debug/echor", "hello", "world", "-n"] }

cargo think the -n argument itself


Adding Clap as a Dependency

there are various methods and crates for parsing command-line arguments. we will use clap (https://crates.io/crates/clap)

let's add clap dependency to Cargo.toml

[package]
name = "echor"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = "4.1.1"

here we are using 4.1.1 a version of clap as a dependency

run cargo build to just build the new binary and not run it :

➜  echor git:(master) ✗ cargo build             
    Updating crates.io index
  Downloaded clap_lex v0.3.1
  Downloaded io-lifetimes v1.0.4
  Downloaded is-terminal v0.4.2
  Downloaded termcolor v1.2.0
  Downloaded errno v0.2.8
  Downloaded rustix v0.36.6
  Downloaded os_str_bytes v6.4.1
  Downloaded clap v4.1.1
  Downloaded 8 crates (599.0 KB) in 0.55s
   Compiling libc v0.2.139
   Compiling io-lifetimes v1.0.4
   Compiling rustix v0.36.6
   Compiling bitflags v1.3.2
   Compiling os_str_bytes v6.4.1
   Compiling termcolor v1.2.0
   Compiling strsim v0.10.0
   Compiling clap_lex v0.3.1
   Compiling errno v0.2.8
   Compiling is-terminal v0.4.2
   Compiling clap v4.1.1
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
    Finished dev [unoptimized + debuginfo] target(s) in 13.25s

a consequence of rust placing the dependencies into a target is that this directory is now quite large. you can use the disk usage command du -shc


parsing Command Line Arguments using Clap

update src/main.rs that creates a new clap::Command struct to parsing the command line arguments

use clap::Command;

fn main() {
    let _matches = Command::new("echor-app")
    .version("0.1.0")
    .author("Sangam Biradar <sangam14@gmail.com")
    .about("Rust echo")
    .get_matches();
}
  • Import the clap::Command struct

  • Create a new App with the name echor app

  • User Semantic version information

  • Include your name and email address so people know where to send the help

execute cargo run

echor git:(master) ✗ cargo run -- -h
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
    Finished dev [unoptimized + debuginfo] target(s) in 0.76s
     Running `target/debug/echor -h`
Rust echo

Usage: echor

Options:
  -h, --help     Print help
  -V, --version  Print version 
➜  echor git:(master) ✗ cargo run -- -V
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/echor -V`
echor-app 0.1.0

Inable derive flag you can create application declaratievly with struct

use std::path::PathBuf;

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Optional name to operate on
    name: Option<String>,

    /// Sets a custom config file
    #[arg(short, long, value_name = "FILE")]
    config: Option<PathBuf>,

    /// Turn debugging information on
    #[arg(short, long, action = clap::ArgAction::Count)]
    debug: u8,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// does testing things
    Test {
        /// lists test values
        #[arg(short, long)]
        list: bool,
    },
}

fn main() {
    let cli = Cli::parse();

    // You can check the value provided by positional arguments, or option arguments
    if let Some(name) = cli.name.as_deref() {
        println!("Value for name: {}", name);
    }

    if let Some(config_path) = cli.config.as_deref() {
        println!("Value for config: {}", config_path.display());
    }

    // You can see how many times a particular flag or argument occurred
    // Note, only flags can have multiple occurrences
    match cli.debug {
        0 => println!("Debug mode is off"),
        1 => println!("Debug mode is kind of on"),
        2 => println!("Debug mode is on"),
        _ => println!("Don't be crazy"),
    }

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    match &cli.command {
        Some(Commands::Test { list }) => {
            if *list {
                println!("Printing testing lists...");
            } else {
                println!("Not printing testing lists...");
            }
        }
        None => {}
    }

    // Continued program logic goes here...
}

cargo run

cargo run -- -h     
   Compiling echor v0.1.0 (/Users/sangambiradar/Documents/rustlabs/echor)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/echor -h`
Usage: echor [OPTIONS] [NAME] [COMMAND]

Commands:
  test  does testing things
  help  Print this message or the help of the given subcommand(s)

Arguments:
  [NAME]  Optional name to operate on

Options:
  -c, --config <FILE>  Sets a custom config file
  -d, --debug...       Turn debugging information on
  -h, --help           Print help
  -V, --version        Print version
➜  echor git:(master) ✗

add Clap withderive feature for CLI and anyhow for error propagation

cargo add clap -F derive
cargo add anyhow

organize directory with the following file structure

 src
├── commands
│   ├── cli.rs
│   └── mod.rs
└── main.rs
Cargo.lock
Cargo.toml

add the following code to src/commands/mod.rs

pub mod cli;

add the following code to src/commands/cli.rs

use anyhow::Result;
use clap::Parser;

/// Rusty example app
#[derive(Parser, Debug)]
#[command(version, bin_name = "rusty", disable_help_subcommand = true)]
pub struct Cli {}

impl Cli {
    pub fn exec(&self) -> Result<()> {
        println!("Hello, World!");

        Ok(())
    }
}

update entry point

mod commands;

use anyhow::Result;
use clap::Parser;


use crate::commands::cli::Cli;

fn main() -> Result<()> {
    let cli = Cli::parse();

    cli.exec()
}

execute cargo run

 cargo run -q -- --help
Rusty example app

Usage: rusty

Options:
  -h, --help     Print help
  -V, --version  Print version

Argument Passing

src
├── commands
│   ├── cli.rs
│   ├── exec.rs
│   └── mod.rs
└── main.rs
Cargo.lock
Cargo.toml

create the following file with content src/commands/exec.rs

use anyhow::Result;
use clap::Args;
use std::process::{exit, Command};

/// Execute an arbitrary command
///
/// All arguments are passed through unless --help is first
#[derive(Args, Debug)]
#[command()]
pub struct Cli {
    #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
    args: Vec<String>,
}

impl Cli {
    pub fn exec(&self) -> Result<()> {
        let mut command = Command::new(&self.args[0]);
        if self.args.len() > 1 {
            command.args(&self.args[1..]);
        }

        let status = command.status()?;
        exit(status.code().unwrap_or(1));
    }
}

add new module

pub mod cli;
pub mod exec;

modify root command

use anyhow::Result;
use clap::{Parser, Subcommand};

/// Rusty example app
#[derive(Parser, Debug)]
#[command(version, bin_name = "rusty", disable_help_subcommand = true)]
pub struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    Exec(super::exec::Cli),
}

impl Cli {
    pub fn exec(&self) -> Result<()> {
        match &self.command {
            Commands::Exec(cli) => cli.exec(),
        }
    }
}

execute cargo run

❯ cargo run -q -- --help
Rusty example app

Usage: rusty <COMMAND>

Commands:
  exec  Execute an arbitrary command

Options:
  -h, --help     Print help information
  -V, --version  Print version information
❯ cargo run -q -- exec --help
Execute an arbitrary command

All arguments are passed through unless --help is first

Usage: rusty exec <ARGS>...

Arguments:
  <ARGS>...


Options:
  -h, --help
          Print help information (use `-h` for a summary)
❯ cargo run -q -- exec ls -a
.
..
.git
.gitignore
Cargo.lock
Cargo.toml
src
target
0
Subscribe to my newsletter

Read articles from Sangam Biradar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sangam Biradar
Sangam Biradar

DevRel at StackGen | Formerly at Deepfence ,Tenable , Accurics | AWS Community Builder also Docker Community Award Winner at Dockercon2020 | CyberSecurity Innovator of Year 2023 award by Bsides Bangalore | Docker/HashiCorp Meetup Organiser Bangalore & Co-Author of Learn Lightweight Kubernetes with k3s (2019) , Packt Publication & also run Non Profit CloudNativeFolks / CloudSecCorner Community To Empower Free Education reach out me twitterhttps://twitter.com/sangamtwts or just follow on GitHub -> https://github.com/sangam14 for Valuable Resources