Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions clap_builder/src/builder/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::output::fmt::Stream;
use crate::output::{fmt::Colorizer, write_help, Usage};
use crate::parser::{ArgMatcher, ArgMatches, Parser};
use crate::util::ChildGraph;
use crate::util::FlatSet;
use crate::util::{color::ColorChoice, Id};
use crate::{Error, INTERNAL_ERROR_MSG};

Expand Down Expand Up @@ -104,6 +105,7 @@ pub struct Command {
current_disp_ord: Option<usize>,
subcommand_value_name: Option<Str>,
subcommand_heading: Option<Str>,
help_heading: Option<Option<Str>>,
external_value_parser: Option<super::ValueParser>,
long_help_exists: bool,
deferred: Option<fn(Command) -> Command>,
Expand Down Expand Up @@ -3701,6 +3703,82 @@ impl Command {
self.subcommand_heading = heading.into_resettable().into_option();
self
}

/// Set a custom help heading
///
/// To place the `help` subcommand under a custom heading amend the default
/// heading using [`Command::subcommand_help_heading`].
///
/// # Examples
///
/// ```rust
/// # use clap_builder as clap;
/// # use clap::{Command, Arg};
/// Command::new("myprog")
/// .version("2.6")
/// .subcommand(
/// Command::new("show")
/// .about("Help for show")
/// )
/// .print_help()
/// # ;
/// ```
///
/// will produce
///
/// ```text
/// myprog
///
/// Usage: myprog [COMMAND]
///
/// Commands:
/// help Print this message or the help of the given subcommand(s)
/// show Help for show
///
/// Options:
/// -h, --help Print help
/// -V, --version Print version
/// ```
///
/// but usage of `help_heading`
///
/// ```rust
/// # use clap_builder as clap;
/// # use clap::{Command, Arg};
/// Command::new("myprog")
/// .version("2.6")
/// .subcommand(
/// Command::new("show")
/// .about("Help for show")
/// .help_heading("Custom heading"),
/// )
/// .print_help()
/// # ;
/// ```
///
/// will produce
///
/// ```text
/// myprog
///
/// Usage: myprog [COMMAND]
///
/// Commands:
/// help Print this message or the help of the given subcommand(s)
///
/// Custom heading:
/// show Help for show
///
/// Options:
/// -h, --help Print help
/// -V, --version Print version
/// ```
#[inline]
#[must_use]
pub fn help_heading(mut self, heading: impl IntoResettable<Str>) -> Self {
self.help_heading = Some(heading.into_resettable().into_option());
self
}
}

/// # Reflection
Expand Down Expand Up @@ -3940,6 +4018,15 @@ impl Command {
self.subcommand_heading.as_deref()
}

/// Get the help heading specified for this command, if any
#[inline]
pub fn get_help_heading(&self) -> Option<&str> {
self.help_heading
.as_ref()
.map(|s| s.as_deref())
.unwrap_or_default()
}

/// Returns the subcommand value name.
#[inline]
pub fn get_subcommand_value_name(&self) -> Option<&str> {
Expand Down Expand Up @@ -4341,6 +4428,13 @@ impl Command {
}
}

#[allow(dead_code)] // overcome clippy minimal report that it is unused.
pub(crate) fn get_subcommand_custom_help_headings(&self) -> FlatSet<&str> {
self.get_subcommands()
.filter_map(|sc| sc.get_help_heading())
.collect::<FlatSet<_>>()
}

