Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Subcommand help headings #5819

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions clap_builder/src/builder/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also need a way to set the help heading for the current command

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will likely run into name conflicts with subcommand_help_heading. We should probably deprecate that and migrate people over to setting next_subcommand_help_heading.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The derive has special handling for regular help headings, so we should probably have the same for these

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only have one next_help_heading?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, that would make api simpler, but I'd rather have clear separation between subcommand and parameter heading

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why?

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.
Expand Down Expand Up @@ -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> + '_ {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 32 additions & 7 deletions clap_builder/src/output/help_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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);
}
}

Expand Down
84 changes: 84 additions & 0 deletions tests/builder/subcommands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use clap::{arg, error::ErrorKind, Arg, ArgAction, Command};


use super::utils;
use snapbox::assert_data_eq;
use snapbox::str;
Expand Down Expand Up @@ -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)
Comment on lines +640 to +641
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this can move forward, we need to come to a resolution on help. The initial discussion should be on #1553. If we need to make new issues off of that, then we can migrate from there.


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);
}