Handling arguments and subcommands

For a program that has subcommands, the following code structure is recommended.

// Run this binary with:
//    cd code
//    cargo run --bin grep-app -- <arguments>

use camino::Utf8PathBuf;
use clap::{ArgEnum, Args, Parser, Subcommand};

/// Here's my app!
#[derive(Debug, Parser)]
#[clap(name = "my-app", version)]
pub struct App {
    #[clap(flatten)]
    global_opts: GlobalOpts,

    #[clap(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Help message for read.
    Read {
        /// An example option
        #[clap(long, short = 'o')]
        example_opt: bool,

        /// The path to read from
        path: Utf8PathBuf,
        // (can #[clap(flatten)] other argument structs here)
    },
    /// Help message for write.
    Write(WriteArgs),
    // ...other commands (can #[clap(flatten)] other enum variants here)
}

#[derive(Debug, Args)]
struct WriteArgs {
    /// The path to write to
    path: Utf8PathBuf,
    // a list of other write args
}

#[derive(Debug, Args)]
struct GlobalOpts {
    /// Color
    #[clap(long, arg_enum, global = true, default_value_t = Color::Auto)]
    color: Color,

    /// Verbosity level (can be specified multiple times)
    #[clap(long, short, global = true, parse(from_occurrences))]
    verbose: usize,
    //... other global options
}

#[derive(Clone, Debug, ArgEnum)]
enum Color {
    Always,
    Auto,
    Never,
}

fn main() {
    let app = App::parse();
    println!(
        "Verbosity level specified {} times",
        app.global_opts.verbose
    );
}

#[allow(dead_code)]
const EXPECTED_HELP: &str = r#"my-app 0.1.0
Here's my app!

USAGE:
    my-app [OPTIONS] <SUBCOMMAND>

OPTIONS:
        --color <COLOR>    Color [default: auto] [possible values: always, auto, never]
    -h, --help             Print help information
    -v, --verbose          Verbosity level (can be specified multiple times)
    -V, --version          Print version information

SUBCOMMANDS:
    help     Print this message or the help of the given subcommand(s)
    read     Help message for read
    write    Help message for write
"#;

#[allow(dead_code)]
const EXPECTED_READ_HELP: &str = r#"read 
Help message for read

USAGE:
    read [OPTIONS] <PATH>

ARGS:
    <PATH>    The path to read from

OPTIONS:
    -h, --help           Print help information
    -o, --example-opt    An example option
"#;

#[cfg(test)]
mod tests {
    use super::*;
    use clap::IntoApp;
    use std::io::Cursor;

    #[test]
    fn test_help() {
        let mut app = App::into_app();
        let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
        app.write_help(&mut cursor).unwrap();
        let help = String::from_utf8(cursor.into_inner()).unwrap();
        println!("{}", help);
        assert_eq!(help, EXPECTED_HELP);
    }

    #[test]
    fn test_read_help() {
        let mut app = App::into_app();
        let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
        let read_cmd = app.find_subcommand_mut("read").unwrap();
        read_cmd.write_help(&mut cursor).unwrap();
        let help = String::from_utf8(cursor.into_inner()).unwrap();
        println!("{}", help);
        assert_eq!(help, EXPECTED_READ_HELP);
    }
}

Notes:

  • Only the top-level App is public.
  • App is a struct, one level above the command enum.
    • While it is possible to make App an enum with all the subcommands, in my experience this design has always come back to bite me. This has always been because I've wanted to introduce global options later.
  • Liberal use of #[clap(flatten)].
    • This option flattens inline options from a struct into the parent struct or enum variant, or from an enum into a parent enum.
    • This helps break up long series of options into smaller, reusable components that can be more easily processed in different sections of the project's code. For example, Color can be further nested into an OutputOpts struct, defined in a separate output.rs file.
    • It also helps code pass a complex set of arguments around as a single parameter, rather than having to add a parameter everywhere.
  • Global options are marked with #[clap(global = true)].
    • This means that global options like --color can be used anywhere in the command line.
  • Use of ArgEnum.
    • ArgEnum simplifies the definition of arguments that take one of a limited number of values.

The top-level help message is:

// Run this binary with:
//    cd code
//    cargo run --bin grep-app -- <arguments>

use camino::Utf8PathBuf;
use clap::{ArgEnum, Args, Parser, Subcommand};

/// Here's my app!
#[derive(Debug, Parser)]
#[clap(name = "my-app", version)]
pub struct App {
    #[clap(flatten)]
    global_opts: GlobalOpts,

    #[clap(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Help message for read.
    Read {
        /// An example option
        #[clap(long, short = 'o')]
        example_opt: bool,

        /// The path to read from
        path: Utf8PathBuf,
        // (can #[clap(flatten)] other argument structs here)
    },
    /// Help message for write.
    Write(WriteArgs),
    // ...other commands (can #[clap(flatten)] other enum variants here)
}

#[derive(Debug, Args)]
struct WriteArgs {
    /// The path to write to
    path: Utf8PathBuf,
    // a list of other write args
}

#[derive(Debug, Args)]
struct GlobalOpts {
    /// Color
    #[clap(long, arg_enum, global = true, default_value_t = Color::Auto)]
    color: Color,

