Skip to content

Commit 503ebfd

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 support for subcommand help headings - display subcommands under their respective headings in help output - only display heading if subcommand has `help_heading` set
1 parent 27da9e2 commit 503ebfd

File tree

3 files changed

+305
-1
lines changed

3 files changed

+305
-1
lines changed

clap_builder/src/builder/command.rs

Lines changed: 92 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> {
@@ -4230,6 +4316,11 @@ impl Command {
42304316
self.is_set(AppSettings::Hidden)
42314317
}
42324318

4319+
/// Report whether [`Command::help_heading`] is set
4320+
pub fn is_help_heading_set(&self) -> bool {
4321+
self.help_heading.is_some()
4322+
}
4323+
42334324
/// Report whether [`Command::subcommand_required`] is set
42344325
pub fn is_subcommand_required_set(&self) -> bool {
42354326
self.is_set(AppSettings::SubcommandRequired)
@@ -5218,6 +5309,7 @@ impl Default for Command {
52185309
current_disp_ord: Some(0),
52195310
subcommand_value_name: Default::default(),
52205311
subcommand_heading: Default::default(),
5312+
help_heading: Default::default(),
52215313
external_value_parser: Default::default(),
52225314
long_help_exists: false,
52235315
deferred: None,

clap_builder/src/output/help_template.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,12 +963,39 @@ impl HelpTemplate<'_, '_> {
963963

964964
let next_line_help = self.will_subcommands_wrap(cmd.get_subcommands(), longest);
965965

966-
for (i, (sc_str, sc)) in ord_v.into_iter().enumerate() {
966+
// First for the commands that don't have a heading
967+
for (i, (sc_str, sc)) in ord_v
968+
.clone()
969+
.into_iter()
970+
.filter(|item| !should_show_help_heading(item.1))
971+
.enumerate()
972+
{
973+
debug!("Processing without heading {}", sc.get_name());
967974
if 0 < i {
968975
self.writer.push_str("\n");
969976
}
970977
self.write_subcommand(sc_str.1, sc, next_line_help, longest);
971978
}
979+
980+
let header = &self.styles.get_header();
981+
let mut current_heading = "";
982+
983+
// Then for the commands that do have a heading
984+
for (sc_str, sc) in ord_v
985+
.into_iter()
986+
.filter(|item| should_show_help_heading(item.1))
987+
{
988+
debug!("Processing with heading {}", sc.get_name());
989+
self.writer.push_str("\n");
990+
if sc.get_help_heading().unwrap() != current_heading {
991+
current_heading = sc.get_help_heading().unwrap();
992+
debug!("Heading changed; print new heading {}.", current_heading);
993+
self.writer.push_str("\n");
994+
let _ = write!(self.writer, "{header}{current_heading}:{header:#}\n",);
995+
}
996+
997+
self.write_subcommand(sc_str.1, sc, next_line_help, longest);
998+
}
972999
}
9731000

9741001
/// Will use next line help on writing subcommands.
@@ -1142,6 +1169,10 @@ fn should_show_subcommand(subcommand: &Command) -> bool {
11421169
!subcommand.is_hide_set()
11431170
}
11441171

1172+
fn should_show_help_heading(subcommand: &Command) -> bool {
1173+
subcommand.is_help_heading_set()
1174+
}
1175+
11451176
#[cfg(test)]
11461177
mod test {
11471178
#[test]

tests/builder/subcommands.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,3 +635,184 @@ fn duplicate_subcommand_alias() {
635635
.subcommand(Command::new("unique").alias("repeat"))
636636
.build();
637637
}
638+
639+
#[test]
640+
fn test_help_header() {
641+
static VISIBLE_ALIAS_HELP: &str = "\
642+
Usage: clap-test [COMMAND]
643+
644+
Commands:
645+
help Print this message or the help of the given subcommand(s)
646+
647+
Test commands:
648+
test Some help
649+
650+
Options:
651+
-h, --help Print help
652+
-V, --version Print version
653+
";
654+
655+
let cmd = Command::new("clap-test")
656+
.version("2.6")
657+
.subcommand(
658+
Command::new("test")
659+
.about("Some help")
660+
.help_heading("Test commands"),
661+
);
662+
utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false);
663+
}
664+
665+
#[test]
666+
fn test_help_header_multiple_help_headers() {
667+
static VISIBLE_ALIAS_HELP: &str = "\
668+
Usage: clap-test [COMMAND]
669+
670+
Commands:
671+
help Print this message or the help of the given subcommand(s)
672+
673+
Test Commands 1:
674+
test1 Some help
675+
676+
Test Commands 2:
677+
test2 Some help
678+
679+
Options:
680+
-h, --help Print help
681+
-V, --version Print version
682+
";
683+
684+
let cmd = Command::new("clap-test")
685+
.version("2.6")
686+
.subcommand(
687+
Command::new("test1")
688+
.about("Some help")
689+
.help_heading("Test Commands 1"),
690+
)
691+
.subcommand(
692+
Command::new("test2")
693+
.about("Some help")
694+
.help_heading("Test Commands 2"),
695+
);
696+
697+
utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false);
698+
}
699+
700+
#[test]
701+
fn test_help_header_hide_commands_header() {
702+
static VISIBLE_ALIAS_HELP: &str = "\
703+
Usage: clap-test [COMMAND]
704+
705+
Test commands:
706+
test Some help
707+
708+
Options:
709+
-h, --help Print help
710+
-V, --version Print version
711+
";
712+
713+
let cmd = Command::new("clap-test")
714+
.version("2.6")
715+
.disable_help_subcommand(true)
716+
.subcommand_help_heading("Test commands")
717+
.subcommand(Command::new("test").about("Some help"));
718+
utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false);
719+
}
720+
721+
#[test]
722+
fn test_multiple_commands_mixed_standard_and_custom_headers() {
723+
static VISIBLE_ALIAS_HELP: &str = "\
724+
Usage: clap-test [COMMAND]
725+
726+
Help Section:
727+
help Print this message or the help of the given subcommand(s)
728+
729+
Commands:
730+
def_cmd1 First command under default command heading
731+
def_cmd2 Second command under default command heading
732+
def_cmd3 Third command under default command heading
733+
def_cmd4 Fourth command under default command heading
734+
735+
First Custom:
736+
ch1_cmd1 First command under first custom command heading
737+
ch1_cmd2 Second command under first custom command heading
738+
ch1_cmd3 Third command under first custom command heading
739+
ch1_cmd4 Fourth command under first custom command heading
740+
741+
Second Custom:
742+
ch2_cmd1 First command under second custom command heading
743+
ch2_cmd2 Second command under second custom command heading
744+
ch2_cmd3 Third command under second custom command heading
745+
ch2_cmd4 Fourth command under second custom command heading
746+
747+
Options:
748+
-h, --help Print help
749+
-V, --version Print version
750+
";
751+
752+
let cmd = Command::new("clap-test")
753+
.version("2.6")
754+
.subcommand_help_heading("Help Section")
755+
.subcommand(
756+
Command::new("def_cmd1")
757+
.about("First command under default command heading")
758+
.help_heading("Commands"),
759+
)
760+
.subcommand(
761+
Command::new("def_cmd2")
762+
.about("Second command under default command heading")
763+
.help_heading("Commands"),
764+
)
765+
.subcommand(
766+
Command::new("def_cmd3")
767+
.about("Third command under default command heading")
768+
.help_heading("Commands"),
769+
)
770+
.subcommand(
771+
Command::new("def_cmd4")
772+
.about("Fourth command under default command heading")
773+
.help_heading("Commands"),
774+
)
775+
.subcommand(
776+
Command::new("ch1_cmd1")
777+
.about("First command under first custom command heading")
778+
.help_heading("First Custom"),
779+
)
780+
.subcommand(
781+
Command::new("ch1_cmd2")
782+
.about("Second command under first custom command heading")
783+
.help_heading("First Custom"),
784+
)
785+
.subcommand(
786+
Command::new("ch1_cmd3")
787+
.about("Third command under first custom command heading")
788+
.help_heading("First Custom"),
789+
)
790+
.subcommand(
791+
Command::new("ch1_cmd4")
792+
.about("Fourth command under first custom command heading")
793+
.help_heading("First Custom"),
794+
)
795+
.subcommand(
796+
Command::new("ch2_cmd1")
797+
.about("First command under second custom command heading")
798+
.help_heading("Second Custom"),
799+
)
800+
.subcommand(
801+
Command::new("ch2_cmd2")
802+
.about("Second command under second custom command heading")
803+
.help_heading("Second Custom"),
804+
)
805+
.subcommand(
806+
Command::new("ch2_cmd3")
807+
.about("Third command under second custom command heading")
808+
.help_heading("Second Custom"),
809+
)
810+
.subcommand(
811+
Command::new("ch2_cmd4")
812+
.about("Fourth command under second custom command heading")
813+
.help_heading("Second Custom"),
814+
);
815+
816+
utils::assert_output(cmd, "clap-test --help", VISIBLE_ALIAS_HELP, false);
817+
}
818+

0 commit comments

Comments
 (0)