diff --git a/examples/23_flag_subcommands_pacman.rs b/examples/23_flag_subcommands_pacman.rs new file mode 100644 index 00000000000..cd186e9ad9b --- /dev/null +++ b/examples/23_flag_subcommands_pacman.rs @@ -0,0 +1,119 @@ +// This feature allows users of the app to pass subcommands in the fashion of short or long flags. +// You may be familiar with it if you ever used [`pacman`](https://wiki.archlinux.org/index.php/pacman). +// Some made up examples of what flag subcommands are: +// +// ```shell +// $ pacman -S +// ^--- short flag subcommand. +// $ pacman --sync +// ^--- long flag subcommand. +// $ pacman -Ss +// ^--- short flag subcommand followed by a short flag +// (users can "stack" short subcommands with short flags or with other short flag subcommands) +// $ pacman -S -s +// ^--- same as above +// $ pacman -S --sync +// ^--- short flag subcommand followed by a long flag +// ``` +// 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. + +use clap::{App, AppSettings, Arg}; + +fn main() { + let matches = App::new("pacman") + .about("package manager utility") + .version("5.2.1") + .setting(AppSettings::SubcommandRequiredElseHelp) + .author("Pacman Development Team") + // Query subcommand + // + // Only a few of its arguments are implemented below. + .subcommand( + App::new("query") + .short_flag('Q') + .long_flag("query") + .about("Query the package database.") + .arg( + Arg::new("search") + .short('s') + .long("search") + .about("search locally-installed packages for matching strings") + .conflicts_with("info") + .multiple_values(true), + ) + .arg( + Arg::new("info") + .long("info") + .short('i') + .conflicts_with("search") + .about("view package information") + .multiple_values(true), + ), + ) + // Sync subcommand + // + // Only a few of its arguments are implemented below. + .subcommand( + App::new("sync") + .short_flag('S') + .long_flag("sync") + .about("Synchronize packages.") + .arg( + Arg::new("search") + .short('s') + .long("search") + .conflicts_with("info") + .takes_value(true) + .multiple_values(true) + .about("search remote repositories for matching strings"), + ) + .arg( + Arg::new("info") + .long("info") + .conflicts_with("search") + .short('i') + .about("view package information"), + ) + .arg( + Arg::new("package") + .about("packages") + .multiple(true) + .required_unless_one(&["search"]) + .takes_value(true), + ), + ) + .get_matches(); + + match matches.subcommand() { + ("sync", Some(sync_matches)) => { + if sync_matches.is_present("search") { + let packages: Vec<_> = sync_matches.values_of("search").unwrap().collect(); + let values = packages.join(", "); + println!("Searching for {}...", values); + return; + } + + let packages: Vec<_> = sync_matches.values_of("package").unwrap().collect(); + let values = packages.join(", "); + + if sync_matches.is_present("info") { + println!("Retrieving info for {}...", values); + } else { + println!("Installing {}...", values); + } + } + ("query", Some(query_matches)) => { + if let Some(packages) = query_matches.values_of("info") { + let comma_sep = packages.collect::>().join(", "); + println!("Retrieving info for {}...", comma_sep); + } else if let Some(queries) = query_matches.values_of("search") { + let comma_sep = queries.collect::>().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 + } +} diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index 6cf4c998de6..ed0ebd8a73c 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -25,7 +25,7 @@ use crate::{ mkeymap::MKeyMap, output::{fmt::Colorizer, Help, HelpWriter, Usage}, parse::{ArgMatcher, ArgMatches, Input, Parser}, - util::{safe_exit, termcolor::ColorChoice, Id, Key}, + util::{safe_exit, termcolor::ColorChoice, ArgStr, Id, Key}, Result as ClapResult, INTERNAL_ERROR_MSG, }; @@ -72,6 +72,8 @@ pub(crate) enum Propagation { pub struct App<'b> { pub(crate) id: Id, pub(crate) name: String, + pub(crate) long_flag: Option<&'b str>, + pub(crate) short_flag: Option, pub(crate) bin_name: Option, pub(crate) author: Option<&'b str>, pub(crate) version: Option<&'b str>, @@ -81,6 +83,8 @@ pub struct App<'b> { pub(crate) before_help: Option<&'b str>, pub(crate) after_help: Option<&'b str>, pub(crate) aliases: Vec<(&'b str, bool)>, // (name, visible) + pub(crate) short_flag_aliases: Vec<(char, bool)>, // (name, visible) + pub(crate) long_flag_aliases: Vec<(&'b str, bool)>, // (name, visible) pub(crate) usage_str: Option<&'b str>, pub(crate) usage: Option, pub(crate) help_str: Option<&'b str>, @@ -104,6 +108,18 @@ impl<'b> App<'b> { &self.name } + /// Get the short flag of the subcommand + #[inline] + pub fn get_short_flag(&self) -> Option { + self.short_flag + } + + /// Get the long flag of the subcommand + #[inline] + pub fn get_long_flag(&self) -> Option<&str> { + self.long_flag + } + /// Get the name of the binary #[inline] pub fn get_bin_name(&self) -> Option<&str> { @@ -127,13 +143,43 @@ impl<'b> App<'b> { self.aliases.iter().filter(|(_, vis)| *vis).map(|a| a.0) } + /// Iterate through the *visible* short aliases for this subcommand. + #[inline] + pub fn get_visible_short_flag_aliases(&self) -> impl Iterator + '_ { + self.short_flag_aliases + .iter() + .filter(|(_, vis)| *vis) + .map(|a| a.0) + } + + /// Iterate through the *visible* short aliases for this subcommand. + #[inline] + pub fn get_visible_long_flag_aliases(&self) -> impl Iterator + '_ { + self.long_flag_aliases + .iter() + .filter(|(_, vis)| *vis) + .map(|a| a.0) + } + /// Iterate through the set of *all* the aliases for this subcommand, both visible and hidden. #[inline] pub fn get_all_aliases(&self) -> impl Iterator { self.aliases.iter().map(|a| a.0) } - /// Iterate through the set of subcommands. + /// Iterate through the set of *all* the short aliases for this subcommand, both visible and hidden. + #[inline] + pub fn get_all_short_flag_aliases(&self) -> impl Iterator + '_ { + self.short_flag_aliases.iter().map(|a| a.0) + } + + /// Iterate through the set of *all* the long aliases for this subcommand, both visible and hidden. + #[inline] + pub fn get_all_long_flag_aliases(&self) -> impl Iterator + '_ { + self.long_flag_aliases.iter().map(|a| a.0) + } + + /// Get the list of subcommands #[inline] pub fn get_subcommands(&self) -> impl Iterator> { self.subcommands.iter() @@ -386,6 +432,68 @@ impl<'b> App<'b> { self } + /// Allows the subcommand to be used as if it were an [`Arg::short`] + /// + /// Sets the short version of the subcommand flag without the preceeding `-`. + /// + /// # Examples + /// + /// ``` + /// # use clap::{App, Arg}; + /// let matches = App::new("pacman") + /// .subcommand( + /// App::new("sync").short_flag('S').arg( + /// Arg::new("search") + /// .short('s') + /// .long("search") + /// .about("search remote repositories for matching strings"), + /// ), + /// ) + /// .get_matches_from(vec!["pacman", "-Ss"]); + /// + /// assert_eq!(matches.subcommand_name().unwrap(), "sync"); + /// let sync_matches = matches.subcommand_matches("sync").unwrap(); + /// assert!(sync_matches.is_present("search")); + /// ``` + pub fn short_flag(mut self, short: char) -> Self { + self.short_flag = Some(short); + self + } + + /// Allows the subcommand to be used as if it were an [`Arg::long`] + /// + /// Sets the long version of the subcommand flag without the preceeding `--`. + /// + /// **NOTE:** Any leading `-` characters will be stripped + /// + /// # Examples + /// + /// To set `long_flag` use a word containing valid UTF-8 codepoints. If you supply a double leading + /// `--` such as `--sync` they will be stripped. Hyphens in the middle of the word; however, + /// will *not* be stripped (i.e. `sync-file` is allowed) + /// + /// ``` + /// # use clap::{App, Arg}; + /// let matches = App::new("pacman") + /// .subcommand( + /// App::new("sync").long_flag("sync").arg( + /// Arg::new("search") + /// .short('s') + /// .long("search") + /// .about("search remote repositories for matching strings"), + /// ), + /// ) + /// .get_matches_from(vec!["pacman", "--sync", "--search"]); + /// + /// assert_eq!(matches.subcommand_name().unwrap(), "sync"); + /// let sync_matches = matches.subcommand_matches("sync").unwrap(); + /// assert!(sync_matches.is_present("search")); + /// ``` + pub fn long_flag(mut self, long: &'b str) -> Self { + self.long_flag = Some(long.trim_start_matches(|c| c == '-')); + self + } + /// Sets a string of the version number to be displayed when displaying version or help /// information with `-V`. /// @@ -791,6 +899,49 @@ impl<'b> App<'b> { self } + /// Allows adding an alias, which function as "hidden" short flag subcommands that + /// automatically dispatch as if this subcommand was used. This is more efficient, and easier + /// than creating multiple hidden subcommands as one only needs to check for the existence of + /// this command, and not all variants. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").short_flag('t') + /// .short_flag_alias('d')) + /// .get_matches_from(vec!["myprog", "-d"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + pub fn short_flag_alias(mut self, name: char) -> Self { + if name == '-' { + panic!("short alias name cannot be `-`"); + } + self.short_flag_aliases.push((name, false)); + self + } + + /// Allows adding an alias, which function as "hidden" long flag subcommands that + /// automatically dispatch as if this subcommand was used. This is more efficient, and easier + /// than creating multiple hidden subcommands as one only needs to check for the existence of + /// this command, and not all variants. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").long_flag("test") + /// .long_flag_alias("testing")) + /// .get_matches_from(vec!["myprog", "--testing"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + pub fn long_flag_alias(mut self, name: &'b str) -> Self { + self.long_flag_aliases.push((name, false)); + self + } + /// Allows adding [``] aliases, which function as "hidden" subcommands that /// automatically dispatch as if this subcommand was used. This is more efficient, and easier /// than creating multiple hidden subcommands as one only needs to check for the existence of @@ -816,6 +967,61 @@ impl<'b> App<'b> { self } + /// Allows adding aliases, which function as "hidden" short flag subcommands that + /// automatically dispatch as if this subcommand was used. This is more efficient, and easier + /// than creating multiple hidden subcommands as one only needs to check for the existence of + /// this command, and not all variants. + /// + /// # Examples + /// + /// ```rust + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").short_flag('t') + /// .short_flag_aliases(&['a', 'b', 'c'])) + /// .arg(Arg::new("input") + /// .about("the file to add") + /// .index(1) + /// .required(false)) + /// .get_matches_from(vec!["myprog", "-a"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + pub fn short_flag_aliases(mut self, names: &[char]) -> Self { + for s in names { + if s == &'-' { + panic!("short alias name cannot be `-`"); + } + self.short_flag_aliases.push((*s, false)); + } + self + } + + /// Allows adding aliases, which function as "hidden" long flag subcommands that + /// automatically dispatch as if this subcommand was used. This is more efficient, and easier + /// than creating multiple hidden subcommands as one only needs to check for the existence of + /// this command, and not all variants. + /// + /// # Examples + /// + /// ```rust + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").long_flag("test") + /// .long_flag_aliases(&["testing", "testall", "test_all"])) + /// .arg(Arg::new("input") + /// .about("the file to add") + /// .index(1) + /// .required(false)) + /// .get_matches_from(vec!["myprog", "--testing"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + pub fn long_flag_aliases(mut self, names: &[&'b str]) -> Self { + for s in names { + self.long_flag_aliases.push((s, false)); + } + self + } + /// Allows adding a [``] alias that functions exactly like those defined with /// [`App::alias`], except that they are visible inside the help message. /// @@ -836,6 +1042,47 @@ impl<'b> App<'b> { self } + /// Allows adding an alias that functions exactly like those defined with + /// [`App::short_flag_alias`], except that they are visible inside the help message. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").short_flag('t') + /// .visible_short_flag_alias('d')) + /// .get_matches_from(vec!["myprog", "-d"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + /// [`App::short_flag_alias`]: ./struct.App.html#method.short_flag_alias + pub fn visible_short_flag_alias(mut self, name: char) -> Self { + if name == '-' { + panic!("short alias name cannot be `-`"); + } + self.short_flag_aliases.push((name, true)); + self + } + + /// Allows adding an alias that functions exactly like those defined with + /// [`App::long_flag_alias`], except that they are visible inside the help message. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").long_flag("test") + /// .visible_long_flag_alias("testing")) + /// .get_matches_from(vec!["myprog", "--testing"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + /// [`App::long_flag_alias`]: ./struct.App.html#method.long_flag_alias + pub fn visible_long_flag_alias(mut self, name: &'b str) -> Self { + self.long_flag_aliases.push((name, true)); + self + } + /// Allows adding multiple [``] aliases that functions exactly like those defined /// with [`App::aliases`], except that they are visible inside the help message. /// @@ -856,6 +1103,51 @@ impl<'b> App<'b> { self } + /// Allows adding multiple short flag aliases that functions exactly like those defined + /// with [`App::short_flag_aliases`], except that they are visible inside the help message. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").short_flag('b') + /// .visible_short_flag_aliases(&['t'])) + /// .get_matches_from(vec!["myprog", "-t"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + /// [`App::short_flag_aliases`]: ./struct.App.html#method.short_flag_aliases + pub fn visible_short_flag_aliases(mut self, names: &[char]) -> Self { + for s in names { + if s == &'-' { + panic!("short alias name cannot be `-`"); + } + self.short_flag_aliases.push((*s, true)); + } + self + } + + /// Allows adding multiple long flag aliases that functions exactly like those defined + /// with [`App::long_flag_aliases`], except that they are visible inside the help message. + /// + /// # Examples + /// + /// ```no_run + /// # use clap::{App, Arg, }; + /// let m = App::new("myprog") + /// .subcommand(App::new("test").long_flag("test") + /// .visible_long_flag_aliases(&["testing", "testall", "test_all"])) + /// .get_matches_from(vec!["myprog", "--testing"]); + /// assert_eq!(m.subcommand_name(), Some("test")); + /// ``` + /// [`App::short_flag_aliases`]: ./struct.App.html#method.short_flag_aliases + pub fn visible_long_flag_aliases(mut self, names: &[&'b str]) -> Self { + for s in names { + self.long_flag_aliases.push((s, true)); + } + self + } + /// Replaces an argument to this application with other arguments. /// /// Below, when the given args are `app install`, they will be changed to `app module install`. @@ -1477,6 +1769,41 @@ impl<'b> App<'b> { } } +// Allows checking for conflicts between `Args` and `Apps` with subcommand flags +#[cfg(debug_assertions)] +#[derive(Debug)] +enum Flag<'a> { + App(&'a App<'a>, String), + Arg(&'a Arg<'a>, String), +} + +#[cfg(debug_assertions)] +impl<'a> Flag<'_> { + pub fn value(&self) -> &str { + match self { + Self::App(_, value) => value, + Self::Arg(_, value) => value, + } + } + + pub fn id(&self) -> &Id { + match self { + Self::App(app, _) => &app.id, + Self::Arg(arg, _) => &arg.id, + } + } +} + +#[cfg(debug_assertions)] +impl<'a> fmt::Display for Flag<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::App(app, _) => write!(f, "App named '{}'", app.name), + Self::Arg(arg, _) => write!(f, "Arg named '{}'", arg.name), + } + } +} + // Internally used only impl<'b> App<'b> { fn _do_parse(&mut self, it: &mut Input) -> ClapResult { @@ -1578,6 +1905,68 @@ impl<'b> App<'b> { } } + #[cfg(debug_assertions)] + fn two_long_flags_of(&self, condition: F) -> Option<(Flag, Flag)> + where + F: Fn(&Flag<'_>) -> bool, + { + let mut flags: Vec = Vec::new(); + for sc in &self.subcommands { + if let Some(long) = sc.long_flag { + flags.push(Flag::App(&sc, long.to_string())); + } + flags.extend( + sc.get_all_long_flag_aliases() + .map(|alias| Flag::App(&sc, alias.to_string())), + ); + self.args.args.iter().for_each(|arg| { + flags.extend( + arg.aliases + .iter() + .map(|(alias, _)| Flag::Arg(arg, alias.to_string())), + ) + }); + flags.extend( + self.args + .args + .iter() + .filter_map(|arg| arg.long.map(|l| Flag::Arg(arg, l.to_string()))), + ); + } + two_elements_of(flags.into_iter().filter(|f| condition(f))) + } + + #[cfg(debug_assertions)] + fn two_short_flags_of(&self, condition: F) -> Option<(Flag, Flag)> + where + F: Fn(&Flag<'_>) -> bool, + { + let mut flags: Vec = Vec::new(); + for sc in &self.subcommands { + if let Some(short) = sc.short_flag { + flags.push(Flag::App(&sc, short.to_string())); + } + flags.extend( + sc.get_all_short_flag_aliases() + .map(|alias| Flag::App(&sc, alias.to_string())), + ); + self.args.args.iter().for_each(|arg| { + flags.extend( + arg.short_aliases + .iter() + .map(|(alias, _)| Flag::Arg(arg, alias.to_string())), + ) + }); + flags.extend( + self.args + .args + .iter() + .filter_map(|arg| arg.short.map(|l| Flag::Arg(arg, l.to_string()))), + ); + } + two_elements_of(flags.into_iter().filter(|f| condition(f))) + } + #[cfg(debug_assertions)] fn two_args_of(&self, condition: F) -> Option<(&Arg, &Arg)> where @@ -1601,6 +1990,38 @@ impl<'b> App<'b> { fn _debug_asserts(&self) { debug!("App::_debug_asserts"); + for sc in &self.subcommands { + // Conflicts between flag subcommands and long args + if let Some(l) = sc.long_flag { + if let Some((first, second)) = self.two_long_flags_of(|f| f.value() == l) { + // Prevent conflicts with itself + if first.id() != second.id() { + panic!( + "Long option names must be unique for each argument, \ + but '--{}' is used by both an {} and an {}", + l, first, second + ); + } + } + } + + // Conflicts between flag subcommands and long args + if let Some(s) = sc.short_flag { + if let Some((first, second)) = + self.two_short_flags_of(|f| f.value() == s.to_string()) + { + // Prevent conflicts with itself + if first.id() != second.id() { + panic!( + "Short option names must be unique for each argument, \ + but '-{}' is used by both an {} and an {}", + s, first, second + ); + } + } + } + } + for arg in &self.args.args { arg._debug_asserts(); @@ -1800,7 +2221,11 @@ impl<'b> App<'b> { .args .iter() .any(|x| x.long == Some("help") || x.id == Id::help_hash()) - || self.is_set(AppSettings::DisableHelpFlags)) + || self.is_set(AppSettings::DisableHelpFlags) + || self + .subcommands + .iter() + .any(|sc| sc.short_flag == Some('h') || sc.long_flag == Some("help"))) { debug!("App::_create_help_and_version: Building --help"); let mut help = Arg::new("help") @@ -1817,7 +2242,11 @@ impl<'b> App<'b> { .args .iter() .any(|x| x.long == Some("version") || x.id == Id::version_hash()) - || self.is_set(AppSettings::DisableVersion)) + || self.is_set(AppSettings::DisableVersion) + || self + .subcommands + .iter() + .any(|sc| sc.short_flag == Some('V') || sc.long_flag == Some("version"))) { debug!("App::_create_help_and_version: Building --version"); let mut version = Arg::new("version") @@ -2015,6 +2444,29 @@ impl<'b> App<'b> { *name == *self.get_name() || self.get_all_aliases().any(|alias| *name == *alias) } + /// Check if this subcommand can be referred to as `name`. In other words, + /// check if `name` is the name of this short flag subcommand or is one of its short flag aliases. + #[inline] + pub(crate) fn short_flag_aliases_to(&self, flag: char) -> bool { + Some(flag) == self.short_flag + || self.get_all_short_flag_aliases().any(|alias| flag == alias) + } + + /// Check if this subcommand can be referred to as `name`. In other words, + /// check if `name` is the name of this long flag subcommand or is one of its long flag aliases. + #[inline] + pub(crate) fn long_flag_aliases_to(&self, flag: &T) -> bool + where + T: PartialEq + ?Sized, + { + match self.long_flag { + Some(long_flag) => { + flag == long_flag || self.get_all_long_flag_aliases().any(|alias| flag == alias) + } + None => self.get_all_long_flag_aliases().any(|alias| flag == alias), + } + } + #[cfg(debug_assertions)] pub(crate) fn id_exists(&self, id: &Id) -> bool { self.args.args.iter().any(|x| x.id == *id) || self.groups.iter().any(|x| x.id == *id) @@ -2111,6 +2563,20 @@ impl<'b> App<'b> { args } + + /// Find a flag subcommand name by short flag or an alias + pub(crate) fn find_short_subcmd(&self, c: char) -> Option<&str> { + self.get_subcommands() + .find(|sc| sc.short_flag_aliases_to(c)) + .map(|sc| sc.get_name()) + } + + /// Find a flag subcommand name by long flag or an alias + pub(crate) fn find_long_subcmd(&self, long: &ArgStr<'_>) -> Option<&str> { + self.get_subcommands() + .find(|sc| sc.long_flag_aliases_to(long)) + .map(|sc| sc.get_name()) + } } impl<'b> Index<&'_ Id> for App<'b> { diff --git a/src/output/help.rs b/src/output/help.rs index 6e1101968d8..5108444ad13 100644 --- a/src/output/help.rs +++ b/src/output/help.rs @@ -547,16 +547,16 @@ impl<'b, 'c, 'd, 'w> Help<'b, 'c, 'd, 'w> { /// Methods to write a single subcommand impl<'b, 'c, 'd, 'w> Help<'b, 'c, 'd, 'w> { - fn write_subcommand(&mut self, app: &App<'b>) -> io::Result<()> { + fn write_subcommand(&mut self, sc_str: &str, app: &App<'b>) -> io::Result<()> { debug!("Help::write_subcommand"); self.none(TAB)?; - self.good(&app.name)?; - let spec_vals = self.sc_val(app)?; + self.good(sc_str)?; + let spec_vals = self.sc_val(sc_str, app)?; self.sc_help(app, &*spec_vals)?; Ok(()) } - fn sc_val(&mut self, app: &App<'b>) -> Result { + fn sc_val(&mut self, sc_str: &str, app: &App<'b>) -> Result { debug!("Help::sc_val: app={}", app.name); let spec_vals = self.sc_spec_vals(app); let h = app.about.unwrap_or(""); @@ -569,7 +569,7 @@ impl<'b, 'c, 'd, 'w> Help<'b, 'c, 'd, 'w> { && h_w > (self.term_w - taken); if !(nlh || self.force_next_line) { - write_nspaces!(self, self.longest + 4 - (str_width(&app.name))); + write_nspaces!(self, self.longest + 4 - (str_width(sc_str))); } Ok(spec_vals) } @@ -577,19 +577,26 @@ impl<'b, 'c, 'd, 'w> Help<'b, 'c, 'd, 'w> { fn sc_spec_vals(&self, a: &App) -> String { debug!("Help::sc_spec_vals: a={}", a.name); let mut spec_vals = vec![]; - if !a.aliases.is_empty() { + if !a.aliases.is_empty() || !a.short_flag_aliases.is_empty() { debug!("Help::spec_vals: Found aliases...{:?}", a.aliases); + debug!( + "Help::spec_vals: Found short flag aliases...{:?}", + a.short_flag_aliases + ); - let als = a - .aliases - .iter() - .filter(|&als| als.1) // visible - .map(|&als| als.0) // name - .collect::>() - .join(", "); + let mut short_als = a + .get_visible_short_flag_aliases() + .map(|a| format!("-{}", a)) + .collect::>(); - if !als.is_empty() { - spec_vals.push(format!(" [aliases: {}]", als)); + let als = a.get_visible_aliases().map(|s| s.to_string()); + + short_als.extend(als); + + let all_als = short_als.join(", "); + + if !all_als.is_empty() { + spec_vals.push(format!(" [aliases: {}]", all_als)); } } spec_vals.join(" ") @@ -773,19 +780,25 @@ impl<'b, 'c, 'd, 'w> Help<'b, 'c, 'd, 'w> { .filter(|s| !s.is_set(AppSettings::Hidden)) { let btm = ord_m.entry(sc.disp_ord).or_insert(BTreeMap::new()); - self.longest = cmp::max(self.longest, str_width(sc.name.as_str())); - btm.insert(sc.name.clone(), sc.clone()); + let mut sc_str = String::new(); + sc_str.push_str(&sc.short_flag.map_or(String::new(), |c| format!("-{}, ", c))); + sc_str.push_str(&sc.long_flag.map_or(String::new(), |c| format!("--{}, ", c))); + sc_str.push_str(&sc.name); + self.longest = cmp::max(self.longest, str_width(&sc_str)); + btm.insert(sc_str, sc.clone()); } + debug!("Help::write_subcommands longest = {}", self.longest); + let mut first = true; for btm in ord_m.values() { - for sc in btm.values() { + for (sc_str, sc) in btm { if first { first = false; } else { self.none("\n")?; } - self.write_subcommand(sc)?; + self.write_subcommand(sc_str, sc)?; } } Ok(()) diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 17b75a4ace1..a390e3f67ca 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -25,6 +25,9 @@ use crate::{ #[derive(Debug, PartialEq, Clone)] pub(crate) enum ParseResult { Flag(Id), + FlagSubCommand(String), + // subcommand name, whether there are more shorts args remaining + FlagSubCommandShort(String, bool), Opt(Id), Pos(Id), MaybeHyphenValue, @@ -92,6 +95,7 @@ where pub(crate) overriden: Vec, pub(crate) seen: Vec, pub(crate) cur_idx: Cell, + pub(crate) skip_idxs: usize, } // Initializing Methods @@ -116,6 +120,7 @@ where overriden: Vec::new(), seen: Vec::new(), cur_idx: Cell::new(0), + skip_idxs: 0, } } @@ -303,7 +308,7 @@ where true } - #[allow(clippy::block_in_if_condition_stmt)] + #[allow(clippy::blocks_in_if_conditions)] // Does all the initializing and prepares the parser pub(crate) fn _build(&mut self) { debug!("Parser::_build"); @@ -382,6 +387,7 @@ where let has_args = self.has_args(); let mut subcmd_name: Option = None; + let mut keep_state = false; let mut external_subcommand = false; let mut needs_val_of: ParseResult = ParseResult::NotFound; let mut pos_counter = 1; @@ -461,7 +467,12 @@ where self.maybe_inc_pos_counter(&mut pos_counter, id); continue; } - + ParseResult::FlagSubCommand(ref name) => { + debug!("Parser::get_matches_with: FlagSubCommand found in long arg {:?}", name); + subcmd_name = Some(name.to_owned()); + break; + } + ParseResult::FlagSubCommandShort(_, _) => unreachable!(), _ => (), } } else if arg_os.starts_with("-") && arg_os.len() != 1 { @@ -494,6 +505,18 @@ where self.maybe_inc_pos_counter(&mut pos_counter, id); continue; } + ParseResult::FlagSubCommandShort(ref name, done) => { + // There are more short args, revist the current short args skipping the subcommand + keep_state = !done; + if keep_state { + it.cursor -= 1; + self.skip_idxs = self.cur_idx.get(); + } + + subcmd_name = Some(name.to_owned()); + break; + } + ParseResult::FlagSubCommand(_) => unreachable!(), _ => (), } } @@ -739,7 +762,7 @@ where .expect(INTERNAL_ERROR_MSG) .name .clone(); - self.parse_subcommand(&sc_name, matcher, it)?; + self.parse_subcommand(&sc_name, matcher, it, keep_state)?; } else if self.is_set(AS::SubcommandRequired) { let bn = self.app.bin_name.as_ref().unwrap_or(&self.app.name); return Err(ClapError::missing_subcommand( @@ -857,6 +880,40 @@ where None } + // Checks if the arg matches a long flag subcommand name, or any of it's aliases (if defined) + fn possible_long_flag_subcommand(&self, arg_os: &ArgStr<'_>) -> Option<&str> { + debug!("Parser::possible_long_flag_subcommand: arg={:?}", arg_os); + if self.is_set(AS::InferSubcommands) { + let options = self + .app + .get_subcommands() + .fold(Vec::new(), |mut options, sc| { + if let Some(long) = sc.long_flag { + if arg_os.is_prefix_of(long) { + options.push(long); + } + options.extend( + sc.get_all_aliases() + .filter(|alias| arg_os.is_prefix_of(alias)), + ) + } + options + }); + if options.len() == 1 { + return Some(options[0]); + } + + for sc in &options { + if sc == arg_os { + return Some(sc); + } + } + } else if let Some(sc_name) = self.app.find_long_subcmd(arg_os) { + return Some(sc_name); + } + None + } + fn parse_help_subcommand(&self, cmds: &[OsString]) -> ClapResult { debug!("Parser::parse_help_subcommand"); @@ -987,6 +1044,7 @@ where sc_name: &str, matcher: &mut ArgMatcher, it: &mut Input, + keep_state: bool, ) -> ClapResult<()> { use std::fmt::Write; @@ -1011,8 +1069,22 @@ where if let Some(sc) = self.app.subcommands.iter_mut().find(|s| s.name == sc_name) { let mut sc_matcher = ArgMatcher::default(); - // bin_name should be parent's bin_name + [] + the sc's name separated by - // a space + // Display subcommand name, short and long in usage + let mut sc_names = sc.name.clone(); + let mut flag_subcmd = false; + if let Some(l) = sc.long_flag { + sc_names.push_str(&format!(", --{}", l)); + flag_subcmd = true; + } + if let Some(s) = sc.short_flag { + sc_names.push_str(&format!(", -{}", s)); + flag_subcmd = true; + } + + if flag_subcmd { + sc_names = format!("{{{}}}", sc_names); + } + sc.usage = Some(format!( "{}{}{}", self.app.bin_name.as_ref().unwrap_or(&String::new()), @@ -1021,8 +1093,11 @@ where } else { "" }, - &*sc.name + sc_names )); + + // bin_name should be parent's bin_name + [] + the sc's name separated by + // a space sc.bin_name = Some(format!( "{}{}{}", self.app.bin_name.as_ref().unwrap_or(&String::new()), @@ -1037,6 +1112,12 @@ where { let mut p = Parser::new(sc); + // HACK: maintain indexes between parsers + // FlagSubCommand short arg needs to revist the current short args, but skip the subcommand itself + if keep_state { + p.cur_idx.set(self.cur_idx.get()); + p.skip_idxs = self.skip_idxs; + } p.get_matches_with(&mut sc_matcher, it)?; } let name = &sc.name; @@ -1158,6 +1239,8 @@ where self.parse_flag(opt, matcher)?; return Ok(ParseResult::Flag(opt.id.clone())); + } else if let Some(sc_name) = self.possible_long_flag_subcommand(&arg) { + return Ok(ParseResult::FlagSubCommand(sc_name.to_string())); } else if self.is_set(AS::AllowLeadingHyphen) { return Ok(ParseResult::MaybeHyphenValue); } else if self.is_set(AS::ValidNegNumFound) { @@ -1196,7 +1279,9 @@ where } let mut ret = ParseResult::NotFound; - for c in arg.chars() { + let skip = self.skip_idxs; + self.skip_idxs = 0; + for c in arg.chars().skip(skip) { debug!("Parser::parse_short_arg:iter:{}", c); // update each index because `-abcd` is four indices to clap @@ -1241,6 +1326,11 @@ where // Default to "we're expecting a value later" return self.parse_opt(&val, opt, false, matcher); + } else if let Some(sc_name) = self.app.find_short_subcmd(c) { + debug!("Parser::parse_short_arg:iter:{}: subcommand={}", c, sc_name); + let name = sc_name.to_string(); + let done_short_args = self.cur_idx.get() == arg.len(); + return Ok(ParseResult::FlagSubCommandShort(name, done_short_args)); } else { let arg = format!("-{}", c); diff --git a/tests/flag_subcommands.rs b/tests/flag_subcommands.rs new file mode 100644 index 00000000000..d3d6be314a7 --- /dev/null +++ b/tests/flag_subcommands.rs @@ -0,0 +1,568 @@ +mod utils; + +use clap::{App, AppSettings, Arg, ErrorKind}; + +#[test] +fn flag_subcommand_normal() { + let matches = App::new("test") + .subcommand( + App::new("some").short_flag('S').long_flag("some").arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ), + ) + .get_matches_from(vec!["myprog", "some", "--test"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_normal_with_alias() { + let matches = App::new("test") + .subcommand( + App::new("some") + .short_flag('S') + .long_flag("S") + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .alias("result"), + ) + .get_matches_from(vec!["myprog", "result", "--test"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_short() { + let matches = App::new("test") + .subcommand( + App::new("some").short_flag('S').arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ), + ) + .get_matches_from(vec!["myprog", "-S", "--test"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_short_with_args() { + let matches = App::new("test") + .subcommand( + App::new("some").short_flag('S').arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ), + ) + .get_matches_from(vec!["myprog", "-St"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_short_with_alias() { + let matches = App::new("test") + .subcommand( + App::new("some") + .short_flag('S') + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .short_flag_alias('M') + .short_flag_alias('B'), + ) + .get_matches_from(vec!["myprog", "-Bt"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_short_with_alias_same_as_short_flag() { + let matches = App::new("test") + .subcommand(App::new("some").short_flag('S').short_flag_alias('S')) + .get_matches_from(vec!["myprog", "-S"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); +} + +#[test] +fn flag_subcommand_long_with_alias_same_as_long_flag() { + let matches = App::new("test") + .subcommand(App::new("some").long_flag("sync").long_flag_alias("sync")) + .get_matches_from(vec!["myprog", "--sync"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); +} + +#[test] +fn flag_subcommand_short_with_aliases_vis_and_hidden() { + let app = App::new("test").subcommand( + App::new("some") + .short_flag('S') + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .visible_short_flag_aliases(&['M', 'B']) + .short_flag_alias('C'), + ); + let app1 = app.clone(); + let matches1 = app1.get_matches_from(vec!["test", "-M"]); + assert_eq!(matches1.subcommand_name().unwrap(), "some"); + + let app2 = app.clone(); + let matches2 = app2.get_matches_from(vec!["test", "-C"]); + assert_eq!(matches2.subcommand_name().unwrap(), "some"); + + let app3 = app.clone(); + let matches3 = app3.get_matches_from(vec!["test", "-B"]); + assert_eq!(matches3.subcommand_name().unwrap(), "some"); +} + +#[test] +fn flag_subcommand_short_with_aliases() { + let matches = App::new("test") + .subcommand( + App::new("some") + .short_flag('S') + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .short_flag_aliases(&['M', 'B']), + ) + .get_matches_from(vec!["myprog", "-Bt"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +#[should_panic] +fn flag_subcommand_short_with_alias_hyphen() { + let _ = App::new("test") + .subcommand( + App::new("some") + .short_flag('S') + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .short_flag_alias('-'), + ) + .get_matches_from(vec!["myprog", "-Bt"]); +} + +#[test] +#[should_panic] +fn flag_subcommand_short_with_aliases_hyphen() { + let _ = App::new("test") + .subcommand( + App::new("some") + .short_flag('S') + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .short_flag_aliases(&['-', '-', '-']), + ) + .get_matches_from(vec!["myprog", "-Bt"]); +} + +#[test] +fn flag_subcommand_long() { + let matches = App::new("test") + .subcommand( + App::new("some").long_flag("some").arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ), + ) + .get_matches_from(vec!["myprog", "--some", "--test"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_long_with_alias() { + let matches = App::new("test") + .subcommand( + App::new("some") + .long_flag("some") + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .long_flag_alias("result"), + ) + .get_matches_from(vec!["myprog", "--result", "--test"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_long_with_aliases() { + let matches = App::new("test") + .subcommand( + App::new("some") + .long_flag("some") + .arg( + Arg::new("test") + .short('t') + .long("test") + .about("testing testing"), + ) + .long_flag_aliases(&["result", "someall"]), + ) + .get_matches_from(vec!["myprog", "--result", "--test"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("test")); +} + +#[test] +fn flag_subcommand_multiple() { + let matches = App::new("test") + .subcommand( + App::new("some") + .short_flag('S') + .long_flag("some") + .arg(Arg::from("-f, --flag 'some flag'")) + .arg(Arg::from("-p, --print 'print something'")) + .subcommand( + App::new("result") + .short_flag('R') + .long_flag("result") + .arg(Arg::from("-f, --flag 'some flag'")) + .arg(Arg::from("-p, --print 'print something'")), + ), + ) + .get_matches_from(vec!["myprog", "-SfpRfp"]); + assert_eq!(matches.subcommand_name().unwrap(), "some"); + let sub_matches = matches.subcommand_matches("some").unwrap(); + assert!(sub_matches.is_present("flag")); + assert!(sub_matches.is_present("print")); + assert_eq!(sub_matches.subcommand_name().unwrap(), "result"); + let result_matches = sub_matches.subcommand_matches("result").unwrap(); + assert!(result_matches.is_present("flag")); + assert!(result_matches.is_present("print")); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic = "Short option names must be unique for each argument, but \'-f\' is used by both an App named \'some\' and an Arg named \'test\'"] +fn flag_subcommand_short_conflict_with_arg() { + let _ = App::new("test") + .subcommand(App::new("some").short_flag('f').long_flag("some")) + .arg(Arg::new("test").short('f')) + .get_matches_from(vec!["myprog", "-f"]); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic = "Short option names must be unique for each argument, but \'-f\' is used by both an App named \'some\' and an App named \'result\'"] +fn flag_subcommand_short_conflict_with_alias() { + let _ = App::new("test") + .subcommand(App::new("some").short_flag('f').long_flag("some")) + .subcommand(App::new("result").short_flag('t').short_flag_alias('f')) + .get_matches_from(vec!["myprog", "-f"]); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic = "Long option names must be unique for each argument, but \'--flag\' is used by both an App named \'some\' and an App named \'result\'"] +fn flag_subcommand_long_conflict_with_alias() { + let _ = App::new("test") + .subcommand(App::new("some").long_flag("flag")) + .subcommand(App::new("result").long_flag("test").long_flag_alias("flag")) + .get_matches_from(vec!["myprog", "--flag"]); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic = "Short option names must be unique for each argument, but \'-f\' is used by both an App named \'some\' and an Arg named \'test\'"] +fn flag_subcommand_short_conflict_with_arg_alias() { + let _ = App::new("test") + .subcommand(App::new("some").short_flag('f').long_flag("some")) + .arg(Arg::new("test").short('t').short_alias('f')) + .get_matches_from(vec!["myprog", "-f"]); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic = "Long option names must be unique for each argument, but \'--some\' is used by both an App named \'some\' and an Arg named \'test\'"] +fn flag_subcommand_long_conflict_with_arg_alias() { + let _ = App::new("test") + .subcommand(App::new("some").short_flag('f').long_flag("some")) + .arg(Arg::new("test").long("test").alias("some")) + .get_matches_from(vec!["myprog", "--some"]); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic = "Long option names must be unique for each argument, but \'--flag\' is used by both an App named \'some\' and an Arg named \'flag\'"] +fn flag_subcommand_long_conflict_with_arg() { + let _ = App::new("test") + .subcommand(App::new("some").short_flag('a').long_flag("flag")) + .arg(Arg::new("flag").long("flag")) + .get_matches_from(vec!["myprog", "--flag"]); +} + +#[test] +fn flag_subcommand_conflict_with_help() { + let _ = App::new("test") + .subcommand(App::new("help").short_flag('h').long_flag("help")) + .get_matches_from(vec!["myprog", "--help"]); +} + +#[test] +fn flag_subcommand_conflict_with_version() { + let _ = App::new("test") + .subcommand(App::new("ver").short_flag('V').long_flag("version")) + .get_matches_from(vec!["myprog", "--version"]); +} + +#[test] +fn flag_subcommand_long_infer_pass() { + let m = App::new("prog") + .setting(AppSettings::InferSubcommands) + .subcommand(App::new("test").long_flag("test")) + .get_matches_from(vec!["prog", "--te"]); + assert_eq!(m.subcommand_name(), Some("test")); +} + +#[cfg(not(feature = "suggestions"))] +#[test] +fn flag_subcommand_long_infer_fail() { + let m = App::new("prog") + .setting(AppSettings::InferSubcommands) + .subcommand(App::new("test").long_flag("test")) + .subcommand(App::new("temp").long_flag("temp")) + .try_get_matches_from(vec!["prog", "--te"]); + assert!(m.is_err(), "{:#?}", m.unwrap()); + assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument); +} + +#[cfg(feature = "suggestions")] +#[test] +fn flag_subcommand_long_infer_fail() { + let m = App::new("prog") + .setting(AppSettings::InferSubcommands) + .subcommand(App::new("test").long_flag("test")) + .subcommand(App::new("temp").long_flag("temp")) + .try_get_matches_from(vec!["prog", "--te"]); + assert!(m.is_err(), "{:#?}", m.unwrap()); + assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument); +} + +#[test] +fn flag_subcommand_long_infer_pass_close() { + let m = App::new("prog") + .setting(AppSettings::InferSubcommands) + .subcommand(App::new("test").long_flag("test")) + .subcommand(App::new("temp").long_flag("temp")) + .get_matches_from(vec!["prog", "--tes"]); + assert_eq!(m.subcommand_name(), Some("test")); +} + +#[test] +fn flag_subcommand_long_infer_exact_match() { + let m = App::new("prog") + .setting(AppSettings::InferSubcommands) + .subcommand(App::new("test").long_flag("test")) + .subcommand(App::new("testa").long_flag("testa")) + .subcommand(App::new("testb").long_flag("testb")) + .get_matches_from(vec!["prog", "--test"]); + assert_eq!(m.subcommand_name(), Some("test")); +} + +static FLAG_SUBCOMMAND_HELP: &str = "pacman-query +Query the package database. + +USAGE: + pacman {query, --query, -Q} [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -i, --info ... view package information + -s, --search ... search locally-installed packages for matching strings"; + +#[test] +fn flag_subcommand_long_short_normal_usage_string() { + let app = App::new("pacman") + .about("package manager utility") + .version("5.2.1") + .setting(AppSettings::SubcommandRequiredElseHelp) + .author("Pacman Development Team") + // Query subcommand + // + // Only a few of its arguments are implemented below. + .subcommand( + App::new("query") + .short_flag('Q') + .long_flag("query") + .about("Query the package database.") + .arg( + Arg::new("search") + .short('s') + .long("search") + .about("search locally-installed packages for matching strings") + .conflicts_with("info") + .multiple_values(true), + ) + .arg( + Arg::new("info") + .long("info") + .short('i') + .conflicts_with("search") + .about("view package information") + .multiple_values(true), + ), + ); + assert!(utils::compare_output( + app, + "pacman -Qh", + FLAG_SUBCOMMAND_HELP, + false + )); +} + +static FLAG_SUBCOMMAND_NO_SHORT_HELP: &str = "pacman-query +Query the package database. + +USAGE: + pacman {query, --query} [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -i, --info ... view package information + -s, --search ... search locally-installed packages for matching strings"; + +#[test] +fn flag_subcommand_long_normal_usage_string() { + let app = App::new("pacman") + .about("package manager utility") + .version("5.2.1") + .setting(AppSettings::SubcommandRequiredElseHelp) + .author("Pacman Development Team") + // Query subcommand + // + // Only a few of its arguments are implemented below. + .subcommand( + App::new("query") + .long_flag("query") + .about("Query the package database.") + .arg( + Arg::new("search") + .short('s') + .long("search") + .about("search locally-installed packages for matching strings") + .conflicts_with("info") + .multiple_values(true), + ) + .arg( + Arg::new("info") + .long("info") + .short('i') + .conflicts_with("search") + .about("view package information") + .multiple_values(true), + ), + ); + assert!(utils::compare_output( + app, + "pacman query --help", + FLAG_SUBCOMMAND_NO_SHORT_HELP, + false + )); +} + +static FLAG_SUBCOMMAND_NO_LONG_HELP: &str = "pacman-query +Query the package database. + +USAGE: + pacman {query, -Q} [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -i, --info ... view package information + -s, --search ... search locally-installed packages for matching strings"; + +#[test] +fn flag_subcommand_short_normal_usage_string() { + let app = App::new("pacman") + .about("package manager utility") + .version("5.2.1") + .setting(AppSettings::SubcommandRequiredElseHelp) + .author("Pacman Development Team") + // Query subcommand + // + // Only a few of its arguments are implemented below. + .subcommand( + App::new("query") + .short_flag('Q') + .about("Query the package database.") + .arg( + Arg::new("search") + .short('s') + .long("search") + .about("search locally-installed packages for matching strings") + .conflicts_with("info") + .multiple_values(true), + ) + .arg( + Arg::new("info") + .long("info") + .short('i') + .conflicts_with("search") + .about("view package information") + .multiple_values(true), + ), + ); + assert!(utils::compare_output( + app, + "pacman query --help", + FLAG_SUBCOMMAND_NO_LONG_HELP, + false + )); +}