This is a blog post version of a short talk I gave recently at a Rust Finland meetup.
- Introduction
- Motivation
- Program to greet
- CLI without dependencies beyond std
- CLI with clap, imperative
- CLI with clap, declarative
- CLI with clap, with subcommands (1/4)
- Output: top level help
- Output: help for a subcommand
- Output: subcommands
- Error: missing subcommand
- Error: extra argument
- AD: I do Rust training for money
Introduction
Command line programs are nicer to use if the command line interface (CLI) is nicer to use. This means, among other things:
- the CLI conforms to the conventions of the underlying platform
- I only use Linux, so what I talk about here may not apply to other systems
- long and short options, and values for options
- the CLI should have built-in help of some sort
- the
--help
option is non-negotiable - the
-h
alias for--help
would be nice
- the
- for complex programs, allow subcommands
- with their own help
- the program should check for errors the user may have made and give helpful error messages
Motivation
Why would you care about making a nice CLI?
- your users will like it
- you do like your users, don't you?
- you will like it
- you do use your own software, don't you?
- a nice CLI tends to be less error prone to use
- this means you get fewer support requests
- if you have competition, a nicer user experience will give you a boost
- it turns out that a nice CLI is easier and cheaper to maintain
- a nice CLI requires a nice command line parser and that, in turn, means code to define the CLI is simpler, easier to get right
Program to greet
Below I will show a few ways to implement a CLI for a program that greets the user ("hello, world"). The program is used like this:
greet
→ "hello, world"greet --whom Earth
→ "hello, Earth"greet --whom=Earth
→ "hello, Earth"
For reasons of how the Unix command line conventions evolved, a long option value may be part of the argument (with an equals sign) or the next argument. Users expect this, but it complicates the command line parser.
CLI without dependencies beyond std
The code below uses only the std
library, and parses the command
line manually. Note that it is buggy: this usually happens, when you
write command line parsing manually.
let mut whom = Some("world".into());
let mut got_whom = false;
let mut args = std::env::args();
args.next();
for arg in args {
if let Some(suffix) = arg.strip_prefix("--whom=") {
whom = Some(suffix.to_string());
} else if arg == "--whom" {
got_whom = true;
} else if got_whom {
whom = Some(arg.to_string());
got_whom = false;
} else {
eprintln!("usage error!");
std::process::exit(1);
}
}
CLI with clap, imperative
The clap crate is by far the most commonly used Rust library for command line parsing. A lot of effort has been put into making it both a pleasure to use for the programmer, and for the user.
The code below uses the traditional, imperative approach: you create a
Command
value, and configure that to know about the accepted command
line arguments. This is straightforward, but scales badly if to
programs with very large numbers of options: curl
, for example, has
255 options.
Note that this code lacks help texts, for brevity.
use clap::{Arg, Command};
fn main() {
let matches = Command::new("greet")
.arg(Arg::new("whom")
.long("whom")
.default_value("world"))
.get_matches();
let whom: &String = matches.get_one("whom").unwrap();
println!("hello, {}", whom);
}
CLI with clap, declarative
The derive
feature of clap allows a declarative approach for
defining command line syntax. The code below does that. It still lacks
help texts.
This style feels more magic, but is easy to work with and fully as powerful as the imperative style. Using the declarative style would be a good idea for anyone who wants to write a curl clone in a weekend.
use clap::Parser;
fn main() {
let args = Args::parse();
println!("{}", args.whom);
}
#[derive(Parser)]
struct Args {
#[clap(long, default_value = "world")]
whom: String,
}
CLI with clap, with subcommands (1/4)
The example program we're looking at could have a farewell mode as well as a greeting mode. This can be done using subcommands:
greet hello --whom=world
greet goodbye --whom=world
The code below demonstrates one approach for how to implement this using clap. The doc comments get turned into help text shown to the user.
use clap::{Parser, Subcommand};
fn main() {
let args = Args::parse();
match args.cmd {
Command::Hello(x) => x.run(),
Command::Goodbye(x) => x.run(),
}
}
/// General purpose greet/farewell messaging.
#[derive(Parser)]
struct Args {
#[command(subcommand)]
cmd: Command,
}
#[derive(Subcommand)]
enum Command {
Hello(Hello),
Goodbye(Goodbye),
}
/// Greet someone.
#[derive(Parser)]
struct Hello {
/// Whom should we greet?
#[clap(long, default_value("world"))]
whom: String,
}
impl Hello {
fn run(&self) {
println!("hello, {}", self.whom);
}
}
/// Say good bye to someone.
#[derive(Parser)]
struct Goodbye {
/// Whom should we say good by to?
#[clap(long, default_value("cruel world"))]
whom: String,
}
impl Goodbye {
fn run(&self) {
println!("good bye, {}", self.whom);
}
}
Output: top level help
$ cargo run -q -- --help
General purpose greet/farewell messaging
Usage: greet <COMMAND>
Commands:
hello Greet someone
goodbye Say good bye to someone
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
Output: help for a subcommand
$ cargo run -q -- hello --help
Greet someone
Usage: greet hello [OPTIONS]
Options:
--whom <WHOM> Whom should we greet? [default: world]
-h, --help Print help
Output: subcommands
$ cargo run -q -- hello
hello, world
$ cargo run -q -- hello --whom=Earth
hello, Earth
Error: missing subcommand
$ cargo run -q --
General purpose greet/farewell messaging
Usage: greet <COMMAND>
Commands:
hello Greet someone
goodbye Say good bye to someone
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
Error: extra argument
$ cargo run -q -- hello there
error: unexpected argument 'there' found
Usage: greet hello [OPTIONS]
For more information, try '--help'.