Skip to content

Commit 28f35c9

Browse files
committed
feat: Add subcommand custom headings
- add tests for subcommand headings feature - include tests for single and multiple help headers - add test for hiding commands header - add test for mixed standard and custom headers - add test case to verify that mixed headings are flattened correctly - test subcmds with `help_heading` - display subcommands under their respective headings in help output - only display heading if subcommand has `help_heading` set - set order for custom headings (as found) - write commands in order under headings in order
1 parent 30ee6c7 commit 28f35c9

File tree

9 files changed

+3172
-50
lines changed

9 files changed

+3172
-50
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clap_builder/src/builder/command.rs

Lines changed: 94 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

@@ -104,6 +105,7 @@ pub struct Command {
104105
current_disp_ord: Option<usize>,
105106
subcommand_value_name: Option<Str>,
106107
subcommand_heading: Option<Str>,
108+
help_heading: Option<Option<Str>>,
107109
external_value_parser: Option<super::ValueParser>,
108110
long_help_exists: bool,
109111
deferred: Option<fn(Command) -> Command>,
@@ -3701,6 +3703,82 @@ impl Command {
37013703
self.subcommand_heading = heading.into_resettable().into_option();
37023704
self
37033705
}
3706+
3707+
/// Set a custom help heading
3708+
///
3709+
/// To place the `help` subcommand under a custom heading amend the default
3710+
/// heading using [`Command::subcommand_help_heading`].
3711+
///
3712+
/// # Examples
3713+
///
3714+
/// ```rust
3715+
/// # use clap_builder as clap;
3716+
/// # use clap::{Command, Arg};
3717+
/// Command::new("myprog")
3718+
/// .version("2.6")
3719+
/// .subcommand(
3720+
/// Command::new("show")
3721+
/// .about("Help for show")
3722+
/// )
3723+
/// .print_help()
3724+
/// # ;
3725+
/// ```
3726+
///
3727+
/// will produce
3728+
///
3729+
/// ```text
3730+
/// myprog
3731+
///
3732+
/// Usage: myprog [COMMAND]
3733+
///
3734+
/// Commands:
3735+
/// help Print this message or the help of the given subcommand(s)
3736+
/// show Help for show
3737+
///
3738+
/// Options:
3739+
/// -h, --help Print help
3740+
/// -V, --version Print version
3741+
/// ```
3742+
///
3743+
/// but usage of `help_heading`
3744+
///
3745+
/// ```rust
3746+
/// # use clap_builder as clap;
3747+
/// # use clap::{Command, Arg};
3748+
/// Command::new("myprog")
3749+
/// .version("2.6")
3750+
/// .subcommand(
3751+
/// Command::new("show")
3752+
/// .about("Help for show")
3753+
/// .help_heading("Custom heading"),
3754+
/// )
3755+
/// .print_help()
3756+
/// # ;
3757+
/// ```
3758+
///
3759+
/// will produce
3760+
///
3761+
/// ```text
3762+
/// myprog
3763+
///
3764+
/// Usage: myprog [COMMAND]
3765+
///
3766+
/// Commands:
3767+
/// help Print this message or the help of the given subcommand(s)
3768+
///
3769+
/// Custom heading:
3770+
/// show Help for show
3771+
///
3772+
/// Options:
3773+
/// -h, --help Print help
3774+
/// -V, --version Print version
3775+
/// ```
3776+
#[inline]
3777+
#[must_use]
3778+
pub fn help_heading(mut self, heading: impl IntoResettable<Str>) -> Self {
3779+
self.help_heading = Some(heading.into_resettable().into_option());
3780+
self
3781+
}
37043782
}
37053783

37063784
/// # Reflection
@@ -3940,6 +4018,15 @@ impl Command {
39404018
self.subcommand_heading.as_deref()
39414019
}
39424020

