unstable-doc only.Expand description
§Example: pacman-like CLI (Builder API)
use clap::{Arg, ArgAction, Command};
fn cli() -> Command {
Command::new("pacman")
.about("package manager utility")
.version("5.2.1")
.subcommand_required(true)
.arg_required_else_help(true)
// Query subcommand
//
// Only a few of its arguments are implemented below.
.subcommand(
Command::new("query")
.short_flag('Q')
.long_flag("query")
.about("Query the package database.")
.arg(
Arg::new("search")
.short('s')
.long("search")
.help("search locally installed packages for matching strings")
.conflicts_with("info")
.action(ArgAction::Set)
.num_args(1..),
)
.arg(
Arg::new("info")
.long("info")
.short('i')
.conflicts_with("search")
.help("view package information")
.action(ArgAction::Set)
.num_args(1..),
),
)
// Sync subcommand
//
// Only a few of its arguments are implemented below.
.subcommand(
Command::new("sync")
.short_flag('S')
.long_flag("sync")
.about("Synchronize packages.")
.arg(
Arg::new("search")
.short('s')
.long("search")
.conflicts_with("info")
.action(ArgAction::Set)
.num_args(1..)
.help("search remote repositories for matching strings"),
)
.arg(
Arg::new("info")
.long("info")
.conflicts_with("search")
.short('i')
.action(ArgAction::SetTrue)
.help("view package information"),
)
.arg(
Arg::new("package")
.help("packages")
.required_unless_present("search")
.action(ArgAction::Set)
.num_args(1..),
),
)
}
fn main() {
let matches = cli().get_matches();
match matches.subcommand() {
Some(("sync", sync_matches)) => {
if sync_matches.contains_id("search") {
let packages: Vec<_> = sync_matches
.get_many::<String>("search")
.expect("contains_id")
.map(|s| s.as_str())
.collect();
let values = packages.join(", ");
println!("Searching for {values}...");
return;
}
let packages: Vec<_> = sync_matches
.get_many::<String>("package")
.expect("is present")
.map(|s| s.as_str())
.collect();
let values = packages.join(", ");
if sync_matches.get_flag("info") {
println!("Retrieving info for {values}...");
} else {
println!("Installing {values}...");
}
}
Some(("query", query_matches)) => {
if let Some(packages) = query_matches.get_many::<String>("info") {
let comma_sep = packages.map(|s| s.as_str()).collect::<Vec<_>>().join(", ");
println!("Retrieving info for {comma_sep}...");
} else if let Some(queries) = query_matches.get_many::<String>("search") {
let comma_sep = queries.map(|s| s.as_str()).collect::<Vec<_>>().join(", ");
println!("Searching Locally for {comma_sep}...");
} else {
println!("Displaying all locally installed packages...");
}
}
_ => unreachable!(), // If all subcommands are defined above, anything else is unreachable
}
}
#[cfg(all(test, feature = "help", feature = "usage"))]
mod tests {
use super::*;
#[test]
fn help_output_is_stable() {
snapbox::assert_data_eq!(
render_reference(cli()),
snapbox::file!["snapshots/pacman.txt"].raw()
);
}
fn render_reference(mut command: Command) -> String {
command = command.term_width(0);
command.build();
let mut buffer = String::new();
write_help(&mut buffer, &mut command, String::new());
buffer
}
fn write_help(buffer: &mut String, cmd: &mut Command, mut path: String) {
let header = if path.is_empty() { "#" } else { "##" };
path.push(' ');
path.push_str(cmd.get_name());
buffer.push_str(header);
buffer.push_str(&path);
buffer.push_str("\n\n");
buffer.push_str(&cmd.render_long_help().to_string());
let has_generated_help = !cmd.is_disable_help_subcommand_set();
for subcommand in cmd.get_subcommands_mut() {
// Generated `help` nodes proxy other commands.
if has_generated_help && subcommand.get_name() == "help" {
continue;
}
buffer.push('\n');
write_help(buffer, subcommand, path.clone());
}
}
}pacman defines subcommands via flags.
Here, -S is a short flag subcommand:
$ pacman -S package
Installing package...
Here --sync is a long flag subcommand:
$ pacman --sync package
Installing package...
Now the short flag subcommand (-S) with a long flag:
$ pacman -S --search name
Searching for name...
And the various forms of short flags that work:
$ pacman -S -s name
Searching for name...
$ pacman -Ss name
Searching for name...
(users can “stack” short subcommands with short flags or with other short flag subcommands)
In the help, this looks like:
$ pacman -h
package manager utility
Usage: pacman[EXE] <COMMAND>
Commands:
query, -Q, --query Query the package database.
sync, -S, --sync Synchronize packages.
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
$ pacman -S -h
Synchronize packages.
Usage: pacman[EXE] {sync|--sync|-S} [OPTIONS] [package]...
Arguments:
[package]... packages
Options:
-s, --search <search>... search remote repositories for matching strings
-i, --info view package information
-h, --help Print help
And errors:
$ pacman -S -s foo -i bar
? failed
error: the argument '--search <search>...' cannot be used with '--info'
Usage: pacman[EXE] {sync|--sync|-S} --search <search>... <package>...
For more information, try '--help'.
NOTE: Keep in mind that subcommands, flags, and long flags are case sensitive: -Q and -q are different flags/subcommands. For example, you can have both -Q subcommand and -q flag, and they will be properly disambiguated.
Let’s make a quick program to illustrate.
§Reviewing help output
The test renders every command’s long help.
Each Markdown heading contains the complete command path.
Generated help subcommands are skipped.
term_width(0) prevents terminal-dependent wrapping.
snapbox compares this output with a snapshot.
The resulting diff makes CLI changes easy to review.
Set SNAPSHOTS=overwrite to accept intentional changes.