Log in to access Rust Adventure videos!

Lesson Details

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",
        ),
    },
}