Skip to main content

## Module pacman

Source
Available on crate feature `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.