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.
- While it is possible to make
- 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 anOutputOpts
struct, defined in a separateoutput.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.
- This means that global options like
- 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);
}
}