You are on page 1of 8

8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

5 Tips for Writing Small CLI Tools in Rust


===========================================

Created Aug 31, 2017 by Pascal Hertleif and published in Pascal's Scribbles. Labeled as
*rust*.

Rust is a great language to write small command line tools in. While it gives you some
tools for common tasks, allows nice abstractions, it also has a type system and approach
to API design that lead you to write robust code. Let me show you some techniques to
make this a nice experience.

Update: I published a crate (Rust library) that contains a lot of what this post
describes: quicli.

## Contents

1. Quick CLI argument handling


2. Error handling
3. Many small crates
4. Many small helper functions
5. Lots of structs
6. Bonus: Logging

## Quick CLI argument handling

There are many libraries out there to help you do that. What I’ve come to enjoy is
`structopt`: It gives you the power to annotate a `struct` or `enum` and turn its
fields/variants into CLI flags:

extern crate structopt;


#[macro_use] extern crate structopt_derive;

use structopt::StructOpt;

/// Do fancy things


#[derive(StructOpt, Debug)]
#[structopt(name = "fancify")]
struct Cli {
/// The source, possibly unfancy
source: String,

/// Level of fanciness we should aim for


#[structopt(long = "level", short = "l", default_value = "42")]
level: u8,

/// Output file

https://deterministic.space/rust-cli-tips.html 1/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

#[structopt(long = "output", short = "w", default_value = "/dev/null")]


output: String,
}

fn main() {
Cli::from_args();
}

This is *very* concise - but also very powerful! (It uses `clap` behind the scenes.)

$ cargo run -- --help


Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/fancify --help`
fancify 0.1.0
Pascal Hertleif <killercup@gmail.com>
Do fancy things

USAGE:
fancify [OPTIONS] <source>

FLAGS:
-h, --help Prints help information
-V, --version Prints version information

OPTIONS:
-l, --level <level> Level of fanciness we should aim for [default: 42]
-w, --output <output> Output file [default: /dev/null]

ARGS:
<source> The source, possibly unfancy

Or:

$ cargo run -- whatever --levl


Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/fancify whatever --levl`
error: Found argument '--levl' which wasn't expected, or isn't valid in this context
Did you mean --level?

USAGE:
fancify <source> --level <level>

For more information try --help

https://deterministic.space/rust-cli-tips.html 2/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

## Error handling

Right now, you can probably just use `error-chain`. It’s a good library. It might appear
to be a bit magical[^1] at first, but in 90% of my code it was really straightforward to
use. Towards the end of 2017, the `failure` crate was published, which takes a different
approach to error-chain, and solves some of the usual issues with it. As it’s still very
new, so I’ll not recommend it just yet, but in the future, I might update this post to
do so.

Let’s pick up our example from before but assume we don’t have any CLI options, so we’ll
hardcode them somehow.

#[macro_use] extern crate error_chain;

use std::fs::File;

