Ideas for crafting CLI in Rust
Tags: rust, cli, terminal
Published: 2023-06-30 00:21:31

I'm a big fan of Rust. My first real world usage of Rust was a cli I created for a hobby project.

In this article I want to share few ideas I used to create cli called `upvpn` for [UpVPN](https://upvpn.app) to mange Serverless VPN sessions. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnjgu5feh0ttymi8fdp5.gif)

### Idea: Subcommand Pattern

A lot of simple clis have following pattern: A cli with many sub-commands and each of the sub-command having its own arguments:

```
<cli-name> <sub-command> [<arg>] ...
```

Overtime we want to be able to add sub-command easily. 

To do so, lets create a mental model of adding a new sub-command to our cli:
1. Each sub-command is represented as an `enum` variant. 
2. Arguments or data to a particular sub-command is represented as `struct`. 
3. To execute sub-command we call `run()` method on `struct` holding all the arguments.

Lets expand each point in detail:

#### Point #1 and #2

First two are simple use cases of [clap.rs](https://clap.rs):

```rust
use clap::{Parser, Subcommand};

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

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Sign in to your https://upvpn.app account
    SignIn(SignIn),
    /// Sign out current device
    SignOut(SignOut),
    /// Current VPN status
    Status(Status),
    /// Available locations for VPN
    Locations(ListLocations),
    /// Connect VPN
    Connect(Connect),
    /// Disconnect VPN
    Disconnect(Disconnect),
}
```

Here each of the variants `SignIn`, `Locations` etc. will be available as sub-command on cli with lower case. And their arguments are stored as data in the `struct` fields.

For instance, `SignIn` has optional argument to hold email of the user:

```rust
[derive(Args, Debug)]
pub struct SignIn {
    email: Option<String>,
}
```

### Point #3

We define a trait `RunCommand` for each of the data structs to derive, so that we can execute the command by simply calling `run()` on it. 

```rust
#[async_trait]
pub trait RunCommand {
    async fn run(self) -> Result<(), CliError>;
}
```

For instance, `SignIn` will be implemented it like this:

```rust
[derive(Args, Debug)]
pub struct SignIn {
    email: Option<String>,
}

#[async_trait]
impl RunCommand for SignIn {
    async fn run(self) -> Result<(), CliError> {
       // todo: implement sign in logic.
       // arguments to cli available here as data fields on self
    }
}
```

Having this trait makes main driver of whole cli very simple: 

```rust
impl Cli {
    pub async fn run(self) -> ExitCode {
        let output = match self.command {
            Commands::SignIn(sign_in) => sign_in.run().await,
            Commands::SignOut(sign_out) => sign_out.run().await,
            Commands::Locations(list_locations) => list_locations.run().await,
            Commands::Connect(connect) => connect.run().await,
            Commands::Disconnect(disconnect) => disconnect.run().await,
            Commands::Status(status) => status.run().await,
        };
       
        // process error in output to return exit code
     }
}

```

### Idea: Error messages for user

Having `RunCommand` trait with single method with return type `Result<(), CliError>` has another benefit, because now we only need to implement `Display` for `CliError` for displaying errors in a user friendly way. 

[thiserror](https://github.com/dtolnay/thiserror) crate makes it easy to do so using macros on `CliError` enum.


### Idea: Graceful Termination

Certain cli operations may be long running in nature, in this situation cli user should be able to exit it gracefully by `SIGTERM` or `ctrl+c`. 

To handle it would require a dedicated async task (or dedicated thread) to listen to these system events and notify rest of the system through oneshot or broadcast `channels`, or as simple as printing a message and exiting whole process by `std::process::exit(code)`. 

Tokio's doc on the [Shutdown topic](https://tokio.rs/tokio/topics/shutdown) is very informative. 

### Idea: Configuration

Cli can take its global configuration from files (json, toml), environment variables or even combination of them. 

[Figment](https://github.com/sergiobenitez/figment) makes merging configuration from various sources very easy. 


### Idea: Progress, Colors and User Input

Colors and Progress of a cli not only make cli pleasant to use, but can convey important information to user about errors, success, and progress of long running operations. 

When asking user input from list of many options, make it easy to filter by typing few characters through `dialoguer::FuzzySelect`. 

When asking user for password, keep it stay hidden using `dialoguer::Password`

[dialoguer](https://docs.rs/dialoguer/latest/dialoguer/), [indicatif](https://docs.rs/indicatif/latest/indicatif/), and [console](https://docs.rs/console/latest/console/) are your best friends for a good cli UX.


That's all! For more, checkout this [book](https://rust-cli.github.io/book/index.html) on building cli in Rust.

To see it all together, checkout cli code in [upvpn-app](https://github.com/upvpn/upvpn-app/tree/main/upvpn-cli) Github repository.