Rust Cli
Rust Cli
html
Writing a program with a simple command line interface (CLI) is a great exercise for a
beginner who is new to the language and wants to get a feel for it. There are many aspects
to this topic, though, that often only reveal themselves later on.
This book is structured like this: We start with a quick tutorial, after which you’ll end up
with a working CLI tool. You’ll be exposed to a few of the core concepts of Rust as well as
the main aspects of CLI applications. What follows are chapters that go into more detail on
some of these aspects.
One last thing before we dive right into CLI applications: If you found an error in this book
or want to help us write more content for it, you can �nd its source in the CLI WG
repository. We’d love to hear your feedback! Thank you!
You’ll learn all the essentials about how to get going, and where to �nd more information.
Feel free to skip parts you don’t need to know right now or jump in at any point.
Getting help: If you at any point feel overwhelmed or confused with the features
used, have a look at the extensive o�cial documentation that comes with Rust, �rst
and foremost the book, The Rust Programming Language. It comes with most Rust
installations ( rustup doc ), and is available online on doc.rust-lang.org.
You are also very welcome to ask questions – the Rust community is known to be
friendly and helpful. Have a look at the community page to see a list of places
第1页 共40页 where 下午6:19
2020/12/23
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
What kind of project do you want to write? How about we start with something simple:
Let’s write a small grep clone. That is a tool that we can give a string and a path and it’ll
print only the lines that contain the given string. Let’s call it grrs (pronounced “grass”).
$ cat test.txt
foo: 10
bar: 20
baz: 30
$ grrs foo test.txt
foo: 10
$ grrs --help
[some help text explaining the available options]
Note: This book is written for Rust 2018. The code examples can also be used on Rust
2015, but you might need to tweak them a bit; add extern crate foo; invocations,
for example.
Make sure you run Rust 1.31.0 (or later) and that you have edition = "2018" set in
the [package] section of your Cargo.toml �le.
Project setup
If you haven’t already, install Rust on your computer (it should only take a few minutes).
After that, open a terminal and navigate to the directory you want to put your application
code into.
Start by running cargo new grrs in the directory you store your programming projects
in. If you look at the newly created grrs directory, you’ll �nd a typical setup for a Rust
project:
A Cargo.toml �le that contains metadata for our project, incl. a list of
dependencies/external libraries we use.
A src/main.rs �le that is the entry point for our (main) binary.
If you can execute cargo run in the grrs directory and get a “Hello World”, you’re all set
up.
We expect our program to look at test.txt and print out the lines that contain foobar .
But how do we get these two values?
The text after the name of the program is often called the “command line arguments”, or
“command line �ags” (especially when they look like --this ). Internally, the operating
system usually represents them as a list of strings – roughly speaking, they get separated
by spaces.
There are many ways to think about these arguments, and how to parse them into
something more easy to work with. You will also need to tell the users of your program
which arguments they need to give and in which format they are expected.
Getting the raw arguments this way is quite easy (in �le src/main.rs , after fn main()
{ ):
arguments as a custom data type that represents the inputs to your program.
Look at grrs foobar test.txt : There are two arguments, �rst the pattern (the string
to look for), and then the path (the �le to look in).
What more can we say about them? Well, for a start, both are required. We haven’t talked
about any default values, so we expect our users to always provide two values.
Furthermore, we can say a bit about their types: The pattern is expected to be a string,
while the second argument is expected to be a path to a �le.
In Rust, it is common to structure programs around the data they handle, so this way of
looking at CLI arguments �ts very well. Let’s start with this (in �le src/main.rs , before fn
main() { ):
struct Cli {
pattern: String,
path: std::path::PathBuf,
}
This de�nes a new structure (a struct ) that has two �elds to store data in: pattern , and
path .
Aside: PathBuf is like a String but for �le system paths that works cross-platform.
Now, we still need to get the actual arguments our program got into this form. One option
would be to manually parse the list of strings we get from the operating system and build
the structure ourselves. It would look something like this:
This works, but it’s not very convenient. How would you deal with the requirement to
support --pattern="foo" or --pattern "foo" ? How would you implement --help ?
The structopt library builds on clap and provides a “derive” macro to generate clap
第4页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
code for struct de�nitions. This is quite nice: All we have to do is annotate a struct and
it’ll generate the code that parses the arguments into the �elds.
use structopt::StructOpt;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(StructOpt)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
#[structopt(parse(from_os_str))]
path: std::path::PathBuf,
}
Note: There are a lot of custom attributes you can add to �elds. For example, we
added one to tell structopt how to parse the PathBuf type. To say you want to use
this �eld for the argument after -o or --output , you’d add #[structopt(short =
"o", long = "output")] . For more information, see the structopt documentation.
Right below the Cli struct our template contains its main function. When the program
starts, it will call this function. The �rst line is:
fn main() {
let args = Cli::from_args();
}
This will try to parse the arguments into our Cli struct.
But what if that fails? That’s the beauty of this approach: Clap knows which �elds to
expect, and what their expected format is. It can automatically generate a nice --help
message, as well as give some great errors to suggest you pass --output when you wrote
--putput .
Note: The from_args method is meant to be used in your main function. When it
fails, it will print out an error or help message and immediately exit the program.
Don’t use it in other places!
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 10.16s
Running `target/debug/grrs`
error: The following required arguments were not provided:
<pattern>
<path>
USAGE:
grrs <pattern> <path>
We can pass arguments when using cargo run directly by writing them after -- :
As you can see, there is no output. Which is good: That just means there is no error and
our program ended.
Exercise for the reader: Make this program output its arguments!
Aside: See that .expect method here? This is a shortcut function to quit that will
make the program exit immediately when the value (in this case the input �le) could
not be read. It’s not very pretty, and in the next chapter on Nicer error reporting we
will look at how to improve this.
Now, let’s iterate over the lines and print each one that contains our pattern:
Exercise for the reader: This is not the best implementation: It will read the whole
�le into memory – however large the �le may be. Find a way to optimize it! (One idea
might be to use a BufReader instead of read_to_string() .)
Results
A function like read_to_string doesn’t return a string. Instead, it returns a Result that
contains either a String or an error of some type (in this case std::io::Error ).
How do you know which it is? Since Result is an enum , you can use match to check
which variant it is:
Aside: Not sure what enums are or how they work in Rust? Check this chapter of the
Rust book to get up to speed.
Unwrapping
Now, we were able to access the content of the �le, but we can’t really do anything with it
after the match block. For this, we’ll need to somehow deal with the error case. The
第7页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
challenge is that all arms of a match block need to return something of the same type.
But there’s a neat trick to get around that:
We can use the String in content after the match block. If result were an error, the
String wouldn’t exist. But since the program would exit before it ever reached a point
where we use content , it’s �ne.
This may seem drastic, but it’s very convenient. If your program needs to read that �le and
can’t do anything if the �le doesn’t exist, exiting is a valid strategy. There’s even a shortcut
method on Result s, called unwrap :
No need to panic
Of course, aborting the program is not the only way to deal with errors. Instead of the
panic! , we can also easily write return :
This, however changes the return type our function needs. Indeed, there was something
hidden in our examples all this time: The function signature this code lives in. And in this
last example with return , it becomes important. Here’s the full example:
Our return type is a Result ! This is why we can write return Err(error); in the second
match arm. See how there is an Ok(()) at the bottom? It’s the default return value of the
function and means “Result is okay, and has no content”.
Aside: Why is this not written as return Ok(()); ? It easily could be – this is totally
valid as well. The last expression of any block in Rust is its return value, and it is
customary to omit needless return s.
Question Mark
Just like calling .unwrap() is a shortcut for the match with panic! in the error arm, we
have another shortcut for the match that return s in the error arm: ? .
That’s right, a question mark. You can append this operator to a value of type Result ,
and Rust will internally expand this to something very similar to the match we just wrote.
Give it a try:
Very concise!
Aside: There are a few more things happening here that are not required to
understand to work with this. For example, the error type in our main function is
Box<dyn std::error::Error> . But we’ve seen above that read_to_string returns
a std::io::Error . This works because ? expands to code that converts error types.
Box<dyn std::error::Error> is also an interesting type. It’s a Box that can contain
any type that implements the standard Error trait. This means that basically all
errors can be put into this box, so we can use ? on all of the usual functions that
return Result s.
Providing Context
The errors you get when using ? in your main function are okay, but they are not great.
For example: When you run std::fs::read_to_string("test.txt")? but the �le
test.txt doesn’t exist, you get this output:
In cases where your code doesn’t literally contain the �le name, it would be very hard to
tell which �le was NotFound . There are multiple ways to deal with this.
For example, we can create our own error type, and then use that to build a custom error
message:
#[derive(Debug)]
struct CustomError(String);
Not very pretty, but we can easily adapt the debug output for our type later on.
This pattern is in fact very common. It has one problem, though: We don’t store the
original error, only its string representation. The often used anyhow library has a neat
solution for that: Similar to our CustomError type, its Context trait can be used to add a
description. Additionally, it also keeps the original error, so we get a “chain” of error
messages pointing out the root cause.
Let’s �rst import the anyhow crate by adding anyhow = "1.0" to the [dependencies]
section of our Cargo.toml �le.
Caused by:
No such file or directory (os error 2)
Output
println!("Hello World");
Using println
You can pretty much print all the things you like with the println! macro. This macro
has some pretty amazing capabilities, but also a special syntax. It expects you to write a
string literal as the �rst parameter, that contains placeholders that will be �lled in by the
values of the parameters that follow as further arguments.
For example:
let x = 42;
println!("My lucky number is {}.", x);
will print
The curly braces ( {} ) in the string above is one of these placeholders. This is the default
placeholder type that tries to print the given value in a human readable way. For numbers
and strings this works very well, but not all types can do that. This is why there is also a
“debug representation”, that you can get by �lling the braces of the placeholder like this:
{:?} .
For example,
will print
If you want your own data types to be printable for debugging and logging, you can in
most cases add a #[derive(Debug)] above their de�nition.
Aside: “User-friendly” printing is done using the Display trait, debug output (human-
readable but targeted at developers) uses the Debug trait. You can �nd more
information about the syntax you can use in println! in the documentation for the
std::fmt module.
Printing errors
Printing errors should be done via stderr to make it easier for users and other tools to
pipe their outputs to �les or more tools.
Aside: On most operating systems, a program can write to two output streams,
stdout and stderr . stdout is for the program’s actual output, while stderr
allows errors and other messages to be kept separate from stdout . That way, output
can be stored to a �le or piped to another program while errors are shown to the
user.
In Rust this is achieved with println! and eprintln! , the former printing to stdout
and the latter to stderr .
println!("This is information");
eprintln!("This is an error! :(");
Beware: Printing escape codes can be dangerous, putting the user’s terminal into a
weird state. Always be careful when manually printing them!
Ideally you should be using a crate like ansi_term when dealing with raw escape
codes to make your (and your user’s) life easier.
First, you might want to reduce the number of writes that actually “�ush” to the terminal.
println! tells the system to �ush to the terminal every time, because it is common to
print each new line. If you don’t need that, you can wrap your stdout handle in a
BufWriter which by default bu�ers up to 8 kB. (You can still call .flush() on this
BufWriter when you want to print immediately.)
Second, it helps to acquire a lock on stdout (or stderr ) and use writeln! to print to it
directly. This prevents the system from locking and unlocking stdout over and over
again.
Using the indicatif crate, you can add progress bars and little spinners to your program.
Here’s a quick example:
fn main() {
let pb = indicatif::ProgressBar::new(100);
for i in 0..100 {
do_hard_work();
pb.println(format!("[+] finished #{}", i));
pb.inc(1);
}
pb.finish_with_message("done");
}
Logging
To make it easier to understand what is happening in our program, we might want to add
some log statements. This is usually easy while writing your application. But it will become
super helpful when running this program again in half a year. In some regard, logging is
the same as using println , except that you can specify the importance of a message. The
levels you can usually use are error, warn, info, debug, and trace (error has the highest
priority, trace the lowest).
To add simple logging to your application, you’ll need two things: The log crate (this
contains macros named after the log level) and an adapter that actually writes the log
output somewhere useful. Having the ability to use log adapters is very �exible: You can,
for example, use them to write logs not only to the terminal but also to syslog, or to a
central log server.
Since we are right now only concerned with writing a CLI application, an easy adapter to
use is env_logger. It’s called “env” logger because you can use an environment variable to
specify which parts of your application you want to log (and at which level you want to log
them). It will pre�x your log messages with a timestamp and the module where the log
messages come from. Since libraries can also use log , you easily con�gure their log
output, too.
fn main() {
env_logger::init();
info!("starting up");
warn!("oops, nothing implemented!");
}
Assuming you have this �le as src/bin/output-log.rs , on Linux and macOS, you can
run it like this:
$ $env:RUST_LOG="output_log=info"
$ cargo run --bin output-log
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/output-log.exe`
[2018-11-30T20:25:52Z INFO output_log] starting up
[2018-11-30T20:25:52Z WARN output_log] oops, nothing implemented!
$ set RUST_LOG=output_log=info
$ cargo run --bin output-log
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/output-log.exe`
[2018-11-30T20:25:52Z INFO output_log] starting up
[2018-11-30T20:25:52Z WARN output_log] oops, nothing implemented!
RUST_LOG is the name of the environment variable you can use to set your log settings.
env_logger also contains a builder so you can programmatically adjust these settings,
and, for example, also show info level messages by default.
There are a lot of alternative logging adapters out there, and also alternatives or
extensions to log . If you know your application will have a lot to log, make sure to review
them, and make your users’ life easier.
Tip: Experience has shown that even mildly useful CLI programs can end up being
used for years to come. (Especially if they were meant as a temporary solution.) If your
application doesn’t work and someone (e.g., you, in the future) needs to �gure out
why, being able to pass --verbose to get additional log output can make the
di�erence between minutes and hours of debugging. The clap-verbosity-�ag crate
contains a quick way to add a --verbose to a project using structopt .
Testing
Over decades of software development, people have discovered one truth: Untested
software rarely works. (Many people would go as far as saying: “Most tested software
doesn’t work either.” But we are all optimists here, right?) So, to ensure that your program
does what you expect it to do, it is wise to test it.
One easy way to do that is to write a README �le that describes what your program
should do. And when you feel ready to make a new release, go through the README and
ensure that the behavior is still as expected. You can make this a more rigorous exercise
by also writing down how your program should react to erroneous inputs.
Here’s another fancy idea: Write that README before you write the code.
Aside: Have a look at test-driven development (TDD) if you haven’t heard of it.
第15页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
Automated testing
Now, this is all �ne and dandy, but doing all of this manually? That can take a lot of time. At
the same time, many people have come to enjoy telling computers to do things for them.
Let’s talk about how to automate these tests.
Rust has a built-in test framework, so let’s start by writing a �rst test:
#[test]
fn check_answer_validity() {
assert_eq!(answer(), 42);
}
You can put this snippet of code in pretty much any �le and cargo test will �nd and run
it. The key here is the #[test] attribute. It allows the build system to discover such
functions and run them as tests, verifying that they don’t panic.
running 1 test
test check_answer_validity ... ok
Now that we’ve seen how we can write tests, we still need to �gure out what to test. As
you’ve seen it’s fairly easy to write assertions for functions. But a CLI application is often
more than one function! Worse, it often deals with user input, reads �les, and writes
output.
To �gure out what we should test, let’s see what our program features are. Mainly, grrs
is supposed to print out the lines that match a given pattern. So, let’s write unit tests for
exactly this: We want to ensure that our most important piece of logic works, and we want
to do it in a way that is not dependent on any of the setup code we have around it (that
deals with CLI arguments, for example).
第16页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
Going back to our �rst implementation of grrs , we added this block of code to the main
function:
// ...
for line in content.lines() {
if line.contains(&args.pattern) {
println!("{}", line);
}
}
Sadly, this is not very easy to test. First of all, it’s in the main function, so we can’t easily call
it. This is easily �xed by moving this piece of code into a function:
Now we can call this function in our test, and see what its output is:
#[test]
fn find_a_match() {
find_matches("lorem ipsum\ndolor sit amet", "lorem");
assert_eq!( // uhhhh
Or… can we? Right now, find_matches prints directly to stdout , i.e., the terminal. We
can’t easily capture this in a test! This is a problem that often comes up when writing tests
after the implementation: We have written a function that is �rmly integrated in the
context it is used in.
Note: This is totally �ne when writing small CLI applications. There’s no need to make
everything testable! It is important to think about which parts of your code you might
want to write unit tests for, however. While we’ll see that it’s easy to change this
function to be testable, this is not always the case.
Alright, how can we make this testable? We’ll need to capture the output somehow. Rust’s
standard library has some neat abstractions for dealing with I/O (input/output) and we’ll
make use of one called std::io::Write . This is a trait that abstracts over things we can
write to, which includes strings but also stdout .
If this is the �rst time you’ve heard “trait” in the context of Rust, you are in for a treat. Traits
are one of the most powerful features of Rust. You can think of them like interfaces in
Java, or type classes in Haskell (whatever you are more familiar with). They allow you to
abstract over behavior that can be shared by di�erent types. Code that uses traits can
第17页express
共40页 ideas in very generic and �exible ways. This means it can also get di�cult to read,下午6:19
2020/12/23
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
though. Don’t let that intimidate you: Even people who have used Rust for years don’t
always get what generic code does immediately. In that case, it helps to think of concrete
uses. For example, in our case, the behavior that we abstract over is “write to it”. Examples
for the types that implement (”impl”) it include: The terminal’s standard output, �les, a
bu�er in memory, or TCP network connections. (Scroll down in the documentation for
std::io::Write to see a list of “Implementors”.)
With that knowledge, let’s change our function to accept a third parameter. It should be of
any type that implements Write . This way, we can then supply a simple string in our tests
and make assertions on it. Here is how we can write this version of find_matches :
The new parameter is mut writer , i.e., a mutable thing we call “writer”. Its type is impl
std::io::Write , which you can read as “a placeholder for any type that implements the
Write trait”. Also note how we replaced the println!(…) we used earlier with
writeln!(writer, …) . println! works the same as writeln! but always uses
standard output.
#[test]
fn find_a_match() {
let mut result = Vec::new();
find_matches("lorem ipsum\ndolor sit amet", "lorem", &mut result);
assert_eq!(result, b"lorem ipsum\n");
}
To now use this in our application code, we have to change the call to find_matches in
main by adding &mut std::io::stdout() as the third parameter. Here’s an example of
a main function that builds on what we’ve seen in the previous chapters and uses our
extracted find_matches function:
Ok(())
}
第18页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
Note: Since stdout expects bytes (not strings), we use std::io::Write instead of
std::fmt::Write . As a result, we give an empty vector as “writer” in our tests (its type
will be inferred to Vec<u8> ), in the assert_eq! we use a b"foo" . (The b pre�x
makes this a byte string literal so its type is going to be &[u8] instead of &str ).
Note: We could also make this function return a String , but that would change its
behavior. Instead of writing to the terminal directly, it would then collect everything
into a string, and dump all the results in one go at the end.
Exercise for the reader: writeln! returns an io::Result because writing can fail,
for example when the bu�er is full and cannot be expanded. Add error handling to
find_matches .
We’ve just seen how to make this piece of code easily testable. We have
Even though the goal was to make it testable, the result we ended up with is actually a very
idiomatic and reusable piece of Rust code. That’s awesome!
The way Rust deals with projects is quite �exible and it’s a good idea to think about what
to put into the library part of your crate early on. You can for example think about writing
a library for your application-speci�c logic �rst and then use it in your CLI just like any
other library. Or, if your project has multiple binaries, you can put the common
functionality into the library part of that crate.
your code.
There is a lot of code we aren’t testing, though: Everything that we wrote to deal with the
outside world! Imagine you wrote the main function, but accidentally left in a hard-coded
string instead of using the argument of the user-supplied path. We should write tests for
that, too! (This level of testing is often called “integration testing”, or “system testing”.)
At its core, we are still writing functions and annotating them with #[test] . It’s just a
matter of what we do inside these functions. For example, we’ll want to use the main
binary of our project, and run it like a regular program. We will also put these tests into a
new �le in a new directory: tests/cli.rs .
Aside: By convention, cargo will look for integration tests in the tests/ directory.
Similarly, it will look for benchmarks in benches/ , and examples in examples /. These
conventions also extend to your main source code: libraries have a src/lib.rs �le,
the main binary is src/main.rs , or, if there are multiple binaries, cargo expects them
to be in src/bin/<name>.rs . Following these conventions will make your code base
more discoverable by people used to reading Rust code.
To recall, grrs is a small tool that searches for a string in a �le. We have previously tested
that we can �nd a match. Let’s think about what other functionality we can test.
These are all valid test cases. Additionally, we should also include one test case for the
“happy path”, i.e., we found at least one match and we print it.
To make these kinds of tests easier, we’re going to use the assert_cmd crate. It has a
bunch of neat helpers that allow us to run our main binary and see how it behaves.
Further, we’ll also add the predicates crate which helps us write assertions that
assert_cmd can test against (and that have great error messages). We’ll add those
dependencies not to the main list, but to a “dev dependencies” section in our
Cargo.toml . They are only required when developing the crate, not when using it.
[dev-dependencies]
assert_cmd = "0.10"
predicates = "1"
This sounds like a lot of setup. Nevertheless – let’s dive right in and create our
tests/cli.rs �le:
#[test]
fn file_doesnt_exist() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("grrs")?;
cmd.arg("foobar").arg("test/file/doesnt/exist");
cmd.assert()
.failure()
.stderr(predicate::str::contains("No such file or directory"));
Ok(())
}
You can run this test with cargo test , just the tests we wrote above. It might take a little
longer the �rst time, as Command::cargo_bin("grrs") needs to compile your main
binary.
We’ll need to have a �le whose content we know, so that we can know what our program
should return and check this expectation in our code. One idea might be to add a �le to
the project with custom content and use that in our tests. Another would be to create
temporary �les in our tests. For this tutorial, we’ll have a look at the latter approach.
Mainly, because it is more �exible and will also work in other cases; for example, when
you are testing programs that change the �les.
To create these temporary �les, we’ll be using the tempfile crate. Let’s add it to the dev-
dependencies in our Cargo.toml :
tempfile = "3"
Here is a new test case (that you can write below the other one) that �rst creates a temp
第21页�le (a “named” one so we can get its path), �lls it with some text, and then runs2020/12/23
共40页 our 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
program to see if we get the correct output. When the file goes out of scope (at the end
of the function), the actual temporary �le will automatically get deleted.
#[test]
fn find_content_in_file() -> Result<(), Box<dyn std::error::Error>> {
let mut file = NamedTempFile::new()?;
writeln!(file, "A test\nActual content\nMore content\nAnother test")?;
Ok(())
}
Exercise for the reader: Add integration tests for passing an empty string as pattern.
Adjust the program as needed.
What to test?
While it can certainly be fun to write integration tests, it will also take some time to write
them, as well as to update them when your application’s behavior changes. To make sure
you use your time wisely, you should ask yourself what you should test.
In general it’s a good idea to write integration tests for all types of behavior that a user can
observe. That means that you don’t need to cover all edge cases: It usually su�ces to have
examples for the di�erent types and rely on unit tests to cover the edge cases.
It is also a good idea not to focus your tests on things you can’t actively control. It would
be a bad idea to test the exact layout of --help as it is generated for you. Instead, you
might just want to check that certain elements are present.
Depending on the nature of your program, you can also try to add more testing
techniques. For example, if you have extracted parts of your program and �nd yourself
writing a lot of example cases as unit tests while trying to come up with all the edge cases,
your should look into proptest . If you have a program which consumes arbitrary �les
and parses them, try to write a fuzzer to �nd bugs in edge cases.
Aside: You can �nd the full, runnable source code used in this chapter in this book’s
repository.
There are a few approaches, and we’ll look at three of them from “quickest to set up” to
“most convenient for users”.
Now that cargo as well as crates.io know you, you are ready to publish crates. Before you
hastily go ahead and publish a new crate (version), it’s a good idea to open your
Cargo.toml once more and make sure you added the necessary metadata. You can �nd
all the possible �elds you can set in the documentation for cargo’s manifest format. Here’s
a quick overview of some common entries:
[package]
name = "grrs"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]
license = "MIT OR Apache-2.0"
description = "A tool to search files"
readme = "README.md"
homepage = "https://github.com/you/grrs"
repository = "https://github.com/you/grrs"
keywords = ["cli", "search", "demo"]
categories = ["command-line-utilities"]
Note: This example includes the mandatory license �eld with a common choice for
Rust projects: The same license that is also used for the compiler itself. It also refers to
a README.md �le. It should include a quick description of what your project is about,
and will be included not only on the crates.io page of your crate, but also what GitHub
第23页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
We’ve seen how to publish a crate to crates.io, and you might be wondering how to install
it. In contrast to libraries, which cargo will download and compile for you when you run
cargo build (or a similar command), you’ll need to tell it to explicitly install binaries.
This is done using cargo install <crate-name> . It will by default download the crate,
compile all the binary targets it contains (in “release” mode, so it might take a while) and
copy them into the ~/.cargo/bin/ directory. (Make sure that your shell knows to look
there for binaries!)
It’s also possible to install crates from git repositories, only install speci�c binaries of a
crate, and specify an alternative directory to install them to. Have a look at cargo
install --help for details.
When to use it
cargo install is a simple way to publish a binary crate. It’s very convenient for Rust
developers to use, but has some signi�cant downsides: Since it will always compile your
source from scratch, users of your tool will need to have Rust, cargo, and all other system
dependencies your project requires to be installed on their machine. Compiling large Rust
codebases can also take some time.
Furthermore, there is no simple way to update tools installed with cargo: The user will
need to run cargo install again at some point, and pass the --force �ag to overwrite
the old binaries. This is a missing feature and there are subcommands like this one you
can install to add that, though.
It’s best to use this for distributing tools that are targeted at other Rust developers. For
example: A lot of cargo subcommands like cargo-tree or cargo-outdated can be
installed with it.
Distributing binaries
Rust is a language that compiles to native code and by default statically links all
dependencies. When you run cargo build on your project that contains a binary called
grrs , you’ll end up with a binary �le called grrs . Try it out: Using cargo build , it’ll be
target/debug/grrs , and when you run cargo build --release , it’ll be
target/release/grrs . Unless you use crates that explicitly need external libraries to be
第24页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
installed on the target system (like using the system’s version of OpenSSL), this binary will
only depend on common system libraries. That means, you take that one �le, send it to
people running the same operating system as you, and they’ll be able to run it.
This is already very powerful! It works around two of the downsides we just saw for
cargo install : There is no need to have Rust installed on the user’s machine, and
instead of it taking a minute to compile, they can instantly run the binary.
So, as we’ve seen, cargo build already builds binaries for us. The only issue is, those are
not guaranteed to work on all platforms. If you run cargo build on your Windows
machine, you won’t get a binary that works on a Mac by default. Is there a way to generate
these binaries for all the interesting platforms automatically?
If your tool is open sourced and hosted on GitHub, it’s quite easy to set up a free CI
(continuous integration) service like Travis CI. (There are other services that also work on
other platforms, but Travis is very popular.) This basically runs setup commands in a
virtual machine each time you push changes to your repository. What those commands
are, and the types of machines they run on, is con�gurable. For example: A good idea is to
run cargo test on a machine with Rust and some common build tools installed. If this
fails, you know there are issues in the most recent changes.
We can also use this to build binaries and upload them to GitHub! Indeed, if we run
cargo build --release and upload the binary somewhere, we should be all set, right?
Not quite. We still need to make sure the binaries we build are compatible with as many
systems as possible. For example, on Linux we can compile not for the current system, but
instead for the x86_64-unknown-linux-musl target, to not depend on default system
libraries. On macOS, we can set MACOSX_DEPLOYMENT_TARGET to 10.7 to only depend on
system features present in versions 10.7 and older.
You can see one example of building binaries using this approach here for Linux and
macOS and here for Windows (using AppVeyor).
Another way is to use pre-built (Docker) images that contain all the tools we need to build
binaries. This allows us to easily target more exotic platforms, too. The trust project
contains scripts that you can include in your project as well as instructions on how to set
this up. It also includes support for Windows using AppVeyor.
If you’d rather set this up locally and generate the release �les on your own machine, still
have a look at trust. It uses cross internally, which works similar to cargo but forwards
commands to a cargo process inside a Docker container. The de�nitions of the images are
also available in cross’ repository.
You point your users to your release page that might look something like this one, and
they can download the artifacts we’ve just created. The release artifacts we’ve just
generated are nothing special: At the end, they are just archive �les that contain our
binaries! This means that users of your tool can download them with their browser,
extract them (often happens automatically), and copy the binaries to a place they like.
This does require some experience with manually “installing” programs, so you want to
add a section to your README �le on how to install this program.
Note: If you used trust to build your binaries and added them to GitHub releases, you
can also tell people to run curl -LSfs https://japaric.github.io/trust
/install.sh | sh -s -- --git your-name/repo-name if you think that makes it
easier.
When to use it
Having binary releases is a good idea in general, there’s hardly any downside to it. It does
not solve the problem of users having to manually install and update your tools, but they
can quickly get the latest releases version without the need to install Rust.
Right now, when a user downloads our release builds, they will get a .tar.gz �le that
only contains binary �les. So, in our example project, they will just get a single grrs �le
they can run. But there are some more �les we already have in our repository that they
might want to have. The README �le that tells them how to use this tool, and the license
�le(s), for example. Since we already have them, they are easy to add.
There are some more interesting �les that make sense especially for command-line tools,
though: How about we also ship a man page in addition to that README �le, and con�g
�les that add completions of the possible �ags to your shell? You can write these by hand,
but clap, the argument parsing library we use (which structopt builds upon) has a way to
generate all these �les for us. See this in-depth chapter for more details.
think about how to install your program, if it can be installed the same way as they install
the other tools. These package managers also allow users to update their programs when
a new version is available.
Sadly, supporting di�erent systems means you’ll have to look at how these di�erent
systems work. For some, it might be as easy as adding a �le to your repository (e.g. adding
a Formula �le like this for macOS’s brew ), but for others you’ll often need to send in
patches yourself and add your tool to their repositories. There are helpful tools like cargo-
rpm, cargo-deb, and cargo-aur, but describing how they work and how to correctly
package your tool for those di�erent systems is beyond the scope of this chapter.
Instead, let’s have a look at a tool that is written in Rust and that is available in many
di�erent package managers.
An example: ripgrep
ripgrep is an alternative to grep / ack / ag and is written in Rust. It’s quite successful and
is packaged for many operating systems: Just look at the “Installation” section of its
README!
Note that it lists a few di�erent options how you can install it: It starts with a link to the
GitHub releases which contain the binaries so you can download them directly; then it lists
how to install it using a bunch of di�erent package managers; �nally, you can also install it
using cargo install .
This seems like a very good idea: Don’t pick and choose one of the approaches presented
here, but start with cargo install , add binary releases, and �nally start distributing
your tool using system package managers.
In-depth topics
A small collection of chapters covering some more details that you might care about when
writing your command line application.
Signal handling
Processes like command line applications need to react to signals sent by the operating
system. The most common example is probably Ctrl+C, the signal that typically tells a
process to terminate. To handle signals in Rust programs you need to consider how you
can receive these signals as well as how you can react to them.
Note: If your applications does not need to gracefully shutdown, the default handling
is �ne (i.e. exit immediately and let the OS cleanup resources like open �le handles). In
第27页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
However, for applications that need to clean up after themselves, this chapter is very
relevant! For example, if your application needs to properly close network
connections (saying “good bye” to the processes at the other end), remove temporary
�les, or reset system settings, read on.
Windows does not have signals. You can use Console Handlers to de�ne callbacks that get
executed when an event occurs. There is also structured exception handling which
handles all the various types of system exceptions such as division by zero, invalid access
exceptions, stack over�ow, and so on
fn main() {
ctrlc::set_handler(move || {
println!("received Ctrl+C!");
})
.expect("Error setting Ctrl-C handler");
This is, of course, not that helpful: It only prints a message but otherwise doesn’t stop the
program.
In a real-world program, it’s a good idea to instead set a variable in the signal handler that
you then check in various places in your program. For example, you can set an
Arc<AtomicBool> (a boolean shareable between threads) in your signal handler, and in
第28页hot loops, or when waiting for a thread, you periodically check its value and break
共40页 when 下午6:19
2020/12/23 it
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
becomes true.
thread::spawn(move || {
for sig in signals.forever() {
println!("Received signal {:?}", sig);
}
});
Ok(())
}
Using channels
Instead of setting a variable and having other parts of the program check it, you can use
channels: You create a channel into which the signal handler emits a value whenever the
signal is received. In your application code you use this and other channels as
synchronization points between threads. Using crossbeam-channel it would look
something like this:
use std::time::Duration;
use crossbeam_channel::{bounded, tick, Receiver, select};
use anyhow::Result;
Ok(receiver)
}
loop {
select! {
recv(ticks) -> _ => {
println!("working!");
}
recv(ctrl_c_events) -> _ => {
println!();
println!("Goodbye!");
break;
}
}
}
Ok(())
}
There are multiple solutions to this, some being more low-level than others.
The easiest crate to use for this is confy . It asks you for the name of your application and
requires you to specify the con�g layout via a struct (that is Serialize , Deserialize )
and it will �gure out the rest!
This is incredibly easy to use for which you of course surrender con�gurability. But if a
simple con�g is all you want, this crate might be for you!
Con�guration environments
TODO
Exit codes
A program doesn’t always succeed. And when an error occurs, you should make sure to
emit the necessary information correctly. In addition to telling the user about errors, on
most systems, when a process exits, it also emits an exit code (an integer between 0 and
255 is compatible with most platforms). You should try to emit the correct code for your
第31页program’s
共40页 state. For example, in the ideal case when your program succeeds, 2020/12/23
it should exit
下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
with 0 .
When an error occurs, it gets a bit more complicated, though. In the wild, many tools exit
with 1 when a common failure occurs. Currently, Rust sets an exit code of 101 when the
process panicked. Beyond that, people have done many things in their programs.
So, what to do? The BSD ecosystem has collected a common de�nition for their exit codes
(you can �nd them here). The Rust library exitcode provides these same codes, ready to
be used in your application. Please see its API documentation for the possible values to
use.
After you add the exitcode dependency to your Cargo.toml , you can use it like this:
fn main() {
// ...actual work...
match result {
Ok(_) => {
println!("Done!");
std::process::exit(exitcode::OK);
}
Err(CustomError::CantReadConfig(e)) => {
eprintln!("Error: {}", e);
std::process::exit(exitcode::CONFIG);
}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(exitcode::DATAERR);
}
}
}
Most importantly, be consistent in the style of communication. Use the same pre�xes and
sentence structure to make the logs easily skimmable.
Try to let your application output tell a story about what it’s doing and how it impacts the
user. This can involve showing a timeline of steps involved or even a progress bar and
indicator for long-running actions. The user should at no point get the feeling that the
application is doing something mysterious that they cannot follow.
Because of this, it’s important to de�ne the severity of events and messages that are
related to it; then use consistent log levels for them. This way users can select the amount
of logging themselves via --verbose �ags or environment variables (like RUST_LOG ).
The commonly used log crate de�nes the following levels (ordered by increasing
severity):
trace
debug
info
warning
error
It’s a good idea to think of info as the default log level. Use it for, well, informative output.
(Some applications that lean towards a more quiet output style might only show warnings
and errors by default.)
Additionally, it’s always a good idea to use similar pre�xes and sentence structure across
log messages, making it easy to use a tool like grep to �lter for them. A message should
provide enough context by itself to be useful in a �ltered log while not being too verbose
at the same time.
When panicking
One aspect often forgotten is that your program also outputs something when it crashes.
In Rust, “crashes” are most often “panics” (i.e., “controlled crashing” in contrast to “the
operating system killed the process”). By default, when a panic occurs, a “panic handler”
will print some information to the console.
For example, if you create a new binary project with cargo new --bin foo and replace
the content of fn main with panic!("Hello World") , you get this when you run your
program:
This is useful information to you, the developer. (Surprise: the program crashed because
of line 2 in your main.rs �le). But for a user who doesn’t even have access to the source
code, this is not very valuable. In fact, it most likely is just confusing. That’s why it’s a good
idea to add a custom panic handler, that provides a bit more end-user focused output.
One library that does just that is called human-panic. To add it to your CLI project, you
import it and call the setup_panic!() macro at the beginning of your main function:
use human_panic::setup_panic;
fn main() {
setup_panic!();
panic!("Hello world")
}
第34页This
共40页will now show a very friendly message, and tells the user what they can do:
2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
foo had a problem and crashed. To help us diagnose the problem you can send
us a crash report.
Expect the output of every program to become the input to another, as yet
unknown, program.
If our programs ful�ll this expectation, our users will be happy. To make sure this works
well, we should provide not just pretty output for humans, but also a version tailored to
what other programs need. Let’s see how we can do this.
Aside: Make sure to read the chapter on CLI output in the tutorial �rst. It covers how
to write output to the terminal.
use atty::Stream;
if atty::is(Stream::Stdout) {
println!("I'm a terminal");
} else {
println!("I'm not");
}
Depending on who will read our output, we can then add extra information. Humans
tend to like colors, for example, if you run ls in a random Rust project, you might see
something like this:
$ ls
CODE_OF_CONDUCT.md LICENSE-APACHE examples
CONTRIBUTING.md LICENSE-MIT proptest-regressions
Cargo.lock README.md src
Cargo.toml convey_derive target
Because this style is made for humans, in most con�gurations it’ll even print some of the
names (like src ) in color to show that they are directories. If you instead pipe this to a �le,
or a program like cat , ls will adapt its output. Instead of using columns that �t my
terminal window it will print every entry on its own line. It will also not emit any colors.
$ ls | cat
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Cargo.lock
Cargo.toml
LICENSE-APACHE
LICENSE-MIT
README.md
convey_derive
examples
proptest-regressions
src
target
This often means that output was limited to what is easy to parse. Formats like TSV (tab-
separated values), where each record is on its own line, and each line contains tab-
第36页 共40页 2020/12/23 下午6:19
Command Line Applications in Rust https://rust-cli.github.io/book/print.html
separated content, are very popular. These simple formats based on lines of text allow
tools like grep to be used on the output of tools like ls . | grep Cargo doesn’t care if
your lines are from ls or �le, it will just �lter line by line.
The downside of this is that you can’t use an easy grep invocation to �lter all the
directories that ls gave you. For that, each directory item would need to carry additional
data.
Still, it’s a good idea to choose a format that is easily parsable in most programming
languages/environments. Thus, over the last years a lot of applications gained the ability
to output their data in JSON. It’s simple enough that parsers exist in practically every
language yet powerful enough to be useful in a lot of cases. While its a text format that can
be read by humans, a lot of people have also worked on implementations that are very
fast at parsing JSON data and serializing data to JSON.
In the description above, we’ve talked about “messages” being written by our program.
This is a good way of thinking about the output: Your program doesn’t necessarily only
output one blob of data but may in fact emit a lot of di�erent information while it is
running. One easy way to support this approach when outputting JSON is to write one
JSON document per message and to put each JSON document on new line (sometimes
called Line-delimited JSON). This can make implementations as simple as using a regular
println! .
Here’s a simple example, using the json! macro from serde_json to quickly write valid
JSON in your Rust source code:
use structopt::StructOpt;
use serde_json::json;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(StructOpt)]
struct Cli {
/// Output JSON instead of human readable messages
#[structopt(long = "json")]
json: bool,
}
fn main() {
let args = Cli::from_args();
if args.json {
println!("{}", json!({
"type": "message",
"content": "Hello world",
}));
} else {
println!("Hello world");
}
}
$ cargo run -q
Hello world
$ cargo run -q -- --json
{"content":"Hello world","type":"message"}
(Running cargo with -q suppresses its usual output. The arguments after -- are passed
to our program.)
ripgrep is a replacement for grep or ag, written in Rust. By default it will produce output
like this:
$ rg default
src/lib.rs
37: Output::default()
src/components/span.rs
6: Span::default()
$ rg default --json
{"type":"begin","data":{"path":{"text":"src/lib.rs"}}}
{"type":"match","data":{"path":{"text":"src/lib.rs"},"lines":{"text":"
Output::default()\n"},"line_number":37,"absolute_offset":761,"submatches":
[{"match":{"text":"default"},"start":12,"end":19}]}}
{"type":"end","data":{"path":
{"text":"src/lib.rs"},"binary_offset":null,"stats":{"elapsed":
{"secs":0,"nanos":137622,"human":"0.000138s"},"searches":1,"searches_with_match":1,"
{"type":"begin","data":{"path":{"text":"src/components/span.rs"}}}
{"type":"match","data":{"path":{"text":"src/components/span.rs"},"lines":
{"text":"
Span::default()\n"},"line_number":6,"absolute_offset":117,"submatches":
[{"match":{"text":"default"},"start":10,"end":17}]}}
{"type":"end","data":{"path":{"text":"src/components
/span.rs"},"binary_offset":null,"stats":{"elapsed":
{"secs":0,"nanos":22025,"human":"0.000022s"},"searches":1,"searches_with_match":1,"b
{"data":{"elapsed_total":
{"human":"0.006995s","nanos":6994920,"secs":0},"stats":
{"bytes_printed":533,"bytes_searched":11285,"elapsed":
{"human":"0.000160s","nanos":159647,"secs":0},"matched_lines":2,"matches":2,"searche
As you can see, each JSON document is an object (map) containing a type �eld. This
would allow us to write a simple frontend for rg that reads these documents as they
come in and show the matches (as well the �les they are in) even while ripgrep is still
searching.
Aside: This is how Visual Studio Code uses ripgrep for its code search.
Even if you do not use this library, it might be a good idea to write a similar abstraction
that �ts your use case.
Both can be automatically generated when using clap v3 (in unreleased beta, at time of
writing), via the man backend.
#[derive(Clap)]
pub struct Head {
/// file to load
#[clap(parse(from_os_str))]
pub file: PathBuf,
/// how many lines to print
#[clap(short = "n", default_value = "5")]
pub count: usize,
}
Secondly, you need to use a build.rs to generate the manual �le at compile time from
the de�nition of your app in code.
There are a few things to keep in mind (such as how you want to package your binary) but
for now we simply put the man �le next to our src folder.
use clap::IntoApp;
use clap_generate::gen_manuals;
#[path="src/cli.rs"]
mod cli;
fn main() {
let app = cli::Head::into_app();
for man in gen_manuals(&app) {
let name = "head.1";
let mut out = fs::File::create("head.1").unwrap();
use std::io::Write;
out.write_all(man.render().as_bytes()).unwrap();
}
}
When you now compile your application there will be a head.1 �le in your project
directory.
If you open that in man you’ll be able to admire your free documentation.