Getting Started with a Rust CLI

Image of Author
October 7, 2023 (last updated October 9, 2023)

Introduction

This guide is how I would start a new Rust CLI project. See Getting Good at Getting Started for the philosophy behind this guide.

Here are some aliases you might encounter along the way:

  • g is my zsh alias for git

rustup

rustup is the Rust toolchain installer. It install Rust and various CLI tools. It is also the Rust version manager. It also installs Rust's build system and package manager: cargo.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

cargo

cargo new creates a new project. We will call this one app.

cargo new app
cd app

At this point you have a "hello world" that you can run.

cargo run
#=> "Hello, world!"

This command (in addition to cargo build) created a binary at ./target/debug/app. You can run $ ./target/debug/app to see the same output. The "debug" build is not optimized. The optimized binary build is called a "release". It is stored in ./target/release/app.

cargo build --release
./target/release/app
#=> "Hello, world!"

cargo install

You can install the binary locally on $PATH. It will build the release version and install it in a directory that is (probably) already on your $PATH.

cargo install --path .
which add
#=> (on Mac) /Users/[username]/.cargo/bin/app
add
#=> "Hello, world!"

git and github

cargo new will implicitly call git init when creating your project. At this point you can write your first commit, create a github repo, and push.

g commit "First commit."
gh repo create
#=> Github CLI workflow for creating a new repo

clap

clap is one of the most popular CLI libraries in Rust.

cargo add clap --features derive

derive is a clap optional feature that allows for deriving CLI commands from structs. It is also clap's recommended method for building CLIs. You can learn more via clap's derive tutorial.

clap stands for "command line argument parser". Parsing command line arguments is what it does. You give it a struct that instructs it on how to parse. In return it builds helpful features, such as a help menu. What you do with your parsed command line arguments is up to your main function.

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Adds numbers
    #[command()]
    Add {
        /// first number
        x: i32,
        /// second number
        y: i32,
    },
}

fn main() {
    let args = Cli::parse();

    match args.command {
        Commands::Add { x, y } => {
            let sum = x + y;
            println!("{x} + {y} = {sum}")
        }
    }
}

Tests

I don't understand what is happening under the hood here, but clap has a builtin testing apparatus described in derive tutorial chapter 4.

struct Cli {
  // ...
}

fn main() {
  // ...
}

#[test]
fn verify_cli() {
    use clap::CommandFactory;
    Cli::command().debug_assert()
}

CICD

You can use github actions to test your Rust CLI remotely. I slightly modify the quickstart cicd workflow from actions-rs, so I'll just link to it and not show my minor modifications.

Documentation

Generate documentation for your package and all of it's dependencies.

cargo doc

It is saved in ./target/doc and can be opened locally in the browser.

cargo doc --open

Publishing

You can publish your crate to crates.io. See more at The Cargo Book: 3.9. Publishing crates on crates.io

You can alternatively publish on OS package manager. For example, see the docs on adding a formula to brew.

Full disclosure I have not done either of these in general, yet.