// Short macro to define a main function that allows you to use `?`
quick_main!(|| -> Result<()> {
// Let's say this is user input
let source = "./whatever";
let level = "42";

// And now, let's convert this to formats that are useful to us


let source = File::open(source) // can return io::Error
.chain_err(|| format!("Can't open `{}`", source))?;

let level = level.parse()?; // can return a ParseIntError

let source_fanciness = get_fanciness(&source)?; // can return CantDetermineFancine

// And now for something cool: An assert that returns an Error


ensure!(source_fanciness < level, ErrorKind::AlreadyFancy(source_fanciness, level

// ...
Ok(())
});

fn get_fanciness(_source: &File) -> Result<u8> {


Ok(255) // Let's assume all inputs are fancy
}

// Let's define some errors here


error_chain! {
errors {
CantDetermineFanciness {
description("unable to determine fanciness from source")

https://deterministic.space/rust-cli-tips.html 3/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

}
AlreadyFancy(source_level: u8, target_level: u8) {
description("already fancy enough")
display("Already fancy enough: Source level {} above target level {}", so
}
}
foreign_links {
Io(::std::io::Error);
InvalidNumber(::std::num::ParseIntError);
}
}

Wow, that’s a lot of code. But I think it’s quite clear: It has a main function that
shows the control flow, followed by a helper function, followed but a full list of
possible errors.

And error-chain’s `quick_main!` macro gives us nice output on errors:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.61 secs
Running `target/debug/fancify`
Error: Can't open `./whatever`
Caused by: No such file or directory (os error 2)

Or:

$ touch whatever
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/fancify`
Error: Already fancy enough: Source level 255 above target level 42

## Many small crates

Don’t be afraid to depend on a lot of crates. Cargo is really good at allowing you not
to care about compiling and updating dependency, so let Cargo, and the Rust community,
help you!

For example, I recently wrote a CLI tool with 37 lines of code. This is the first block:

https://deterministic.space/rust-cli-tips.html 4/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

extern crate handlebars;


extern crate structopt;
#[macro_use] extern crate structopt_derive;
#[macro_use] extern crate error_chain;
extern crate serde;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json as json;
extern crate serde_yaml as yaml;
extern crate comrak;

## Many small helper functions

I tend to write a lot of small functions. Here’s an example of such a “small helper
function”:

fn open(path: &str) -> Result<File> {


File::open(path).chain_err(|| format!("Can't open `{}`", path))
}

Okay, that’s a bit underwhelming. How about this one?

fn read(path: &str) -> Result<String> {


let mut result = String::new();
let mut file = open(path)?;
file.read_to_string(&mut result)?;
Ok(result)
}

It’s one level more abstract than the standard library, hides an allocation of a String
with unknown length, but… it’s really handy.

I know could put the function bodies just inside the main code, but giving these little
code blocks names and getting the option of reusing them is really powerful. It also
makes the `main` function a tad more abstract and easier to read (no need to see through
all the implementation details). Furthermore (but this tends to not really shine in
small CLI apps) it makes things easily unit-testable.

And by the way: In most small CLI tools, performance is not that important. Feel free to
prefer `.clone()` to sprinkling your code with lifetime parameters.

## Lots of structs

https://deterministic.space/rust-cli-tips.html 5/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

In my experience, it really pays off to use a lot of structs. Some scenarios:

- Have the choice between using `serde_json::Value` with a bunch of `match`es or a


struct with `#[derive(Deserialize)]`? Choose the struct, get performance, nice errors,
and documentation about the shape you expect.
- Pass the same 3 parameters to a bunch of functions? Group them in a (tuple) struct,
give the group a name, and maybe even turn some of these functions into methods.
- See yourself writing a lot of boilerplate code? See if you can write a struct/enum and
use a derive.

## Bonus: Logging

And a bonus round: Some logging with `loggerv`! (It’s really simple, but usually
suffices for CLI apps. No need to go all in with streaming JSON logs to logstash for
now.)

#[macro_use] extern crate log;


extern crate loggerv;

#[macro_use] extern crate error_chain;


extern crate structopt;
#[macro_use] extern crate structopt_derive;
use structopt::StructOpt;

/// Do fancy things


#[derive(StructOpt, Debug)]
#[structopt(name = "fancify")]
struct Cli {
/// Enable logging, use multiple `v`s to increase verbosity
#[structopt(short = "v", long = "verbose")]
verbosity: u64,
}

quick_main!(|| -> Result<()> {


let args = Cli::from_args();

loggerv::init_with_verbosity(args.verbosity)?;

// ...
let thing = "foobar";
debug!("Thing happened: {}", thing);
// ...

info!("It's all good!");


Ok(())
});

error_chain! {

https://deterministic.space/rust-cli-tips.html 6/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

foreign_links {
Log(::log::SetLoggerError);
}
}

Sweet! Let’s run it three time with more of less verbosity!

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/fancify`
$ cargo run -- -v
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/fancify -v`
fancify: It's all good!
$ cargo run -- -vv
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/fancify -vv`
fancify: Thing happened: foobar
fancify: It's all good!

## Conclusion

These were my five tips for writing small CLI applications in Rust (writing nice
libraries is another topic). If you have more tips, let me know!

If you want to dig a little deeper, I’d suggest looking at how to multi-platform build
Rust binaries releases, how to use `clap` to get autocompletion for CLI args, and how to
write integration test for your CLI apps (upcoming post).

___

1. If you are afraid of macro-heavy code, you can use cargo-expand to see what concrete
code the macro generates. ↩

Thanks for reading. Comment on this post! Revision history.

This post has been discussed on Twitter and Reddit.

Back to index.

RSS feed / Follow me on Twitter

https://deterministic.space/rust-cli-tips.html 7/8
8/4/2019 5 Tips for Writing Small CLI Tools in Rust - Pascal’s Scribbles

© Copyright 2019 Pascal Hertleif / Imprint

https://deterministic.space/rust-cli-tips.html 8/8

You might also like