Rusty Utilities
Building Powerful Command Line Applications in Rust
Slides available at slides.com/kevinknapp/rusty_utilities
'talk: {
- Why Rust
- Command Line Uitilities
- Command Line Interfaces using clap
}
- Rust Basics
- CLI UX
- Environmental Variables
- Configuration Files
$ whoami
Kevin Knapp
Systems & Network Admin
Avid skydiver
.NETer->Pythonista->Gopher->Rustacean


//
//


There's more than one way to skin a cat...
Command Line Utilities in Rust
In 30 seconds...
Major Components
- Controller
- Interface
- Configuration
- Application
- [Utilities]
- Output
- Errors
- Results

Controller
-
The single point where execution starts and ends
- Typically does the following in serial:
- Builds the Interface
- Parses the Interface results into a Configuration
- Hands off the configuration to the Application
- Displays any terminal results and output
- Exits execution
mod cli;
fn main() {
if let Err(e) = cli::parse().map(Config::from)
.and_then(run) {
e.display_and_exit();
}
}
fn run(c: Config) -> Result<Error> {
// do the real stuff
Ok(())
}
Interface
- The language between the user and program
- Can validate input format
- Will normalize user input into a valid result
- Types of interfaces include
- Curses based
- Single run
- Interactive
- Hybrids
- More to follow...
Configuration
- A normalized representation of all program settings, input, and functionality
- Often dictates how a program will function
- Is typically built from the results of the interface parsing
- Consider owned vs borrowed values
- Consider mutability
struct Config {
files: Vec<PathBuf>,
pretty: bool,
verbose: VerbLevel
}
Application
- Does the things
- Uses Result<T> idioms

As far as this talk is concerned...
Output
- Errors or Results
- Displayed inline or propagated up

Keep the user in mind!
Results
- Typically written to stdout
- Allows redirecting and piping
- Prefer write! and writeln! over print! and println!
- println! macros can panic! but writeln! macros return Result<T>s
- writeln! macros allow abstracting over any io::Write or fmt::Write objects
- Consider customisable output if applicable
- Changing formats
- Machine readable (JSON, YAML, CSV, etc.)
Errors
- Arguably the most important information to return
- Consider error-chain
- Use distinct mod
- Consider an enum
- Impl From<E> where E is an expected external error (io, etc.)
- Impl std::error::Error for MyError
- Is the error terminal?
// From globset
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Error {
UnclosedClass,
InvalidRange(char, char),
Regex(String),
// snip..
}
Utilities
- Grouped sets of stand alone "helpers"
- Should be separate mod
- If big enough, or autonomous, consider separate crate
Command Line Interfaces in Rust

clap
Command Line Argument Parser

It is a simple-to-use, efficient, and full-featured library for parsing command line arguments and subcommands when writing console/terminal applications.
//
//
Think of it as encoding a type system into arguments...
The Basics

Getting Started
- Add clap to Cargo.toml dependencies array
- Add clap to the top level project (src/main.rs)
[dependencies]
clap = "2.20.4"
// src/main.rs
extern crate clap;
Key Players
- The Builder Pattern
- App
- Arg
- ArgMatches
- SubCommands
Builder Pattern
- Allows a large number of [optional] inputs/configuration
- Negates needing many different constructors
- Typically finalizes construction via a "terminal method"
- Two types of Builders
- Consuming (terminal method takes self)
- clap is a consuming builder*
- Non-Consuming (terminal method takes &self or &mut self)
- Consuming (terminal method takes self)
- Contains all the valid Args, subcommands, etc.
- Recursive tree-like structure
- Can contain metadata
- Generates a help message and two args automatically
- -h/--help
- -V/--version
- Can be overriden
- Terminal method: get_matches()
- And family...
extern crate clap;
use clap::App;
fn main() {
App::new("prog")
.author("Kevin K.")
.version("1.1")
.about("Does awesome things!")
.get_matches();
}
- Represents a valid argument
- Various settings determine final "type"
- Three "types" of args
- Flags
- Options
- Postional/Free
// A flag that can be used -F
Arg::with_name("flag")
.help("I describe a flag")
.short("F")
// An option that can be used --option <val>
Arg::with_name("opt")
.help("Something about this option")
.long("option")
.takes_value(true)
// A positional argument that can be used <val>
Arg::with_name("arg")
.help("I'm free!")
Adding Args
- Can be added one at a time, or in clusters
App::new("prog")
.arg(Arg::with_name("flag")
.help("I describe a flag")
.short("F")
)
.args(&[
Arg::with_name("opt")
.help("Something about this option")
.long("option")
.takes_value(true),
Arg::with_name("arg")
.help("I'm free!")
])
.get_matches();
I can define args...

