Skip to content

Commit 170e21b

Browse files
committed
feat(clap): Support custom help headings for subcommands
- Add support for custom help headings for subcommands - Allows grouping subcommands under custom headings in help output - Implement `get_subcommand_custom_help_headings` to collect custom headings - Update `HelpTemplate` to iterate and display subcommands under custom headings - Update `Usage` to include subcommands with custom headings in usage string
1 parent d9e586b commit 170e21b

File tree

3 files changed

+77
-97
lines changed

3 files changed

+77
-97
lines changed

clap_builder/src/builder/command.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::output::fmt::Stream;
2727
use crate::output::{fmt::Colorizer, write_help, Usage};
2828
use crate::parser::{ArgMatcher, ArgMatches, Parser};
2929
use crate::util::ChildGraph;
30+
use crate::util::FlatSet;
3031
use crate::util::{color::ColorChoice, Id};
3132
use crate::{Error, INTERNAL_ERROR_MSG};
3233

@@ -4427,6 +4428,12 @@ impl Command {
44274428
}
44284429
}
44294430

4431+
pub(crate) fn get_subcommand_custom_help_headings(&self) -> FlatSet<&str> {
4432+
self.get_subcommands()
4433+
.filter_map(|sc| sc.get_help_heading())
4434+
.collect::<FlatSet<_>>()
4435+
}
4436+
44304437
fn _do_parse(
44314438
&mut self,
44324439
raw_args: &mut clap_lex::RawArgs,

clap_builder/src/output/help_template.rs

Lines changed: 67 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ impl HelpTemplate<'_, '_> {
389389
.collect::<Vec<_>>();
390390
let subcmds = self.cmd.has_visible_subcommands();
391391

392-
let custom_headings = self
392+
let custom_arg_headings = self
393393
.cmd
394394
.get_arguments()
395395
.filter_map(|arg| arg.get_help_heading())
@@ -434,8 +434,8 @@ impl HelpTemplate<'_, '_> {
434434
let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",);
435435
self.write_args(&non_pos, "Options", option_sort_key);
436436
}
437-
if !custom_headings.is_empty() {
438-
for heading in custom_headings {
437+
if !custom_arg_headings.is_empty() {
438+
for heading in custom_arg_headings {
439439
let args = self
440440
.cmd
441441
.get_arguments()
@@ -891,50 +891,49 @@ impl HelpTemplate<'_, '_> {
891891
);
892892
}
893893

894-
let custom_headings = self
895-
.cmd
896-
.get_subcommands()
897-
.filter_map(|cmd| cmd.get_help_heading())
898-
.collect::<FlatSet<_>>();
894+
let custom_sc_headings = self.cmd.get_subcommand_custom_help_headings();
899895

900-
// Write commands that have no custom heading
901-
for (_, subcommand) in ord_v
902-
.iter()
903-
.filter(|item| item.1.get_help_heading().is_none())
904-
.filter(|item| item.1.get_name() != "help")
905-
{
906-
self.write_flat_subcommand(subcommand, first);
907-
}
896+
// Commands under default heading
897+
self.write_flat_subcommands_under_heading(&ord_v, None, first, false);
908898

909899
// Commands under custom headings
910-
if !custom_headings.is_empty() {
911-
for heading in custom_headings {
912-
let cmds = self
913-
.cmd
914-
.get_subcommands()
915-
.filter(|sc| {
916-
if let Some(help_heading) = sc.get_help_heading() {
917-
return help_heading == heading;
918-
}
919-
false
920-
})
921-
.filter(|sc| should_show_subcommand(sc))
922-
.collect::<Vec<_>>();
923-
924-
if !cmds.is_empty() {
925-
for (_, subcommand) in ord_v
926-
.clone()
927-
.into_iter()
928-
.filter(|item| item.1.get_help_heading() == Some(heading))
929-
{
930-
self.write_flat_subcommand(subcommand, first);
931-
}
932-
}
933-
}
900+
for heading in custom_sc_headings {
901+
self.write_flat_subcommands_under_heading(&ord_v, Some(heading), first, false);
934902
}
935903

936904
// Help command
937-
for (_, subcommand) in ord_v.iter().filter(|item| item.1.get_name() == "help") {
905+
self.write_flat_subcommands_under_heading(&ord_v, None, first, true);
906+
}
907+
908+
fn write_flat_subcommands_under_heading(
909+
&mut self,
910+
ord_v: &BTreeMap<(usize, &str), &Command>,
911+
heading: Option<&str>,
912+
first: &mut bool,
913+
include_help: bool,
914+
) {
915+
debug!("help_template::write subcommand under heading: `{heading:?}`");
916+
// If a custom heading is set ignore the include help flag
917+
let bt: Vec<(&(usize, &str), &&Command)> = if heading.is_some() {
918+
ord_v
919+
.iter()
920+
.filter(|item| item.1.get_help_heading() == heading)
921+
.collect()
922+
} else {
923+
ord_v
924+
.iter()
925+
.filter(|item| item.1.get_help_heading() == heading)
926+
.filter(|item| {
927+
if include_help {
928+
item.1.get_name() == "help"
929+
} else {
930+
item.1.get_name() != "help"
931+
}
932+
})
933+
.collect()
934+
};
935+
936+
for (_, subcommand) in bt {
938937
self.write_flat_subcommand(subcommand, first);
939938
}
940939
}
@@ -988,7 +987,6 @@ impl HelpTemplate<'_, '_> {
988987
debug!("HelpTemplate::write_subcommands");
989988
use std::fmt::Write as _;
990989
let literal = &self.styles.get_literal();
991-
let header = &self.styles.get_header();
992990

993991
// The shortest an arg can legally be is 2 (i.e. '-x')
994992
let mut longest = 2;
@@ -1006,9 +1004,6 @@ impl HelpTemplate<'_, '_> {
10061004
if let Some(long) = subcommand.get_long_flag() {
10071005
let _ = write!(styled, ", {literal}--{long}{literal:#}",);
10081006
}
1009-
// if subcommand.get_help_heading().is_some() {
1010-
// has_heading = true;
1011-
// }
10121007
longest = longest.max(styled.display_width());
10131008
ord_v.insert((subcommand.get_display_order(), styled), subcommand);
10141009
}
@@ -1017,63 +1012,44 @@ impl HelpTemplate<'_, '_> {
10171012

10181013
let next_line_help = self.will_subcommands_wrap(cmd.get_subcommands(), longest);
10191014

1020-
let custom_headings = self
1021-
.cmd
1022-
.get_subcommands()
1023-
.filter_map(|cmd| cmd.get_help_heading())
1024-
.collect::<FlatSet<_>>();
1015+
let custom_sc_headings = self.cmd.get_subcommand_custom_help_headings();
1016+
1017+
// User commands without heading
1018+
self.write_subcommands_under_heading(&ord_v, None, next_line_help, longest);
1019+
1020+
// User commands with heading
1021+
for heading in custom_sc_headings {
1022+
self.write_subcommands_under_heading(&ord_v, Some(heading), next_line_help, longest);
1023+
}
1024+
}
1025+
1026+
fn write_subcommands_under_heading(
1027+
&mut self,
1028+
ord_v: &BTreeMap<(usize, StyledStr), &Command>,
1029+
heading: Option<&str>,
1030+
next_line_help: bool,
1031+
longest: usize,
1032+
) {
1033+
debug!("help_template::write subcommand under heading: `{heading:?}`");
1034+
use std::fmt::Write as _;
1035+
let header = &self.styles.get_header();
1036+
1037+
if let Some(heading) = heading {
1038+
self.writer.push_str("\n\n");
1039+
let _ = write!(self.writer, "{header}{heading}:{header:#}\n",);
1040+
}
10251041

1026-
// First for the commands that don't have a heading
10271042
for (i, (sc_str, sc)) in ord_v
10281043
.clone()
10291044
.into_iter()
1030-
.filter(|item| item.1.get_help_heading().is_none())
1045+
.filter(|item| item.1.get_help_heading() == heading)
10311046
.enumerate()
10321047
{
1033-
debug!("Processing without heading {}", sc.get_name());
10341048
if 0 < i {
10351049
self.writer.push_str("\n");
10361050
}
10371051
self.write_subcommand(sc_str.1, sc, next_line_help, longest);
10381052
}
1039-
1040-
// Commands under custom headings
1041-
if !custom_headings.is_empty() {
1042-
for heading in custom_headings {
1043-
let cmds = self
1044-
.cmd
1045-
.get_subcommands()
1046-
.filter(|sc| {
1047-
if let Some(help_heading) = sc.get_help_heading() {
1048-
return help_heading == heading;
1049-
}
1050-
false
1051-
})
1052-
.filter(|sc| should_show_subcommand(sc))
1053-
.collect::<Vec<_>>();
1054-
1055-
if !cmds.is_empty() {
1056-
self.writer.push_str("\n\n");
1057-
let _ = write!(self.writer, "{header}{heading}:{header:#}\n",);
1058-
for (i, (sc_str, sc)) in ord_v
1059-
.clone()
1060-
.into_iter()
1061-
.filter(|item| item.1.get_help_heading() == Some(heading))
1062-
.enumerate()
1063-
{
1064-
debug!(
1065-
"Processing with heading: name {}, heading {}",
1066-
sc.get_name(),
1067-
heading
1068-
);
1069-
if 0 < i {
1070-
self.writer.push_str("\n");
1071-
}
1072-
self.write_subcommand(sc_str.1, sc, next_line_help, longest);
1073-
}
1074-
}
1075-
}
1076-
}
10771053
}
10781054

10791055
/// Will use next line help on writing subcommands.

clap_builder/src/output/usage.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,10 @@ impl Usage<'_> {
113113
}
114114
let mut cmd = self.cmd.clone();
115115
cmd.build();
116-
116+
117117
let mut first = true;
118118
// Get the custom headings
119-
let custom_headings = cmd
120-
.get_subcommands()
121-
.filter_map(|sc| sc.get_help_heading())
122-
.collect::<FlatSet<_>>();
119+
let custom_sc_headings = cmd.get_subcommand_custom_help_headings();
123120

124121
// Write commands without headings
125122
for sub in cmd
@@ -140,7 +137,7 @@ impl Usage<'_> {
140137
}
141138

142139
// Write commands with headings
143-
for heading in custom_headings {
140+
for heading in custom_sc_headings {
144141
for sub in cmd
145142
.get_subcommands()
146143
.filter(|c| !c.is_hide_set())

0 commit comments

Comments
 (0)