Skip to content

Commit b985eb4

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 - shuffled order in multiple command test - add support for subcommand help headings - 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 - update usage and help templates to display in custom headings order
1 parent 30ee6c7 commit b985eb4

File tree

8 files changed

+477
-16
lines changed

8 files changed

+477
-16
lines changed

Cargo.lock

Lines changed: 8 additions & 8 deletions
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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ pub struct Command {
104104
current_disp_ord: Option<usize>,
105105
subcommand_value_name: Option<Str>,
106106
subcommand_heading: Option<Str>,
107+
help_heading: Option<Option<Str>>,
107108
external_value_parser: Option<super::ValueParser>,
108109
long_help_exists: bool,
109110
deferred: Option<fn(Command) -> Command>,
@@ -3701,6 +3702,82 @@ impl Command {
37013702
self.subcommand_heading = heading.into_resettable().into_option();
37023703
self
37033704
}
3705+
3706+
/// Set a custom help heading
3707+
///
3708+
/// To place the `help` subcommand under a custom heading amend the default
3709+
/// heading using [`Command::subcommand_help_heading`].
3710+
///
3711+
/// # Examples
3712+
///
3713+
/// ```rust
3714+
/// # use clap_builder as clap;
3715+
/// # use clap::{Command, Arg};
3716+
/// Command::new("myprog")
3717+
/// .version("2.6")
3718+
/// .subcommand(
3719+
/// Command::new("show")
3720+
/// .about("Help for show")
3721+
/// )
3722+
/// .print_help()
3723+
/// # ;
3724+
/// ```
3725+
///
3726+
/// will produce
3727+
///
3728+
/// ```text
3729+
/// myprog
3730+
///
3731+
/// Usage: myprog [COMMAND]
3732+
///
3733+
/// Commands:
3734+
/// help Print this message or the help of the given subcommand(s)
3735+
/// show Help for show
3736+
///
3737+
/// Options:
3738+
/// -h, --help Print help
3739+
/// -V, --version Print version
3740+
/// ```
3741+
///
3742+
/// but usage of `help_heading`
3743+
///
3744+
/// ```rust
3745+
/// # use clap_builder as clap;
3746+
/// # use clap::{Command, Arg};
3747+
/// Command::new("myprog")
3748+
/// .version("2.6")
3749+
/// .subcommand(
3750+
/// Command::new("show")
3751+
/// .about("Help for show")
3752+
/// .help_heading("Custom heading"),
3753+
/// )
3754+
/// .print_help()
3755+
/// # ;
3756+
/// ```
3757+
///
3758+
/// will produce
3759+
///
3760+
/// ```text
3761+
/// myprog
3762+
///
3763+
/// Usage: myprog [COMMAND]
3764+
///
3765+
/// Commands:
3766+
/// help Print this message or the help of the given subcommand(s)
3767+
///
3768+
/// Custom heading:
3769+
/// show Help for show
3770+
///
3771+
/// Options:
3772+
/// -h, --help Print help
3773+
/// -V, --version Print version
3774+
/// ```
3775+
#[inline]
3776+
#[must_use]
3777+
pub fn help_heading(mut self, heading: impl IntoResettable<Str>) -> Self {
3778+
self.help_heading = Some(heading.into_resettable().into_option());
3779+
self
3780+
}
37043781
}
37053782

37063783
/// # Reflection
@@ -3940,6 +4017,15 @@ impl Command {
39404017
self.subcommand_heading.as_deref()
39414018
}
39424019

