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.