diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs index 6472071fcee..2d2cb27c9d9 100644 --- a/clap_builder/src/builder/command.rs +++ b/clap_builder/src/builder/command.rs @@ -20,6 +20,7 @@ use crate::builder::Str; use crate::builder::StyledStr; use crate::builder::Styles; use crate::builder::{Arg, ArgGroup, ArgPredicate}; +use crate::builder::{CommandGroup}; use crate::error::ErrorKind; use crate::error::Result as ClapResult; use crate::mkeymap::MKeyMap; @@ -100,10 +101,13 @@ pub struct Command { args: MKeyMap, subcommands: Vec, groups: Vec, + command_groups: Vec, current_help_heading: Option, + current_subcommand_help_heading: Option, current_disp_ord: Option, subcommand_value_name: Option, subcommand_heading: Option, + subcommand_help_heading: Option>, external_value_parser: Option, long_help_exists: bool, deferred: Option Command>, @@ -340,6 +344,44 @@ impl Command { self.groups.push(f(a)); self } + + /// Allows one to mutate an [`CommandGroup`] after it's been added to a [`Command`]. + /// + /// # Panics + /// + /// If the argument is undefined + /// + /// # Examples + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, CommandGroup, arg, ArgGroup}; + /// + /// Command::new("foo") + /// .command_group(CommandGroup::new("bar") + /// .help_heading("Bar commands") + /// .command("bar") + /// .mut_command_group("bar", |g| g.clear()); + /// ``` + + #[must_use] + #[cfg_attr(debug_assertions, track_caller)] + pub fn mut_command_group(mut self, cmd_id: impl AsRef, f: F) -> Self + where + F: FnOnce(CommandGroup) -> CommandGroup, + { + let id = cmd_id.as_ref(); + let index = self + .command_groups + .iter() + .position(|g| g.get_id() == id) + .unwrap_or_else(|| panic!("Command group `{id}` is undefined")); + let a = self.command_groups.remove(index); + + self.command_groups.push(f(a)); + self + } + /// Allows one to mutate a [`Command`] after it's been added as a subcommand. /// /// This can be useful for modifying auto-generated arguments of nested subcommands with @@ -425,6 +467,40 @@ impl Command { self } + /// Adds an [`CommandGroup`] to the application. + /// + /// [`CommandGroup`]s are a family of related subcommands. + /// By placing them in a logical group, you can display help more clearly. + /// + /// # Examples + /// + /// The following example demonstrates using an [`CommandGroup`] to group related commands + /// when usage help is displayed. + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, CommandGroup}; + /// Command::new("add-pizza-ingredient") + /// .subcommand(Command::new("pepperoni")) + /// .subcommand(Command::new("ham")) + /// .subcommand(Command::new("peppers")) + /// .subcommand(Command::new("onion")) + /// .command_group(CommandGroup::new("meats") + /// .help_heading("Meaty ingredients") + /// .commands(["pepperoni", "ham"])) + /// .command_group(CommandGroup::new("veggies") + /// .help_heading("Vegetables") + /// .commands(["peppers", "onion"])) + /// # ; + /// ``` + #[inline] + #[must_use] + pub fn command_group(mut self, group: impl Into) -> Self { + self.command_groups.push(group.into()); + self + } + + /// Adds multiple [`ArgGroup`]s to the [`Command`] at once. /// /// # Examples @@ -456,6 +532,42 @@ impl Command { self } + /// Adds an [`CommandGroup`] to the application. + /// + /// [`CommandGroup`]s are a family of related subcommands. + /// By placing them in a logical group, you can display help more clearly. + /// + /// # Examples + /// + /// The following example demonstrates using an [`CommandGroup`] to group related commands + /// when usage help is displayed. + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, CommandGroup}; + /// Command::new("add-pizza-ingredient") + /// .subcommand(Command::new("pepperoni")) + /// .subcommand(Command::new("ham")) + /// .subcommand(Command::new("peppers")) + /// .subcommand(Command::new("onion")) + /// .command_groups([ + /// CommandGroup::new("meats") + /// .help_heading("Meaty ingredients") + /// .commands(["pepperoni", "ham"]), + /// CommandGroup::new("veggies") + /// .help_heading("Vegetables") + /// .commands(["peppers", "onion"]) + /// ]) + /// # ; + /// ``` + #[must_use] + pub fn command_groups(mut self, groups: impl IntoIterator>) -> Self { + for g in groups { + self = self.command_group(g.into()); + } + self + } + /// Adds a subcommand to the list of valid possibilities. /// /// Subcommands are effectively sub-[`Command`]s, because they can contain their own arguments, @@ -490,6 +602,8 @@ impl Command { subcmd.disp_ord.get_or_insert(current); *current_disp_ord = current + 1; } + subcmd.subcommand_help_heading + .get_or_insert_with(|| self.current_subcommand_help_heading.clone()); self.subcommands.push(subcmd); self } @@ -2137,6 +2251,32 @@ impl Command { self } + /// Set the default section heading for future subcommands. + /// + /// This will be used for any subcommand that hasn't had [`Command::subcommand_help_heading`] called. + /// + /// This is useful if the default `Commands` heading is + /// not specific enough for one's use case. + /// + /// [`Command::subcommand`]: Command::subcommand() + /// [`Command::subcommand_help_heading`]: crate::Command::subcommand_help_heading() + #[inline] + #[must_use] + pub fn next_subcommand_help_heading(mut self, heading: impl IntoResettable) -> Self { + self.current_subcommand_help_heading = heading.into_resettable().into_option(); + self + } + + /*/// Change the starting value for assigning future display orders for args. + /// + /// This will be used for any arg that hasn't had [`Arg::display_order`] called. + #[inline] + #[must_use] + pub fn next_subcommand_display_order(mut self, disp_ord: impl IntoResettable) -> Self { + self.current_subocommand_disp_ord = disp_ord.into_resettable().into_option(); + self + }*/ + /// Change the starting value for assigning future display orders for args. /// /// This will be used for any arg that hasn't had [`Arg::display_order`] called. @@ -3527,6 +3667,14 @@ impl Command { self.current_help_heading.as_deref() } + /// Get the custom section heading specified via [`Command::next_subcommand_help_heading`]. + /// + /// [`Command::subcommand_help_heading`]: Command::subcommand_help_heading() + #[inline] + pub fn get_next_subcommand_help_heading(&self) -> Option<&str> { + self.current_subcommand_help_heading.as_deref() + } + /// Iterate through the *visible* aliases for this subcommand. #[inline] pub fn get_visible_aliases(&self) -> impl Iterator + '_ { @@ -3694,6 +3842,12 @@ impl Command { self.groups.iter() } + /// Iterate through the set of command groups. + #[inline] + pub fn get_command_groups(&self) -> impl Iterator { + self.command_groups.iter() + } + /// Iterate through the set of arguments. #[inline] pub fn get_arguments(&self) -> impl Iterator { @@ -4139,6 +4293,22 @@ impl Command { self.args._build(); + for c in self.subcommands.iter_mut() { + // Fill in the command_groups + for g in &c.command_groups { + if let Some(cg) = self.command_groups.iter_mut().find(|grp| grp.id == g.id) { + cg.commands.push(c.name.clone()); + } else { + let mut cg = CommandGroup::new(g.get_id().clone()); + cg.commands.push(c.name.clone()); + self.command_groups.push(cg); + } + } + } + + + + #[allow(deprecated)] { let highest_idx = self @@ -4899,10 +5069,14 @@ impl Default for Command { args: Default::default(), subcommands: Default::default(), groups: Default::default(), + command_groups: Default::default(), current_help_heading: Default::default(), + current_subcommand_help_heading: Default::default(), current_disp_ord: Some(0), + //current_subcommand_disp_ord: Some(0), subcommand_value_name: Default::default(), subcommand_heading: Default::default(), + subcommand_help_heading: Default::default(), external_value_parser: Default::default(), long_help_exists: false, deferred: None, diff --git a/clap_builder/src/builder/command_group.rs b/clap_builder/src/builder/command_group.rs new file mode 100644 index 00000000000..187f6f9ddc8 --- /dev/null +++ b/clap_builder/src/builder/command_group.rs @@ -0,0 +1,278 @@ +// Internal +use crate::builder::IntoResettable; +use crate::builder::Str; +use crate::util::Id; + +/// Family of related [commands]. +/// +/// By placing commands in a logical group, you can make help and documentation easier to +/// understand and navigate. +/// +/// # Examples +/// +/// The following example demonstrates using a `CommandGroup` separate subcommands into two groups. +/// +/// ```rust +/// # use clap_builder as clap; +/// # use clap::{Command, command, CommandGroup, error::ErrorKind}; +/// let result = Command::new("git") +/// .subcommands([ +/// Command::new("git-apply"), +/// Command::new("git-mktree"), +/// Command::new("git-cat-file"), +/// Command::new("git-cherry"), +/// ]) +/// .command_group(CommandGroup::new("manipulation") +/// .help_heading("Manipulation commands") +/// .commands(["git-apply", "git-mktree"])) +/// .command_group(CommandGroup::new("interrogation") +/// .help_heading("Interrogation commands") +/// .commands(["git-cat-file", "git-cherry"])) +/// +/// .try_get_matches_from(vec!["git", "git-apply"]); +/// assert!(result.is_ok()); +/// ``` +/// +/// This next example demonstrates having only some commands belonging to a group +/// In the documentation, groups of commands will be displayed last, +/// while commands not belonging to any group will be displayed first. +/// ```rust +/// # use clap_builder as clap; +/// # use clap::{Command, arg, ArgGroup, Id}; +/// let result = Command::new("cmd") +/// .subcommands([ +/// Command::new("add"), +/// Command::new("remove"), +/// Command::new("show"), +/// ]) +/// .command_group(CommandGroup::new("manipulation") +/// .help_heading("Manipulation commands") +/// .commands(["add", "remove"])) +/// .try_get_matches_from(vec!["cmd", "add"]); +/// assert!(result.is_ok()); +/// ``` +/// +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct CommandGroup { + pub(crate) id: Id, + pub(crate) commands: Vec, + pub(crate) heading: Option, +} + +/// # Builder +impl CommandGroup { + /// Create a `CommandGroup` using a unique name. + /// + /// The name will be used to get values from the group and to refer to the group + /// by subcommands. + /// + /// # Examples + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, ArgGroup}; + /// CommandGroup::new("config") + /// # ; + /// ``` + pub fn new(id: impl Into) -> Self { + CommandGroup::default().id(id) + } + + /// Sets the group name. + /// + /// # Examples + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, ArgGroup}; + /// CommandGroup::default().id("config") + /// # ; + /// ``` + #[must_use] + pub fn id(mut self, id: impl Into) -> Self { + self.id = id.into(); + self + } + + /// Adds an [command] to this group by name + /// + /// # Examples + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, CommandGroup}; + /// let m = Command::new("myprog") + /// .subcommands([ + /// Command::new("add"), + /// Command::new("remove"), + /// Command::new("show"), + /// ]) + /// .command_group(CommandGroup::new("manipulation") + /// .help_heading("Manipulation commands") + /// .command("add")) + /// .get_matches_from(vec!["myprog", "add"]); + /// ``` + /// [argument]: crate::Str + #[must_use] + pub fn command(mut self, cmd_name: impl IntoResettable) -> Self { + if let Some(cmd_name) = cmd_name.into_resettable().into_option() { + self.commands.push(cmd_name); + } else { + self.commands.clear(); + } + self + } + + /// Adds multiple [commands] to this group by name + /// + /// # Examples + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, Arg, ArgGroup, ArgAction}; + /// let m = Command::new("myprog") + /// .arg(Arg::new("flag") + /// .short('f') + /// .action(ArgAction::SetTrue)) + /// .arg(Arg::new("color") + /// .short('c') + /// .action(ArgAction::SetTrue)) + /// .group(ArgGroup::new("req_flags") + /// .args(["flag", "color"])) + /// .get_matches_from(vec!["myprog", "-f"]); + /// // maybe we don't know which of the two flags was used... + /// assert!(m.contains_id("req_flags")); + /// // but we can also check individually if needed + /// assert!(m.contains_id("flag")); + /// ``` + /// [commands]: crate::Arg + #[must_use] + pub fn commands(mut self, ns: impl IntoIterator>) -> Self { + for n in ns { + self = self.command(n); + } + self + } + + + #[must_use] + pub fn help_heading(mut self, heading: impl IntoResettable) -> Self { + self.heading = heading.into_resettable().into_option(); + self + } + + + /// Getters for all args. It will return a vector of `Id` + /// + /// # Example + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{ArgGroup}; + /// let args: Vec<&str> = vec!["a1".into(), "a4".into()]; + /// let grp = ArgGroup::new("program").args(&args); + /// + /// for (pos, arg) in grp.get_args().enumerate() { + /// assert_eq!(*arg, args[pos]); + /// } + /// ``` + pub fn get_commands(&self) -> impl Iterator { + self.commands.iter() + } +} + +/// # Reflection +impl CommandGroup { + /// Get the name of the group + #[inline] + pub fn get_id(&self) -> &Id { + &self.id + } + +} + +impl From<&'_ CommandGroup> for CommandGroup { + fn from(g: &CommandGroup) -> Self { + g.clone() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn groups() { + let g = ArgGroup::new("test") + .arg("a1") + .arg("a4") + .args(["a2", "a3"]) + .required(true) + .conflicts_with("c1") + .conflicts_with_all(["c2", "c3"]) + .conflicts_with("c4") + .requires("r1") + .requires_all(["r2", "r3"]) + .requires("r4"); + + let args: Vec = vec!["a1".into(), "a4".into(), "a2".into(), "a3".into()]; + let reqs: Vec = vec!["r1".into(), "r2".into(), "r3".into(), "r4".into()]; + let confs: Vec = vec!["c1".into(), "c2".into(), "c3".into(), "c4".into()]; + + assert_eq!(g.args, args); + assert_eq!(g.requires, reqs); + assert_eq!(g.conflicts, confs); + } + + #[test] + fn test_from() { + let g = ArgGroup::new("test") + .arg("a1") + .arg("a4") + .args(["a2", "a3"]) + .required(true) + .conflicts_with("c1") + .conflicts_with_all(["c2", "c3"]) + .conflicts_with("c4") + .requires("r1") + .requires_all(["r2", "r3"]) + .requires("r4"); + + let args: Vec = vec!["a1".into(), "a4".into(), "a2".into(), "a3".into()]; + let reqs: Vec = vec!["r1".into(), "r2".into(), "r3".into(), "r4".into()]; + let confs: Vec = vec!["c1".into(), "c2".into(), "c3".into(), "c4".into()]; + + let g2 = ArgGroup::from(&g); + assert_eq!(g2.args, args); + assert_eq!(g2.requires, reqs); + assert_eq!(g2.conflicts, confs); + } + + // This test will *fail to compile* if ArgGroup is not Send + Sync + #[test] + fn arg_group_send_sync() { + fn foo(_: T) {} + foo(ArgGroup::new("test")); + } + + #[test] + fn arg_group_expose_is_multiple_helper() { + let args: Vec = vec!["a1".into(), "a4".into()]; + + let mut grp_multiple = ArgGroup::new("test_multiple").args(&args).multiple(true); + assert!(grp_multiple.is_multiple()); + + let mut grp_not_multiple = ArgGroup::new("test_multiple").args(&args).multiple(false); + assert!(!grp_not_multiple.is_multiple()); + } + + #[test] + fn arg_group_expose_get_args_helper() { + let args: Vec = vec!["a1".into(), "a4".into()]; + let grp = ArgGroup::new("program").args(&args); + + for (pos, arg) in grp.get_args().enumerate() { + assert_eq!(*arg, args[pos]); + } + } +} diff --git a/clap_builder/src/builder/mod.rs b/clap_builder/src/builder/mod.rs index 9f871d7ff7c..9fa7570672e 100644 --- a/clap_builder/src/builder/mod.rs +++ b/clap_builder/src/builder/mod.rs @@ -7,6 +7,7 @@ mod arg_group; mod arg_predicate; mod arg_settings; mod command; +mod command_group; mod ext; mod os_str; mod possible_value; @@ -33,6 +34,7 @@ pub use arg::ArgExt; pub use arg_group::ArgGroup; pub use arg_predicate::ArgPredicate; pub use command::Command; +pub use command_group::CommandGroup; #[cfg(feature = "unstable-ext")] pub use command::CommandExt; pub use os_str::OsStr; diff --git a/clap_builder/src/lib.rs b/clap_builder/src/lib.rs index 602c2fb654e..00d2c567b12 100644 --- a/clap_builder/src/lib.rs +++ b/clap_builder/src/lib.rs @@ -16,6 +16,7 @@ compile_error!("`std` feature is currently required to build `clap`"); pub use crate::builder::ArgAction; pub use crate::builder::Command; +pub use crate::builder::CommandGroup; pub use crate::builder::ValueHint; pub use crate::builder::{Arg, ArgGroup}; pub use crate::parser::ArgMatches; diff --git a/clap_builder/src/output/help_template.rs b/clap_builder/src/output/help_template.rs index da08ccd3895..bbc87fe6d92 100644 --- a/clap_builder/src/output/help_template.rs +++ b/clap_builder/src/output/help_template.rs @@ -401,12 +401,14 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self.writer.push_str("\n\n"); } first = false; - let default_help_heading = Str::from("Commands"); - let help_heading = self - .cmd - .get_subcommand_help_heading() - .unwrap_or(&default_help_heading); - let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); + if self.needs_subcmd_help_header() { + let default_help_heading = Str::from("Commands"); + let help_heading = self + .cmd + .get_subcommand_help_heading() + .unwrap_or(&default_help_heading); + let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); + } self.write_subcommands(self.cmd); } @@ -907,6 +909,28 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { } } + /// Check if this subcommand should display help header + /// which will be the case if either there are subcommands and no command groups + /// or there are subcommands and some of them do not belong to any group + #[cfg(any(feature = "usage", feature = "help"))] + pub(crate) fn needs_subcmd_help_header(&self) -> bool { + self.visible_ungroupped_subcommands() + .next() + .is_some() + } + + pub(crate) fn visible_subcommands(&self) -> impl Iterator { + self.cmd + .get_subcommands() + .filter(|subcommand| should_show_subcommand(subcommand)) + } + + #[cfg(any(feature = "usage", feature = "help"))] + pub(crate) fn visible_ungroupped_subcommands(&self) -> impl Iterator { + self.visible_subcommands() + .filter(|sc| self.cmd.get_command_groups().filter(|cg| cg.commands.contains(sc.get_name_str())).next().is_none()) + } + /// Writes help for subcommands of a Parser Object to the wrapped stream. fn write_subcommands(&mut self, cmd: &Command) { debug!("HelpTemplate::write_subcommands"); @@ -930,19 +954,71 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { let _ = write!(styled, ", {literal}--{long}{literal:#}",); } longest = longest.max(styled.display_width()); - ord_v.push((subcommand.get_display_order(), styled, subcommand)); + let no_group = self.cmd.get_command_groups().filter(|cg| cg.commands.contains(subcommand.get_name_str())).next().is_none(); + ord_v.push((subcommand.get_display_order(), styled, subcommand, no_group, subcommand.get_subcommand_help_heading())); } - ord_v.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1))); + ord_v.sort_by(|a, b| (a.4, a.0, &a.1).cmp(&(b.4, b.0, &b.1))); debug!("HelpTemplate::write_subcommands longest = {longest}"); + let mut first = true; + let mut current_help_heading = &ord_v[0].4; + //first show commands that do not belong to any group + //if groups are not used, that means all visible commands let next_line_help = self.will_subcommands_wrap(cmd.get_subcommands(), longest); + for (i, (_, sc_str, sc, no_group, help_heading)) in ord_v.iter().enumerate() { + if *no_group { + if 0 < i && !first { + self.writer.push_str("\n"); + } + if current_help_heading != help_heading { + if let Some(_help_heading) = help_heading { + let header = &self.styles.get_header(); + let _ = write!(self.writer, "\n{header}{_help_heading}:{header:#}\n",); + } - for (i, (_, sc_str, sc)) in ord_v.into_iter().enumerate() { - if 0 < i { - self.writer.push_str("\n"); + } + self.write_subcommand(sc_str.clone(), sc, next_line_help, longest); + first = false; + + } + } + //if groups are defined, show group header followed by all commands in group + if cmd.get_command_groups().next().is_some() { + let header = &self.styles.get_header(); + + for cmd_group in cmd.get_command_groups() { + + if !first { + self.writer.push_str("\n\n"); + } + if let Some(ref heading) = cmd_group.heading { + let _ = write!(self.writer, "{header}{heading}:{header:#}\n",); + first=false; + } + + let next_line_help = { + let it = cmd.get_subcommands() + .filter(|sc| { + let s: &Str = sc.get_name_str(); + cmd_group.commands.contains(&s ) + }); + self.will_subcommands_wrap(it, longest) + }; + for (j, cmd_name) in cmd_group.commands.iter().enumerate() { + match ord_v.iter_mut().filter(|(_, _, sc, _, _)| sc.get_name() == cmd_name).next() { + None => {}, + Some((_, sc_str, sc, _, _)) => { + if 0 < j { + self.writer.push_str("\n"); + } + + self.write_subcommand(sc_str.clone(), sc, next_line_help, longest); + + } + } + } } - self.write_subcommand(sc_str, sc, next_line_help, longest); } } diff --git a/examples/git.rs b/examples/git.rs index fc8fd01f79e..d33c713ef2a 100644 --- a/examples/git.rs +++ b/examples/git.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use std::path::PathBuf; -use clap::{arg, Command}; +use clap::{arg, Command, CommandGroup}; fn cli() -> Command { Command::new("git") @@ -28,7 +28,7 @@ fn cli() -> Command { .require_equals(true) .default_value("auto") .default_missing_value("always"), - ), + ).subcommand_help_heading(Some("DIFF")), ) .subcommand( Command::new("push") diff --git a/tests/builder/command_group.rs b/tests/builder/command_group.rs new file mode 100644 index 00000000000..4e762cafbd6 --- /dev/null +++ b/tests/builder/command_group.rs @@ -0,0 +1,163 @@ +use clap::{arg, error::ErrorKind, Arg, ArgAction, Command, CommandGroup}; + +use super::utils; +use snapbox::assert_data_eq; +use snapbox::str; + +#[test] +fn command_group_help_output_one_ungrouped_command() { + let visible_help: &str = "\ +Usage: clap-test [COMMAND] + +Commands: + help Print this message or the help of the given subcommand(s) + + test Some help + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .subcommand( + Command::new("test") + .about("Some help") + ) + .command_group(CommandGroup::new("Test commands") + .commands(&["test"])); + + + utils::assert_output(cmd, "clap-test --help", visible_help, false); +} + +#[test] +fn command_group_help_output_one_ungrouped_command_one_group() { + let visible_help: &str = "\ +Usage: clap-test [COMMAND] + +Commands: + help Print this message or the help of the given subcommand(s) + +Test: + test Some help + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .subcommand( + Command::new("test") + .about("Some help") + ) + .command_group(CommandGroup::new("Test commands") + .help_heading("Test") + .commands(&["test"])); + + + utils::assert_output(cmd, "clap-test --help", visible_help, false); +} + +#[test] +fn command_group_help_output_no_ungrouped_commands_no_heading() { + let visible_help: &str = "\ +Usage: clap-test [COMMAND] + + test Some help + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .subcommand( + Command::new("test") + .about("Some help") + ) + .command_group(CommandGroup::new("test_commands") + .commands(&["test", "help"])); + + + utils::assert_output(cmd, "clap-test --help", visible_help, false); +} + +#[test] +fn command_group_help_output_group_with_heading() { + let visible_help: &str = "\ +Usage: clap-test [COMMAND] + +Test: + test Some help + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .subcommand( + Command::new("test") + .about("Some help") + ) + .command_group(CommandGroup::new("test_commands") + .help_heading("Test") + .commands(&["test", "help"])); + + + utils::assert_output(cmd, "clap-test --help", visible_help, false); +} + + +#[test] +fn command_group_help_output_two_groups_with_headings() { + let visible_help: &str = "\ +Usage: clap-test [COMMAND] + +TestGroup1: + test1 Some help + test2 Some help + +TestGroup2: + test3 Some help + test4 Some help + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .subcommand( + Command::new("test1").about("Some help") + ) + .subcommand( + Command::new("test2").about("Some help") + ) + .subcommand( + Command::new("test3").about("Some help") + ) + .subcommand( + Command::new("test4").about("Some help") + ) + .command_group(CommandGroup::new("test_commands1") + .help_heading("TestGroup1") + .commands(&["test1", "test2"])) + .command_group(CommandGroup::new("test_commands2") + .help_heading("TestGroup2") + .commands(&["test3", "test4", "help"])); + + + utils::assert_output(cmd, "clap-test --help", visible_help, false); +} +