Here's a short description how I create command line programs in the Rust language. This post is something I can point people at when they ask questions. It will also inevitably provoke people to tell me of better ways.

I write command line programs often, and these days mostly in Rust. By often I mean at least one per week. They're usually throwaway experiments: I usually start with cargo init /tmp/foo, and only if it seems viable do I move it to my home directory. Too often they turn into long-lived projects that I have to maintain.

The more important thing in my tool box for this is the clap crate, and its derive feature.

cargo add clap --features derive

This allows me to define the command line syntax using Rust type declarations. The following struct defines an optional string argument, with a default value, and an option -f or --filename that takes a filename value. The code below is all I need to write.

use clap::Parser;

#[derive(Parser)]
struct Args {
    #[clap(default_value = "world")]
    whom: String,

    #[clap(short, long)]
    filename: Option<PathBuf>,
}

clap also support subcommands: for example, my command line password manager supports commands like the following:

sopass value list
sopass value show
sopass value add foo bar

I happen to find the multiple levels of subcommands natural, even if they are a recent evolution in Unix command line conventions. clap allows them, but of course doesn't require subcommands at all, never mind multiple levels.

To implement subcommands, I define an enum with one variant per subcommand, and the variant contains a type that implements the subcommand.

#[derive(Parser)]
struct Args {
    cmd: Cmd;
}

#[derive(Parser)]
enum Cmd {
    Greet(GreetCmd),
    ...
}

#[derive(Parser)]
struct GreetCmd {
    #[clap(default_value = "world")]
    whom: String,
}

impl GreetCmd {
    fn run(&self) -> Result<(), anyhow::Error> {
        println!("hello, {}", self.whom);
        Ok(())
    }
}

The main program then uses these:

let args = Args::parse();
match &args.cmd {
    Cmd::Greet(x) => x.run()?,
    ...
}

When I want to have multiple levels of subcommands, I define a trait for the lowest level, or leaf command:

pub trait Leaf {
    type Error;
    fn run(&self, config: &Config) -> Result<(), Self::Error>;
}

I implement that trait for every leaf command struct.

I define all the non-leaf commands in the main module, so they're conveniently in one place. Each non-leaf command needs to match on its subcommand type and call the run method for the value contained in each variant, like I did above.

This results in a bit of repetitive code, but it's not too bad. It's certainly not bad enough that I've ever wanted to either generate code in build.rs or define a macro.

This is what I do. I find it reasonably convenient, despite being a little repetitive. I'm sure there are other approaches that suit other people better.