Writing Rust CLIs - Clap
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
structCreate a new App with the name
echor
appUser 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
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