We’ll be building out a new CLI named garden
, so in a new directory we can run cargo init
.
❯ cargo init --name garden
Created binary (application) package
This gives us the usual Cargo project with a src/main.rs
.
Since we’ve used clap before in other workshops, we’ll immediately roll into adding clap as a dependency and enabling both the env
feature and the derive
feature using the -F
flag.
❯ cargo add clap -F env -F derive
Updating crates.io index
Adding clap v4.3.19 to dependencies.
Features:
+ color
+ derive
+ env
+ error-context
+ help
+ std
+ suggestions
+ usage
- cargo
- debug
- deprecated
- string
- unicode
- unstable-doc
- unstable-styles
- unstable-v5
- wrap_help
Updating crates.io index
Our Cargo.toml
will have clap as a dependency with these features enabled.
[package]
name = "garden"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.3.19", features = ["env", "derive"] }
garden
is going to have multiple subcommands. The first of which is garden write
, which will be responsible for allowing the user to create new markdown files in a pre-determined directory. Our help text will look like this.
❯ cargo run -- --help
Compiling garden v0.1.0 (/rust-adventure/digital-garden)
Finished dev [unoptimized + debuginfo] target(s) in 2.83s
Running `target/debug/garden --help`
A CLI for the growing and curation of a digital garden
Visit https://www.rustadventure.dev for more!
Usage: garden [OPTIONS] <COMMAND>
Commands:
write write something in your garden
help Print this message or the help of the given subcommand(s)
Options:
-p, --garden-path <GARDEN_PATH>
[env: GARDEN_PATH=]
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
The code that implements that is here. Our Args
struct gets the Parser
derive macro, as well as the clap version
attribute which powers the --version
flag.
Our garden_path
is a global flag that can apply to any of our subcommands. It also uses clap(env)
to enable setting the path via an environment variable. The environment variables name is inferred and would be GARDEN_PATH
in this case.
use clap::{Parser, Subcommand};
use std::path::PathBuf;
/// A CLI for the growing and curation of a digital garden
///
/// Visit https://www.rustadventure.dev for more!
#[derive(Parser, Debug)]
#[clap(version)]
struct Args {
#[clap(short = 'p', long, env)]
garden_path: Option<PathBuf>,
#[command(subcommand)]
cmd: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// write something in your garden
///
/// This command will open your $EDITOR, wait for you
/// to write something, and then save the file to your
/// garden
Write {
/// Optionally set a title for what you are going to write about
#[clap(short, long)]
title: Option<String>,
},
}
fn main() {
let args = Args::parse();
dbg!(args);
}
The we use the command
helper to define our subcommands via an enum.
We need to use the Subcommand
derive macro to be able to use our enum like this.
The Commands
enum then is a set of variants that define what our subcommands will be, as long as the flags they take.
The Write
subcommand includes a --title
flag with short and long forms. The flag doesn’t have to be provided, so we make the type an Option<String>
and clap won’t fail if the title isn’t provided.
Our main
function only handles running the parse
function the Parser
derive macro created for us, and debugging that value out.
Running the CLI
If we only run cargo run
now, we’ll see the help text because the root without a subcommand doesn’t do anything.
We can use the write
subcommand to see what values Args
holds.
❯ cargo run -- write
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/garden write`
[src/main.rs:32] args = Args {
garden_path: None,
cmd: Write {
title: None,
},
}
We also get help text on our subcommands automatically. Both the short form
❯ cargo run -- write -h
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/garden write -h`
write something in your garden
Usage: garden write [OPTIONS]
Options:
-t, --title <TITLE> Optionally set a title for what you are going to write about
-h, --help Print help (see more with '--help')
and the long form
❯ cargo run -- write --help
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/garden write --help`
write something in your garden
This command will open your $EDITOR, wait for you to write something, and then save the file to your garden
Usage: garden write [OPTIONS]
Options:
-t, --title <TITLE>
Optionally set a title for what you are going to write about
-h, --help
Print help (see a summary with '-h')
All together, our CLI specification allows us to set a garden path via a flag or an environment variable, and use a subcommand. That subcommand then also gets to have its own flags, like --title
.
All of this information gets dropped into our Args
struct when we parse, making it easy to process later.
❯ cargo run -- -p some-garden write -t "My New Post"
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/garden -p some-garden write -t 'My New Post'`
[src/main.rs:32] args = Args {
garden_path: Some(
"some-garden",
),
cmd: Write {
title: Some(
"My New Post",
),
},
}