In my day-to-day, I use a lot of command-line tools.
I generally find myself to be much more productive working in a non-GUI environment, mostly down to the fact that I can type much faster than I can move and aim a mouse and the less time I spend switching between keyboard and mouse the more time I can spend typing.
Unfortunately, command-line tools have one major drawback: discoverability.
If I am given a GUI application, I can click around, hover over different UI elements, and generally get a feel for how the interface is laid out.
In a command-line tool, I don’t have any of these visual cues to help me learn the functionality; I have to start typing and see what happens.
To supplement this lack of visual cues, good command-line tools generally have autocomplete snippets for your shell so you can type the command name and start hitting
TAB to see what options are available at a particular time, as well as extensive
man pages that describe all the various options, commands, and any sub-commands (though these
man pages are often extraordinarily verbose and rarely provide useful examples of how the tool is commonly used, so alternatives such as
tldr fill this gap).
supernova to be a good command-line tool, so I have been thinking about ways to improve discoverability and make the tool easier to use.
A really interesting post by Jeff Dickey, an engineer at Heroku, crossed my timeline recently titled 12 Factor CLI Apps
In his post, Jeff provides twelve principles to guide CLI design in a similar fashion to Heroku’s original Twelve-Factor App methodology.
Out of the twelve principles, Principle 2 stood out to me, particularly in the context of
supernova; Jeff suggests to “prefer flags to args” when designing a CLI.
Sometimes args are just fine though when the argument is obvious such as
$rm file_to_remove. A good rule of thumb is 1 type of argument is fine, 2 types are very suspect, and 3 are never good.
This rule of thumb got me thinking about
supernova’s current calling convention.
Currently, you would use
supernova like this:
$ supernova <username> [<auth-token>]
This violates Jeff’s rule of thumb because
<auth-token> are not the same type of argument, and it’s arguably even worse because the
<auth-token> is optional here.
0xazure/supernova#16 to track this problem with our current calling convention.
In it, I suggest using the clap crate to improve our CLI and provide the facilities to do command-line argument parsing.
Clap is a great crate that makes it super easy to add all kinds of CLI goodies to your command-line tool including auto-generated help, version, and usage information which are all, as Jeff highlights in Principle 1: Great help is essential, important to good CLIs.
Adding clap to the project would check off Principles 1 & 2, so I went ahead and created a pull request to add clap and improve
supernova’s calling convention. All that was necessary was to create a new clap-based
App with the desired arguments and flags, provide good help messages, and set
<auth-token> to be an optional argument.
Having done all that, this is what clap generates, all for 13 lines of code:
$ supernova --help supernova 0.1.0 USAGE: supernova [OPTIONS] <USERNAME> FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: -t, --token <TOKEN> Sets the authentication token for requests to GitHub ARGS: <USERNAME> The user whose stars to collect
Clap will print the name as well as the version with every
I was a little wary of this at first, because while I definitely want to include this information in my CLI, I also don’t want the maintenance burden of having to remember to update the version string in
main.rs every time a new version is published.
However, clap exposes some very handy macros that make use of environment variables exported by
cargo at build time to pull this information out of
Cargo.toml, so these values will always be up to date with the crate’s metadata and don’t require setting the values in code.
To actually extract all of the parsed arguments from clap, we call
App::get_matches() which produces an
ArgMatches struct we can query for specific arguments by name. Instead of parsing out each argument, I decided to try my hand at implementing the
From trait to convert
Config type so it can be passed directly to the next function call.
Traits in Rust
Before I talk about implementing the
From trait, I want to quickly talk about what traits actually are, as well as why I chose to implement
From instead of one of the other conversion traits.
The Rust Book explains traits like this:
A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way.
If you are familiar with the idea of interfaces in other languages such as Java or Go, traits are a very similar idea.
There is an important caveat with traits. Again, from the Rust Book, Chapter 10.2, Implementing a Trait on a Type:
One restriction to note with trait implementations is that we can implement a trait on a type only if either the trait or the type is local to our crate.
This means that I will need to implement my conversion on a type that I control,
Config, to convert between
The restriction on traits is very interesting, because while it is not necessary in my case it raises the question of how to convert from types that I do control into types defined in the standard library or in other crates.
If I wanted to convert a
Config object into a
String for example, it might make sense to use the conveniently named
However, the documentation for
Library authors should not directly implement this trait, but should prefer implementing the
Fromtrait, which offers greater flexibility and provides an equivalent
Intoimplementation for free, thanks to a blanket implementation in the standard library.
So, instead of implementing
Into, I should implement the conversion of
String, and I get the equivalent
Into implementation for free?
It seems a bit backwards since we are defining a trait that converts in the opposite direction of our needs, but because of this blanket implementation in the standard library we get symmetric conversions between types for free as long as we implement
From on the type.
That means we only have to write the conversion function once instead of needing to write it once in each direction, and I’m definitely in favour of anything that reduces the amount of code I need to write.
Actually implementing the
From trait was straight-forward, other than the necessary addition of a lifetime annotation because of how
ArgMatches is implemented on clap’s side.
From also let me replace another method in
Config which reduces the surface area of
I’m not totally happy with implementing
Config because I had to pull clap into the library side of
supernova instead of leaving arguments parsing completely in
main, so I may go back and change this implementation to provide better separation between data and arguments parsing.
Or I may decide to move
Config out to its own module which would also increase separation of concerns.
Either way, I was very happy to get my hands back on the keyboard writing Rust code this week, and I hope to be writing more in the near future!