    /// Verbosity level (can be specified multiple times)
    #[clap(long, short, global = true, parse(from_occurrences))]
    verbose: usize,
    //... other global options
}

#[derive(Clone, Debug, ArgEnum)]
enum Color {
    Always,
    Auto,
    Never,
}

fn main() {
    let app = App::parse();
    println!(
        "Verbosity level specified {} times",
        app.global_opts.verbose
    );
}

#[allow(dead_code)]
const EXPECTED_HELP: &str = r#"my-app 0.1.0
Here's my app!

USAGE:
    my-app [OPTIONS] <SUBCOMMAND>

OPTIONS:
        --color <COLOR>    Color [default: auto] [possible values: always, auto, never]
    -h, --help             Print help information
    -v, --verbose          Verbosity level (can be specified multiple times)
    -V, --version          Print version information

SUBCOMMANDS:
    help     Print this message or the help of the given subcommand(s)
    read     Help message for read
    write    Help message for write
"#;

#[allow(dead_code)]
const EXPECTED_READ_HELP: &str = r#"read 
Help message for read

USAGE:
    read [OPTIONS] <PATH>

ARGS:
    <PATH>    The path to read from

OPTIONS:
    -h, --help           Print help information
    -o, --example-opt    An example option
"#;

#[cfg(test)]
mod tests {
    use super::*;
    use clap::IntoApp;
    use std::io::Cursor;

    #[test]
    fn test_help() {
        let mut app = App::into_app();
        let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
        app.write_help(&mut cursor).unwrap();
        let help = String::from_utf8(cursor.into_inner()).unwrap();
        println!("{}", help);
        assert_eq!(help, EXPECTED_HELP);
    }

    #[test]
    fn test_read_help() {
        let mut app = App::into_app();
        let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
        let read_cmd = app.find_subcommand_mut("read").unwrap();
        read_cmd.write_help(&mut cursor).unwrap();
        let help = String::from_utf8(cursor.into_inner()).unwrap();
        println!("{}", help);
        assert_eq!(help, EXPECTED_READ_HELP);
    }
}

The help for the read command is:

// Run this binary with:
//    cd code
//    cargo run --bin grep-app -- <arguments>

use camino::Utf8PathBuf;
use clap::{ArgEnum, Args, Parser, Subcommand};

/// Here's my app!
#[derive(Debug, Parser)]
#[clap(name = "my-app", version)]
pub struct App {
    #[clap(flatten)]
    global_opts: GlobalOpts,

    #[clap(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Help message for read.
    Read {
        /// An example option
        #[clap(long, short = 'o')]
        example_opt: bool,

        /// The path to read from
        path: Utf8PathBuf,
        // (can #[clap(flatten)] other argument structs here)
    },
    /// Help message for write.
    Write(WriteArgs),
    // ...other commands (can #[clap(flatten)] other enum variants here)
}

#[derive(Debug, Args)]
struct WriteArgs {
    /// The path to write to
    path: Utf8PathBuf,
    // a list of other write args
}

#[derive(Debug, Args)]
struct GlobalOpts {
    /// Color
    #[clap(long, arg_enum, global = true, default_value_t = Color::Auto)]
    color: Color,

    /// Verbosity level (can be specified multiple times)
    #[clap(long, short, global = true, parse(from_occurrences))]
    verbose: usize,
    //... other global options
}

#[derive(Clone, Debug, ArgEnum)]
enum Color {
    Always,
    Auto,
    Never,
}

fn main() {
    let app = App::parse();
    println!(
        "Verbosity level specified {} times",
        app.global_opts.verbose
    );
}

#[allow(dead_code)]
const EXPECTED_HELP: &str = r#"my-app 0.1.0
Here's my app!

USAGE:
    my-app [OPTIONS] <SUBCOMMAND>

OPTIONS:
        --color <COLOR>    Color [default: auto] [possible values: always, auto, never]
    -h, --help             Print help information
    -v, --verbose          Verbosity level (can be specified multiple times)
    -V, --version          Print version information

SUBCOMMANDS:
    help     Print this message or the help of the given subcommand(s)
    read     Help message for read
    write    Help message for write
"#;

#[allow(dead_code)]
const EXPECTED_READ_HELP: &str = r#"read 
Help message for read

USAGE:
    read [OPTIONS] <PATH>

ARGS:
    <PATH>    The path to read from

OPTIONS:
    -h, --help           Print help information
    -o, --example-opt    An example option
"#;

#[cfg(test)]
mod tests {
    use super::*;
    use clap::IntoApp;
    use std::io::Cursor;

    #[test]
    fn test_help() {
        let mut app = App::into_app();
        let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
        app.write_help(&mut cursor).unwrap();
        let help = String::from_utf8(cursor.into_inner()).unwrap();
        println!("{}", help);
        assert_eq!(help, EXPECTED_HELP);
    }

    #[test]
    fn test_read_help() {
        let mut app = App::into_app();
        let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
        let read_cmd = app.find_subcommand_mut("read").unwrap();
        read_cmd.write_help(&mut cursor).unwrap();
        let help = String::from_utf8(cursor.into_inner()).unwrap();
        println!("{}", help);
        assert_eq!(help, EXPECTED_READ_HELP);
    }
}