4020+
/// Get the help heading specified for this command, if any
4021+
#[inline]
4022+
pub fn get_help_heading(&self) -> Option<&str> {
4023+
self.help_heading
4024+
.as_ref()
4025+
.map(|s| s.as_deref())
4026+
.unwrap_or_default()
4027+
}
4028+
39434029
/// Returns the subcommand value name.
39444030
#[inline]
39454031
pub fn get_subcommand_value_name(&self) -> Option<&str> {
@@ -5218,6 +5304,7 @@ impl Default for Command {
52185304
current_disp_ord: Some(0),
52195305
subcommand_value_name: Default::default(),
52205306
subcommand_heading: Default::default(),
5307+
help_heading: Default::default(),
52215308
external_value_parser: Default::default(),
52225309
long_help_exists: false,
52235310
deferred: None,

clap_builder/src/output/help_template.rs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,18 @@ impl HelpTemplate<'_, '_> {
892892
subcommand,
893893
);
894894
}
895-
for (_, subcommand) in ord_v {
895+
896+
let custom_headings = self
897+
.cmd
898+
.get_subcommands()
899+
.filter_map(|cmd| cmd.get_help_heading())
900+
.collect::<FlatSet<_>>();
901+
902+
// Write commands that have no custom heading
903+
for (_, subcommand) in ord_v
904+
.iter()
905+
.filter(|item| item.1.get_help_heading().is_none())
906+
{
896907
if !*first {
897908
self.writer.push_str("\n\n");
898909
}
@@ -931,16 +942,83 @@ impl HelpTemplate<'_, '_> {
931942
sub_help.write_flat_subcommands(subcommand, first);
932943
}
933944
}
945+
946+
// Commands under custom headings
947+
if !custom_headings.is_empty() {
948+
for heading in custom_headings {
949+
let cmds = self
950+
.cmd
951+
.get_subcommands()
952+
.filter(|sc| {
953+
if let Some(help_heading) = sc.get_help_heading() {
954+
return help_heading == heading;
955+
}
956+
false
957+
})
958+
.filter(|sc| should_show_subcommand(sc))
959+
.collect::<Vec<_>>();
960+
961+
if !cmds.is_empty() {
962+
for (_, subcommand) in ord_v
963+
.clone()
964+
.into_iter()
965+
.filter(|item| item.1.get_help_heading() == Some(heading))
966+
{
967+
if !*first {
968+
self.writer.push_str("\n\n");
969+
}
970+
*first = false;
971+
972+
let heading = subcommand.get_usage_name_fallback();
973+
let about = subcommand
974+
.get_about()
975+
.or_else(|| subcommand.get_long_about())
976+
.unwrap_or_default();
977+
978+
let _ = write!(self.writer, "{header}{heading}:{header:#}",);
979+
if !about.is_empty() {
980+
let _ = write!(self.writer, "\n{about}",);
981+
}
982+
983+
let args = subcommand
984+
.get_arguments()
985+
.filter(|arg| {
986+
should_show_arg(self.use_long, arg) && !arg.is_global_set()
987+
})
988+
.collect::<Vec<_>>();
989+
if !args.is_empty() {
990+
self.writer.push_str("\n");
991+
}
992+
993+
let mut sub_help = HelpTemplate {
994+
writer: self.writer,
995+
cmd: subcommand,
996+
styles: self.styles,
997+
usage: self.usage,
998+
next_line_help: self.next_line_help,
999+
term_w: self.term_w,
1000+
use_long: self.use_long,
1001+
};
1002+
sub_help.write_args(&args, heading, option_sort_key);
1003+
if subcommand.is_flatten_help_set() {
1004+
sub_help.write_flat_subcommands(subcommand, first);
1005+
}
1006+
}
1007+
}
1008+
}
1009+
}
9341010
}
9351011

9361012
/// Writes help for subcommands of a Parser Object to the wrapped stream.
9371013
fn write_subcommands(&mut self, cmd: &Command) {
9381014
debug!("HelpTemplate::write_subcommands");
9391015
use std::fmt::Write as _;
9401016
let literal = &self.styles.get_literal();
1017+
let header = &self.styles.get_header();
9411018

9421019
// The shortest an arg can legally be is 2 (i.e. '-x')
9431020
let mut longest = 2;
1021+
// let mut has_heading = false;
9441022
let mut ord_v = BTreeMap::new();
9451023
for subcommand in cmd
9461024
.get_subcommands()
@@ -955,6 +1033,9 @@ impl HelpTemplate<'_, '_> {
9551033
if let Some(long) = subcommand.get_long_flag() {
9561034
let _ = write!(styled, ", {literal}--{long}{literal:#}",);
9571035
}
1036+
// if subcommand.get_help_heading().is_some() {
1037+
// has_heading = true;
1038+
// }
9581039
longest = longest.max(styled.display_width());
9591040
ord_v.insert((subcommand.get_display_order(), styled), subcommand);
9601041
}
@@ -963,12 +1044,63 @@ impl HelpTemplate<'_, '_> {
9631044

9641045
let next_line_help = self.will_subcommands_wrap(cmd.get_subcommands(), longest);
9651046

966-
for (i, (sc_str, sc)) in ord_v.into_iter().enumerate() {
1047+
let custom_headings = self
1048+
.cmd
1049+
.get_subcommands()
1050+
.filter_map(|cmd| cmd.get_help_heading())
1051+
.collect::<FlatSet<_>>();
1052+
1053+
// First for the commands that don't have a heading
1054+
for (i, (sc_str, sc)) in ord_v
1055+
.clone()
1056+
.into_iter()
1057+
.filter(|item| item.1.get_help_heading().is_none())
1058+
.enumerate()
1059+
{
1060+
debug!("Processing without heading {}", sc.get_name());
9671061
if 0 < i {
9681062
self.writer.push_str("\n");
9691063
}
9701064
self.write_subcommand(sc_str.1, sc, next_line_help, longest);
9711065
}
1066+
1067+
// Commands under custom headings
1068+
if !custom_headings.is_empty() {
1069+
for heading in custom_headings {
1070+
let cmds = self
1071+
.cmd
1072+
.get_subcommands()
1073+
.filter(|sc| {
1074+
if let Some(help_heading) = sc.get_help_heading() {
1075+
return help_heading == heading;
1076+
}
1077+
false
1078+
})
1079+
.filter(|sc| should_show_subcommand(sc))
1080+
.collect::<Vec<_>>();
1081+
1082+
if !cmds.is_empty() {
1083+
self.writer.push_str("\n\n");
1084+
let _ = write!(self.writer, "{header}{heading}:{header:#}\n",);
1085+
for (i, (sc_str, sc)) in ord_v
1086+
.clone()
1087+
.into_iter()
1088+
.filter(|item| item.1.get_help_heading() == Some(heading))
1089+
.enumerate()
1090+
{
1091+
debug!(
1092+
"Processing with heading: name {}, heading {}",
1093+
sc.get_name(),
1094+
heading
1095+
);
1096+
if 0 < i {
1097+
self.writer.push_str("\n");
1098+
}
1099+
self.write_subcommand(sc_str.1, sc, next_line_help, longest);
1100+
}
1101+
}
1102+
}
1103+
}
9721104
}
9731105

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

0 commit comments

Comments
 (0)