Writing command line utilities in Rust

Laziness is an important skill to have as a developer. For me laziness is the key driver behind automation and workflow optimisation. Over the past couple of months I have channeled my laziness into building command-line utilities to automate common or tedious tasks. In this post I will talk about which tools I use to build CLIs (command-line interface) and some examples of tools I’ve built.

Rust

My language of choice for building CLIs is Rust, even though Ruby is by far the language I know the best. Rust is great for CLIs for a few reasons:

Strong types

I am a strong believer in test driven development, but for small command-line utilities I often don’t feel the need to have tests. However I have experienced coming back to something I wrote six months ago and having a hard time changing things because I didn’t write tests. Years of strict TDD makes you worried when changing code that isn’t tested.

With Rust I feel less need to have tests, because its type system is great and catches lots of silly mistakes you might otherwise deploy.

I have found it to be straight forward to make changes to Rust code I haven’t look at for a couple of months.

Precompiled binaries

Rust makes distributing your tools really easy, because it compiles to a static binary. That means if I compile something on macOS, I can send the binary to my colleagues (who also use macOS) and they can just run it right away. There is no need for them to install Rust and compile the tool themselves.

In the past I’ve used Ruby for CLIs but those of my colleagues who didn’t use Ruby daily had a hard time because installing Ruby on macOS isn’t trivial.

Parsing command-line arguments with structopt

Parsing command-line arguments is not easy. Imagine a fictional git workflow kind of tool that can merge branches. You can imagine calling it in the following ways:

gittool merge my-branch
gittool merge my-branch --into master
gittool merge --into master my-branch
gittool merge my-branch --into master --rebase
gittool merge --into master my-branch --rebase
gittool merge my-branch -rebase -i master

There is lots of complexity around subcommands, arguments, options that take one or more arguments, flags that don’t take arguments, and making sure that order doesn’t matter.

structopt is a Rust library that handles all that complexity for you in the most elegant way I have ever seen. Rather than having some complicated API for you to set which options your CLI has you just define a data type that represents your subcommands and arguments, and structopt will generate Rust code for parsing the arguments. Here is how you could implement the gittool merge command from above:

use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "gittool")]
enum GitTool {
    #[structopt(name = "merge")]
    Merge {
        branch: String,

        #[structopt(
            short = "i",
            long = "into",
            default_value = "master")]
        into: String,

        #[structopt(long = "no-rebase")]
        no_rebase: bool,
    },
}

fn main() {
    let options = GitTool::from_args();
    println!("{:?}", options);
}

With Rust’s powerful code generation features, this really is all it takes. We can now call our tool like with gittool merge my-branch --into staging --no-rebase. Doing so will print Merge { branch: "my-branch", into: "staging", no_rebase: true }.

This means instead of dealing with parsing of command-line arguments, we just deal with a value of type GitTool which is a normal Rust enum.

It even generates awesome docs:

$ gittool -h
gittool 0.1.0
David Pedersen <david.pdrsn@gmail.com>

USAGE:
    gittool <SUBCOMMAND>

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

SUBCOMMANDS:
    help     Prints this message or the help of the given subcommand(s)
    merge


$ gittool merge -h
gittool-merge 0.1.0
David Pedersen <david.pdrsn@gmail.com>

USAGE:
    gittool merge [FLAGS] [OPTIONS] <branch>

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

OPTIONS:
    -i, --into <into>     [default: master]

ARGS:
    <branch>

Tonsser’s tools

Tonsser has several internal tools made in this way. Here are the ones we use the most:

api-git

api-git is a tool similar to the one I described above. We mostly use it to merge branches and deploy to our different environments. Calling api-git merge my-branch will do the following:

  1. Make sure the master branch is up to date
  2. Rebase master onto my-branch, unless called with --no-rebase
  3. Merge my-branch into master
  4. Push master so CI can run tests the deploy
  5. Merge my-branch into our staging and development branches. Once these branches pass CI they will also be deployed
  6. Remove my-branch locally and on GitHub

All this done with 21 Git commands.

Additionally if something fails at any point it will pause, tell you which step it was at, and let you fix the error. You can then continue running from the step it told you. Here is an example:

$ api-git merge my-branch
...

-- Running step 17: git merge --no-edit master
# stdout/stderr will appear here

Step 17 failed. Fix the problem and rerun with:

  api-git merge --no-rebase --into master my-branch --from-step 17

# manually fix the error
# continue where we left off with the command it gave us

$ api-git merge --no-rebase --into master my-branch --from-step 17 

This tool doesn’t do anything fancy but it guarantees you that you’ll always merge in the correct way and that proper clean up will be done. I consider this tool an invaluable part of the backend team’s workflow.

phrase-upload-keys

We use PhraseApp to manage our translations. Their web UI is fine but for some reason there is no way to create lots of strings all at once. That means you have to do lots tedious clicking if some sprint requires 20 new strings.

So I made a small wrapper around their HTTP API which can create strings in bulk. It is open source and you can find the code here https://github.com/tonsser/phrase-upload-keys

This tool has become the primary way I add strings to PhraseApp and it has saved me thousands of clicks.

ci

ci is a small terminal dashboard for our CI service that shows build outcome for each of your local branches.

Running it looks like so:

~/dev/major/api 15:14:49 master@c3abfd8be
$ ci
some-other-branch ok
           master ok
          develop ok
          staging failed 15633

The 15633 is the build number, which I use for another tool which will find the tests that failed in a particular build. This means I basically don’t have to open CI in a browser, I can do everything through the terminal.

It currently only supports CircleCI, which is what the backend team uses. You can find the code here https://github.com/tonsser/ci-dashboard

If you want to see Tonsser’s other open source tools and libraries head over to https://github.com/tonsser. If you want to dive deeper into building CLIs with Rust I recommend the “Command line apps in Rust” book.

This post is written by David Pedersen
David is Backend Engineer at Tonsser
On Twitter On GitHub

Like what you're reading? Then subscribe to our newsletter
No spam, just one email every two weeks with the posts we published in that time