- The parsed structure of all valid args for that command
- A recursive tree-like structure
-
Values, number of occurrences, or pressence are accessed by name
- Values are stored in order
let m = App::new("prog")
// ...
.get_matches();
// Testing presence
println!("Was -F used?...{:?}", m.is_present("flag"));
// Getting a value
if let Some(val) = m.value_of("arg") {
println!("<arg> has the value {}", val);
}
// Checking occurrences
println!("--option <val> was used {} times", m.occurrences_of("opt"));
A little more detail...

Common Arg settings We've Seen So Far
-
index
- Starts at 1 not 0
- Inferred if not specified manually
-
takes_value
- Stackable with shorts: -xFsval == -x -F -sval
-
Values associated via
- --some val
- --some=val
- -sval
- multiple (*)
Arg::with_name("opt")
// ..snip
.required(true)
Arg::with_name("arg")
// ..snip
.default_value("lol")
Arg::with_name("flag")
// ..snip
.multiple(true)
}
Some new basics
* When combined with options, must consider multiple values vs multiple occurrences
- Sub Apps. Period.
- Metadata is independent of parent
- Added the same as Args
App::new("git")
.version("1.0")
.about("does git things")
.arg(Arg::with_name("work-tree")
.long("work-tree")
.takes_value(true))
.subcommand(SubCommand::with_name("clone")
.version("2.0")
.about("clones things")
.arg(Arg::with_name("repo")
.help("The repo URL to clone")))
.get_matches();
SubCommand Trees
Top Level App (git) TOP
|
-----------------------------------------
/ | \ \
clone push add commit LEVEL 1
| / \ / \ |
url origin remote ref name message LEVEL 2
/ /\
path remote local LEVEL 3
$ git clone url
$ git push origin path
$ git add ref local
$ git commit message
Valid Invocations
- Recursive tree-like structure
- Requesting the ArgMatches from SubCommand gives back an independant ArgMatches
let m = App::new("git")
// ...
.get_matches();
match m.subcommand() {
("clone", Some(clone_matches)) =>{
println!("Cloning {}", clone_matches.value_of("repo").unwrap());
},
("push", Some(push_matches)) =>{
match push_matches.subcommand() {
("remote", Some(remote_matches)) =>{
println!("Pushing to {}", remote_matches.value_of("repo").unwrap());
},
("local", Some(_)) =>{
println!("'git push local' was used");
},
_ => unreachable!(),
}
},
("add", Some(add_matches)) =>{
println!("Adding {}", add_matches.values_of("stuff")
.unwrap()
.collect::<Vec<_>>()
.join(", "));
},
("", None) => println!("No subcommand was used"),
_ => unreachable!(),
}
Intermediate
...Customization...

Argument Relationships
- One of the features that lets clap validate FOR YOU
- Force other arguments to be present (requires, and family) (Run Online)
- Hard conflicts
- Disallow use of other arguments and form an error (conflicts_with, and family) (Run Online)
- Soft conflicts
- POSIX style overrides (overrides_with, and family) (Run Online)
Customizing a Single Value
- Can change how values are parsed or their aesthetics
- Can set specific values (Pro Tip: consider arg_enum!)
- Or allow empty values which allows --option= or --option ""
- We can also give values names for help messages and errors
Arg::with_name("mode")
.takes_value(true)
.possible_value("fast")
.possible_values(&[
"slow",
"medium"
])
Arg::with_name("config")
.long("config")
.value_name("FILE")
// produces --config <FILE>
Customizing Multiple Values
- Can change how values delimited and terminated
- Default delimiter is "," with no terminator
- Delimiter can be set or turned off entirely
- Terminators are useful for accepting extern commands or similar
App::new("do")
.arg(Arg::with_name("cmds")
.multiple(true)
.allow_hyphen_values(true)
.value_terminator(";"))
.arg(Arg::with_name("location"))
// run with: $ do find -type f -name special ; /home/clap
- Can set the exact number of values, or minimums and maximums
Validators
- The other feature that lets clap validate FOR YOU
- Perform a function on a value to ensure it's validity
fn is_png(val: String) -> Result<(), String> {
if val.ends_with(".png") {
Ok(())
} else {
Err(String::from("the file format must be png."))
}
}
App::new("myapp")
.arg(Arg::with_name("input")
.help("the input file to use")
.validator(is_png))
Let's Try It...