4021+
/// Get the help heading specified for this command, if any
4022+
#[inline]
4023+
pub fn get_help_heading(&self) -> Option<&str> {
4024+
self.help_heading
4025+
.as_ref()
4026+
.map(|s| s.as_deref())
4027+
.unwrap_or_default()
4028+
}
4029+
39434030
/// Returns the subcommand value name.
39444031
#[inline]
39454032
pub fn get_subcommand_value_name(&self) -> Option<&str> {
@@ -4341,6 +4428,12 @@ impl Command {
43414428
}
43424429
}
43434430

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+
43444437
fn _do_parse(
43454438
&mut self,
43464439
raw_args: &mut clap_lex::RawArgs,
@@ -5218,6 +5311,7 @@ impl Default for Command {
52185311
current_disp_ord: Some(0),
52195312
subcommand_value_name: Default::default(),
52205313
subcommand_heading: Default::default(),
5314+
help_heading: Default::default(),
52215315
external_value_parser: Default::default(),
52225316
long_help_exists: false,
52235317
deferred: None,

clap_builder/src/output/help_template.rs

Lines changed: 121 additions & 40 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()
@@ -879,8 +879,6 @@ impl HelpTemplate<'_, '_> {
879879
cmd.get_name(),
880880
*first
881881
);
882-
use std::fmt::Write as _;
883-
let header = &self.styles.get_header();
884882

885883
let mut ord_v = BTreeMap::new();
886884
for subcommand in cmd
@@ -892,44 +890,95 @@ impl HelpTemplate<'_, '_> {
892890
subcommand,
893891
);
894892
}
895-
for (_, subcommand) in ord_v {
896-
if !*first {
897-
self.writer.push_str("\n\n");
898-
}
899-
*first = false;
900893

901-
let heading = subcommand.get_usage_name_fallback();
902-
let about = subcommand
903-
.get_about()
904-
.or_else(|| subcommand.get_long_about())
905-
.unwrap_or_default();
894+
let custom_sc_headings = self.cmd.get_subcommand_custom_help_headings();
906895

907-
let _ = write!(self.writer, "{header}{heading}:{header:#}",);
908-
if !about.is_empty() {
909-
let _ = write!(self.writer, "\n{about}",);
910-
}
896+
// Commands under default heading
897+
self.write_flat_subcommands_under_heading(&ord_v, None, first, false);
911898

912-
let args = subcommand
913-
.get_arguments()
914-
.filter(|arg| should_show_arg(self.use_long, arg) && !arg.is_global_set())
915-
.collect::<Vec<_>>();
916-
if !args.is_empty() {
917-
self.writer.push_str("\n");
918-
}
899+
// Commands under custom headings
900+
for heading in custom_sc_headings {
901+
self.write_flat_subcommands_under_heading(&ord_v, Some(heading), first, false);
902+
}
919903

920-
let mut sub_help = HelpTemplate {
921-
writer: self.writer,
922-
cmd: subcommand,
923-
styles: self.styles,
924-
usage: self.usage,
925-
next_line_help: self.next_line_help,
926-
term_w: self.term_w,
927-
use_long: self.use_long,
928-
};
929-
sub_help.write_args(&args, heading, option_sort_key);
930-
if subcommand.is_flatten_help_set() {
931-
sub_help.write_flat_subcommands(subcommand, first);
932-
}
904+
// Help command
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 {
937+
self.write_flat_subcommand(subcommand, first);
938+
}
939+
}
940+
941+
fn write_flat_subcommand(&mut self, subcommand: &Command, first: &mut bool) {
942+
use std::fmt::Write as _;
943+
let header = &self.styles.get_header();
944+
945+
if !*first {
946+
self.writer.push_str("\n\n");
947+
}
948+
949+
*first = false;
950+
951+
let heading = subcommand.get_usage_name_fallback();
952+
let about = subcommand
953+
.get_about()
954+
.or_else(|| subcommand.get_long_about())
955+
.unwrap_or_default();
956+
957+
let _ = write!(self.writer, "{header}{heading}:{header:#}",);
958+
if !about.is_empty() {
959+
let _ = write!(self.writer, "\n{about}",);
960+
}
961+
962+
let args = subcommand
963+
.get_arguments()
964+
.filter(|arg| should_show_arg(self.use_long, arg) && !arg.is_global_set())
965+
.collect::<Vec<_>>();
966+
if !args.is_empty() {
967+
self.writer.push_str("\n");
968+
}
969+
970+
let mut sub_help = HelpTemplate {
971+
writer: self.writer,
972+
cmd: subcommand,
973+
styles: self.styles,
974+
usage: self.usage,
975+
next_line_help: self.next_line_help,
976+
term_w: self.term_w,
977+
use_long: self.use_long,
978+
};
979+
sub_help.write_args(&args, heading, option_sort_key);
980+
if subcommand.is_flatten_help_set() {
981+
sub_help.write_flat_subcommands(subcommand, first);
933982
}
934983
}
935984

@@ -963,7 +1012,39 @@ impl HelpTemplate<'_, '_> {
9631012

9641013
let next_line_help = self.will_subcommands_wrap(cmd.get_subcommands(), longest);
9651014

966-
for (i, (sc_str, sc)) in ord_v.into_iter().enumerate() {
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+
}
1041+
1042+
for (i, (sc_str, sc)) in ord_v
1043+
.clone()
1044+
.into_iter()
1045+
.filter(|item| item.1.get_help_heading() == heading)
1046+
.enumerate()
1047+
{
9671048
if 0 < i {
9681049
self.writer.push_str("\n");
9691050
}

clap_builder/src/output/usage.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,49 @@ impl Usage<'_> {
113113
}
114114
let mut cmd = self.cmd.clone();
115115
cmd.build();
116-
for (i, sub) in cmd
116+
117+
let mut first = true;
118+
// Get the custom headings
119+
let custom_sc_headings = cmd.get_subcommand_custom_help_headings();
120+
121+
// Write commands without headings
122+
for sub in cmd
117123
.get_subcommands()
118124
.filter(|c| !c.is_hide_set())
119-
.enumerate()
125+
.filter(|c| c.get_help_heading().is_none())
126+
.filter(|c| c.get_name() != "help")
120127
{
121-
if i != 0 {
128+
if sub.get_name() == "help" {
129+
continue;
130+
}
131+
if !first {
132+
styled.trim_end();
133+
let _ = write!(styled, "{USAGE_SEP}");
134+
}
135+
first = false;
136+
Usage::new(sub).write_usage_no_title(styled, &[]);
137+
}
138+
139+
// Write commands with headings
140+
for heading in custom_sc_headings {
141+
for sub in cmd
142+
.get_subcommands()
143+
.filter(|c| !c.is_hide_set())
144+
.filter(|c| c.get_help_heading() == Some(heading))
145+
.filter(|c| c.get_name() != "help")
146+
{
147+
if !first {
148+
styled.trim_end();
149+
let _ = write!(styled, "{USAGE_SEP}");
150+
}
151+
first = false;
152+
Usage::new(sub).write_usage_no_title(styled, &[]);
153+
}
154+
}
155+
156+
// Write help command last (regardless of custom headings)
157+
if let Some(sub) = cmd.find_subcommand("help") {
158+
if !first {
122159
styled.trim_end();
123160
let _ = write!(styled, "{USAGE_SEP}");
124161
}

0 commit comments

Comments
 (0)