11 Nov 2018

Improving CLI Ergonomics

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).

I want 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. He says:

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 <username> and <auth-token> are not the same type of argument, and it’s arguably even worse because the <auth-token> is optional here.

I opened 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.

Introducing Clap

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 --help request. 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 ArgMatches into supernova’s 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 ArgMatches and Config.

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 Into trait. However, the documentation for Into states:

Library authors should not directly implement this trait, but should prefer implementing the From trait, which offers greater flexibility and provides an equivalent Into implementation for free, thanks to a blanket implementation in the standard library.

So, instead of implementing Into, I should implement the conversion of Config from 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.

The From Trait

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. Implementing From also let me replace another method in Config which reduces the surface area of Config’s implementation.

I’m not totally happy with implementing From on 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!

04 Nov 2018

Open Source Level Up: Becoming a Maintainer

After a month of open source following my return to open source, the next step in my journey is to really immerse myself in one or two projects and start making larger contributions. The open source course I’m taking is encouraging us to complete contributions to three “external” open source projects over the next 5 weeks, as well as make three contributions to “internal” projects during the same period. In this case, “external” refers to established projects in the open source community and “internal” refers to projects we as a class are starting ourselves to get a feel for being core maintainers of a project so we can gain experience on both sides of the process. As a class we had a brainstorming session to come up with ideas for internal projects we could start, and I donated one of my existing side-projects to the list, which has lead to…

Level Up!

With absolutely zero fanfare, hoops to jump through, or documents to sign, I suddenly became the initial maintainer of one of our course’s internal projects. I was able to generate enough interest in my side-project that members of my class wanted to contribute, and the project was added to the official list of internal projects. The amazing thing about open source development is you don’t need anyone’s permission or to be told to create something; if you have an idea and enough people are interested in using and/or growing that idea, you have an open source project. The project itself grows in a very organic way as people find out about the project and drop in to see what’s going on. Some people just drop in and perhaps contribute a little bit or share their use-case for the project, while others get started and then decide to stick around for a long time; that is the nature of open source.

supernova

The project I am maintaining is 0xazure/supernova, and I would really encourage you to come check us out! We’re still in the early stages, but we would encourage any and all to take a look at what we’re building, make comments and submit issues or pull requests, and give us feedback. Even though it has been deemed an “internal” project for the course, we still welcome contributions from anyone who wants to get involved so stop by and file an issue if you have any questions.

supernova started out as a learning project for me to improve my understanding of the Rust programming language, a {,de}serializing library written in Rust called Serde, and the GitHub API. I am a project-oriented learner, so whenever I have some tools and techniques I’d like to learn or improve, I start a project that tries to use them so I can have a better understanding of their strengths and weaknesses. The initial implementation as a CLI tool was designed to pull stars data from the GitHub API and display it in a formatted list. I have a tendency to use stars like I would bookmarks so I have accumulated a lot of stars since I joined GitHub, but I have no great way to view all of my stars in one place (GitHub insists on paginating the list) or export them in various formats. supernova was only added to the internal projects list a week ago, but we already have a number of open issues to start growing supernova’s feature set.

First Week

In the first week we’ve been focusing on a lot of the low-hanging fruit for a new open source project: setting up the project infrastructure and documenting what we already have.

There are currently a few open issues for writing docs:

as well as the ever-present need to document existing code.

Within the first week we’ve also set up and enabled TravisCI for testing and linting our codebase thanks to Sean Prashad and made supernova more accessible to new contributors by converting existing documentation to Markdown thanks to Mordax. I really enjoyed landing these contributions; it’s really exciting to see something I started as a side-project to get better at writing Rust grow and take on a life of its own.

Next Week

In the next week or so we hope to have a lot of the initial set up completed so that we can move on to adding functionality. My personal goal for next week is to submit a pull request for improving the usability of supernova by following Guideline #2 of 12 Factor CLI Apps to “prefer flags to args” to make input to the CLI more explicit and clear, as well as many other usability benefits.

I also need to start thinking about what external project(s) I want to contribute to over the next few weeks; I have some ideas from my contributions during Hackoberfest, but I’ll need to make a decision soon so I can get set up to contribute.