fn _do_parse(
&mut self,
raw_args: &mut clap_lex::RawArgs,
Expand Down Expand Up @@ -5218,6 +5312,7 @@ impl Default for Command {
current_disp_ord: Some(0),
subcommand_value_name: Default::default(),
subcommand_heading: Default::default(),
help_heading: Default::default(),
external_value_parser: Default::default(),
long_help_exists: false,
deferred: None,
Expand Down
161 changes: 121 additions & 40 deletions clap_builder/src/output/help_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ impl HelpTemplate<'_, '_> {
.collect::<Vec<_>>();
let subcmds = self.cmd.has_visible_subcommands();

let custom_headings = self
let custom_arg_headings = self
Copy link
Member

Choose a reason for hiding this comment

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

Please put supporting but unrelated changes like this in a commit before to not distract from the actual change

Copy link
Author

Choose a reason for hiding this comment

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

By a commit before you mean a commit before the change is introduced? i.e.
commit1 - above change
commit2 - implementation of help heading

.cmd
.get_arguments()
.filter_map(|arg| arg.get_help_heading())
Expand Down Expand Up @@ -434,8 +434,8 @@ impl HelpTemplate<'_, '_> {
let _ = write!(self.writer, "{header}{help_heading}:{header:#}\n",);
self.write_args(&non_pos, "Options", option_sort_key);
}
if !custom_headings.is_empty() {
for heading in custom_headings {
if !custom_arg_headings.is_empty() {
for heading in custom_arg_headings {
let args = self
.cmd
.get_arguments()
Expand Down Expand Up @@ -879,8 +879,6 @@ impl HelpTemplate<'_, '_> {
cmd.get_name(),
*first
);
use std::fmt::Write as _;
let header = &self.styles.get_header();

let mut ord_v = BTreeMap::new();
for subcommand in cmd
Expand All @@ -892,44 +890,95 @@ impl HelpTemplate<'_, '_> {
subcommand,
);
}
for (_, subcommand) in ord_v {
if !*first {
self.writer.push_str("\n\n");
}
*first = false;

let heading = subcommand.get_usage_name_fallback();
let about = subcommand
.get_about()
.or_else(|| subcommand.get_long_about())
.unwrap_or_default();
let custom_sc_headings = self.cmd.get_subcommand_custom_help_headings();

let _ = write!(self.writer, "{header}{heading}:{header:#}",);
if !about.is_empty() {
let _ = write!(self.writer, "\n{about}",);
}
// Commands under default heading
self.write_flat_subcommands_under_heading(&ord_v, None, first, false);

let args = subcommand
.get_arguments()
.filter(|arg| should_show_arg(self.use_long, arg) && !arg.is_global_set())
.collect::<Vec<_>>();
if !args.is_empty() {
self.writer.push_str("\n");
}
// Commands under custom headings
for heading in custom_sc_headings {
self.write_flat_subcommands_under_heading(&ord_v, Some(heading), first, false);
}

let mut sub_help = HelpTemplate {
writer: self.writer,
cmd: subcommand,
styles: self.styles,
usage: self.usage,
next_line_help: self.next_line_help,
term_w: self.term_w,
use_long: self.use_long,
};
sub_help.write_args(&args, heading, option_sort_key);
if subcommand.is_flatten_help_set() {
sub_help.write_flat_subcommands(subcommand, first);
}
// Help command
self.write_flat_subcommands_under_heading(&ord_v, None, first, true);
}

fn write_flat_subcommands_under_heading(
Copy link
Member

Choose a reason for hiding this comment

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

If you are extrating logic to functions, please do that in a separate commit before this one

I'm also generally not a fan of functions inside of functions

Copy link
Author

Choose a reason for hiding this comment

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

Function inside a function was not intended and I will correct. I will review whether the logic was added as part of this code or was added in the last weeks and present in a separate commit if it is refactoring original code.

&mut self,
ord_v: &BTreeMap<(usize, &str), &Command>,
heading: Option<&str>,
first: &mut bool,
include_help: bool,
) {
debug!("help_template::write subcommand under heading: `{heading:?}`");
// If a custom heading is set ignore the include help flag
let bt: Vec<(&(usize, &str), &&Command)> = if heading.is_some() {
ord_v
.iter()
.filter(|item| item.1.get_help_heading() == heading)
.collect()
} else {
ord_v
.iter()
.filter(|item| item.1.get_help_heading() == heading)
.filter(|item| {
if include_help {
item.1.get_name() == "help"
} else {
item.1.get_name() != "help"
}
})
.collect()
};

for (_, subcommand) in bt {
self.write_flat_subcommand(subcommand, first);
}
}

fn write_flat_subcommand(&mut self, subcommand: &Command, first: &mut bool) {
use std::fmt::Write as _;
let header = &self.styles.get_header();

if !*first {
self.writer.push_str("\n\n");
}

*first = false;

let heading = subcommand.get_usage_name_fallback();
let about = subcommand
.get_about()
.or_else(|| subcommand.get_long_about())
.unwrap_or_default();

let _ = write!(self.writer, "{header}{heading}:{header:#}",);
if !about.is_empty() {
let _ = write!(self.writer, "\n{about}",);
}

let args = subcommand
.get_arguments()
.filter(|arg| should_show_arg(self.use_long, arg) && !arg.is_global_set())
.collect::<Vec<_>>();
if !args.is_empty() {
self.writer.push_str("\n");
}

let mut sub_help = HelpTemplate {
writer: self.writer,
cmd: subcommand,
styles: self.styles,
usage: self.usage,
next_line_help: self.next_line_help,
term_w: self.term_w,
use_long: self.use_long,
};
sub_help.write_args(&args, heading, option_sort_key);
if subcommand.is_flatten_help_set() {
sub_help.write_flat_subcommands(subcommand, first);
}
}

Expand Down Expand Up @@ -963,7 +1012,39 @@ impl HelpTemplate<'_, '_> {

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

for (i, (sc_str, sc)) in ord_v.into_iter().enumerate() {
let custom_sc_headings = self.cmd.get_subcommand_custom_help_headings();

// User commands without heading
self.write_subcommands_under_heading(&ord_v, None, next_line_help, longest);

// User commands with heading
for heading in custom_sc_headings {
self.write_subcommands_under_heading(&ord_v, Some(heading), next_line_help, longest);
}
}

fn write_subcommands_under_heading(
&mut self,
ord_v: &BTreeMap<(usize, StyledStr), &Command>,
heading: Option<&str>,
next_line_help: bool,
longest: usize,
) {
debug!("help_template::write subcommand under heading: `{heading:?}`");
use std::fmt::Write as _;
let header = &self.styles.get_header();

if let Some(heading) = heading {
self.writer.push_str("\n\n");
let _ = write!(self.writer, "{header}{heading}:{header:#}\n",);
}

for (i, (sc_str, sc)) in ord_v
.clone()
.into_iter()
.filter(|item| item.1.get_help_heading() == heading)
.enumerate()
{
if 0 < i {
self.writer.push_str("\n");
}
Expand Down
43 changes: 40 additions & 3 deletions clap_builder/src/output/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,49 @@ impl Usage<'_> {
}
let mut cmd = self.cmd.clone();
cmd.build();
for (i, sub) in cmd

let mut first = true;
// Get the custom headings
let custom_sc_headings = cmd.get_subcommand_custom_help_headings();

// Write commands without headings
for sub in cmd
.get_subcommands()
.filter(|c| !c.is_hide_set())
.enumerate()
.filter(|c| c.get_help_heading().is_none())
.filter(|c| c.get_name() != "help")
{
if i != 0 {
if sub.get_name() == "help" {
continue;
}
if !first {
styled.trim_end();
let _ = write!(styled, "{USAGE_SEP}");
}
first = false;
Usage::new(sub).write_usage_no_title(styled, &[]);
}

// Write commands with headings
for heading in custom_sc_headings {
for sub in cmd
.get_subcommands()
.filter(|c| !c.is_hide_set())
.filter(|c| c.get_help_heading() == Some(heading))
.filter(|c| c.get_name() != "help")
{
if !first {
styled.trim_end();
let _ = write!(styled, "{USAGE_SEP}");
}
first = false;
Usage::new(sub).write_usage_no_title(styled, &[]);
}
}

// Write help command last (regardless of custom headings)
if let Some(sub) = cmd.find_subcommand("help") {
if !first {
styled.trim_end();
let _ = write!(styled, "{USAGE_SEP}");
}
Expand Down
Loading
Loading