From ab5c1b36171f7d888cc88581b9a0af1066d5cd4a Mon Sep 17 00:00:00 2001 From: Maciej Dziardziel <fiedzia@gmail.com> Date: Thu, 14 Nov 2024 22:27:12 +0000 Subject: [PATCH] feat: Group subcommands by help header --- clap_builder/src/builder/command.rs | 39 +++++++++++ clap_builder/src/output/help_template.rs | 39 +++++++++-- tests/builder/subcommands.rs | 84 ++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 7 deletions(-) diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs index 0bdb6a74da7..775cbca7ee7 100644 --- a/clap_builder/src/builder/command.rs +++ b/clap_builder/src/builder/command.rs @@ -101,9 +101,11 @@ pub struct Command { subcommands: Vec<Command>, groups: Vec<ArgGroup>, current_help_heading: Option<Str>, + current_subcommand_help_heading: Option<Str>, current_disp_ord: Option<usize>, subcommand_value_name: Option<Str>, subcommand_heading: Option<Str>, + subcommand_help_heading: Option<Option<Str>>, external_value_parser: Option<super::ValueParser>, long_help_exists: bool, deferred: Option<fn(Command) -> Command>, @@ -490,6 +492,9 @@ 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 } @@ -2295,6 +2300,22 @@ 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<Str>) -> 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. @@ -3753,6 +3774,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<Item = &str> + '_ { @@ -4889,6 +4918,14 @@ impl Command { .any(|sc| sc.name != "help" && !sc.is_set(AppSettings::Hidden)) } + #[cfg(any(feature = "usage", feature = "help"))] + pub(crate) fn needs_commands_header(&self) -> bool { + self.subcommands + .iter() + .filter(|sc| !sc.is_set(AppSettings::Hidden)) + .any(|sc| sc.subcommand_help_heading.is_none()) + } + /// Check if this subcommand can be referred to as `name`. In other words, /// check if `name` is the name of this subcommand or is one of its aliases. #[inline] @@ -5131,9 +5168,11 @@ impl Default for Command { subcommands: Default::default(), groups: Default::default(), current_help_heading: Default::default(), + current_subcommand_help_heading: Default::default(), current_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/output/help_template.rs b/clap_builder/src/output/help_template.rs index da08ccd3895..85bb2aa2085 100644 --- a/clap_builder/src/output/help_template.rs +++ b/clap_builder/src/output/help_template.rs @@ -406,7 +406,9 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { .cmd .get_subcommand_help_heading() .unwrap_or(&default_help_heading); - let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); + if self.cmd.needs_commands_header() { + let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); + } self.write_subcommands(self.cmd); } @@ -864,13 +866,14 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { .filter(|subcommand| should_show_subcommand(subcommand)) { ord_v.push(( + subcommand.get_subcommand_help_heading(), subcommand.get_display_order(), subcommand.get_name(), subcommand, )); } - ord_v.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1))); - for (_, _, subcommand) in ord_v { + ord_v.sort_by(|a, b| (a.0, a.1, &a.2).cmp(&(b.0, b.1, &b.2))); + for (_, _, _, subcommand) in ord_v { if !*first { self.writer.push_str("\n\n"); } @@ -930,19 +933,41 @@ 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)); + ord_v.push(( + subcommand.get_subcommand_help_heading(), + subcommand.get_display_order(), + styled, + subcommand, + )); } - ord_v.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1))); + ord_v.sort_by(|a, b| (a.0, a.1, &a.2).cmp(&(b.0, b.1, &b.2))); debug!("HelpTemplate::write_subcommands longest = {longest}"); let next_line_help = self.will_subcommands_wrap(cmd.get_subcommands(), longest); + let mut current_help_heading = &None; + let mut help_heading_nl_needed = true; - for (i, (_, sc_str, sc)) in ord_v.into_iter().enumerate() { + for (i, (opt_help_heading, _, sc_str, sc)) in ord_v.iter().enumerate() { if 0 < i { self.writer.push_str("\n"); } - self.write_subcommand(sc_str, sc, next_line_help, longest); + if current_help_heading != opt_help_heading { + if let Some(help_heading) = opt_help_heading { + let header = &self.styles.get_header(); + if help_heading_nl_needed { + help_heading_nl_needed = false; + } else { + self.writer.push_str("\n"); + }; + let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",); + } + current_help_heading = &ord_v[i].0; + } else { + help_heading_nl_needed = false; + } + + self.write_subcommand(sc_str.clone(), sc, next_line_help, longest); } } diff --git a/tests/builder/subcommands.rs b/tests/builder/subcommands.rs index 1cf2f6db12a..cbd70f6c656 100644 --- a/tests/builder/subcommands.rs +++ b/tests/builder/subcommands.rs @@ -1,5 +1,6 @@ use clap::{arg, error::ErrorKind, Arg, ArgAction, Command}; + use super::utils; use snapbox::assert_data_eq; use snapbox::str; @@ -630,3 +631,86 @@ fn duplicate_subcommand_alias() { .subcommand(Command::new("unique").alias("repeat")) .build(); } + +#[test] +fn subcommand_help_header() { + static VISIBLE_ALIAS_HELP: &str = "\ +Usage: clap-test [COMMAND] + +Commands: + help Print this message or the help of the given subcommand(s) + +Test commands: + 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") + .subcommand_help_heading("Test commands") + ); + utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false); +} + + +#[test] +fn subcommand_help_header_hide_commands_header() { + static VISIBLE_ALIAS_HELP: &str = "\ +Usage: clap-test [COMMAND] + +Test commands: + test Some help + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .disable_help_subcommand(true) + .subcommand( + Command::new("test") + .about("Some help") + .subcommand_help_heading("Test commands") + ); + utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false); +} + + +#[test] +fn subcommand_help_header_multiple_help_headers() { + static VISIBLE_ALIAS_HELP: &str = "\ +Usage: clap-test [COMMAND] + +Test commands 1: + test1 Some help + +Test commands 2: + test2 Some help + +Options: + -h, --help Print help + -V, --version Print version +"; + + let cmd = Command::new("clap-test") + .version("2.6") + .disable_help_subcommand(true) + .subcommand( + Command::new("test1") + .about("Some help") + .subcommand_help_heading("Test commands 1") + ) + .subcommand( + Command::new("test2") + .about("Some help") + .subcommand_help_heading("Test commands 2") + ); + + utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false); +}