Simplified IPv4 address
- Consists of four octets with numbers 0-255
- Delimited by a dot "."
extern crate clap;
use clap::{App, Arg};
fn main() {
let m = App::new("net")
.arg(Arg::with_name("ip")
.short("i")
.long("ip-addr")
.value_name("X.X.X.X")
.help("An IP Address")
.required(true)
.number_of_values(4)
.validator(valid_octet)
.value_delimiter(".")
.require_delimiter(true))
.get_matches();
let ip = m.values_of("ip").unwrap().collect::<Vec<_>>().join(".");
println!("{} is probably a valid IP", ip);
}
fn valid_octet(o: String) -> Result<(), String> {
if o.parse::<u8>().is_err() {
return Err(format!("'{}' must be a number between 0 and 255", o));
}
Ok(())
}
Customizing an App with AppSettings
- Can change behavior or just aesthetics
app.setting(AppSettings::GlobalVersion);
// or
app.settings(&[
AppSettings::Hidden,
AppSettings::ArgRequiredElseHelp
]);
- Can also be applied globally which applies to all child SubCommands as well
Notable AppSettings
-
AllowLeadingHyphen
-
DeriveDisplayOrder
-
Hidden
-
SubcommandsNegateReqs
-
{Subcommand, Arg}RequiredElseHelp
-
SubcommandRequired
-
UnifiedHelpMessage
-
*VersionlessSubcommands
-
*GlobalVersion
*Automatically applied globally
Customizing the Help with Templates
app.template(
"{bin} - v{version}
DESCRIPTION:
{about}
USAGE:
{usage}
AUTHOR:
{author}
ARGS:
{positionals}
OPTIONS:
{flags}
{options}
COMMANDS:
{subcommands}
")
Advanced Topics
Getting the most out of it

Conditionals
- Conditions can be single or chained conditions
- Args can conditionally...
- Be required if other args are used with specific values
- Have different default values depending on whether or not other args are used with/without specific values
- Require other args based on the values provided
Aliases
- Allows one to link SubCommands or Args
- These can be shown in the help text (visible_alias) or hidden from the help text
- When used, it's no different than original arg/subcommand being called
- Great for
- CLI migrations
- UX (was it --completion or --completions??)
App::new("alias")
.arg(Arg::with_name("flag")
.long("flag")
.alias("flags")
.visible_alias("flutter"))
.subcommand(SubCommand::with_name("doc")
.alias("docs")
.visible_alias("documentation"))
Shell Completions
- Supports Bash, Zsh, Fish, and PowerShell
- Can be generated at compile time or runtime
Compile Time Completions
// src/cli.rs
use clap::{App, Arg, SubCommand};
pub fn build_cli() -> App<'static, 'static> {
App::new("compl")
// ..snip
}
# Cargo.toml
build = "build.rs"
[build-dependencies]
clap = "2.9"
// build.rs
extern crate clap;
use clap::Shell;
include!("src/cli.rs");
fn main() {
let mut app = build_cli();
for shell in &Shell::variants() {
app.gen_completions("myapp", shell.parse().unwrap(), env!("OUT_DIR"));
}
}
Runtime Completions
extern crate clap;
use std::io;
use clap::{App, Arg, SubCommand, Shell};
fn build_cli() -> App<'static, 'static> {
App::new("My Super Program")
// ..snip
.subcommand(SubCommand::with_name("completions")
.arg(Arg::with_name("shell")
.possible_values(&Shell::variants())
.required(true)))
}
fn main() {
let m = build_cli().get_matches();
if let Some(compl_m) = m.subcommand_matches("completions") {
let mut app = build_cli();
let shell = compl_m.value_of("shell").unwrap().parse().unwrap();
app.gen_completions_to("demo", shell, &mut io::stdout());
}
}
Key Topics Not Covered
- Usage Parser - "-c, --config <FILE> 'Some help'"
- Speed
- From YAML
- macros
- Invalid UTF-8
Still to come...

Current Tracking Issues
Credits
-
Distil Networks
- Hosting Rust DC
-
Integer32
- Play links
-
Aaron Turon
- Builder Pattern Guidelines
-
Andrew Gallant
- globset/ripgrep/general advice and practices
Get Involved!
- android-rs-glue
- uutils/coreutils
- Port your favorite CLI!
Contact / Follow Up
- clap.rs
- YouTube - Argument Parsing in Rust v2
- Gitter - (kbknapp/clap-rs)
- Github (kbknapp/clap-rs)
- Twitter (@thekbknapp)
- IRC - #rust, #clap-rs (kbknapp)

Rusty Utilities
By Kevin Knapp
Rusty Utilities
Building Powerful Command Line Applications with Rust
- 1,318