Skip to content

Commit

Permalink
Merge pull request #5206 from epage/flatten
Browse files Browse the repository at this point in the history
feat(help): Opt-in to flatten subcommands into parent command help
  • Loading branch information
epage authored Nov 10, 2023
2 parents 3383242 + 9c0f7a7 commit 6b2a2cc
Show file tree
Hide file tree
Showing 9 changed files with 658 additions and 17 deletions.
1 change: 1 addition & 0 deletions clap_builder/src/builder/app_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub(crate) enum AppSettings {
SubcommandsNegateReqs,
ArgsNegateSubcommands,
SubcommandPrecedenceOverArg,
FlattenHelp,
ArgRequiredElseHelp,
NextLineHelp,
DisableColoredHelp,
Expand Down
21 changes: 21 additions & 0 deletions clap_builder/src/builder/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2049,6 +2049,21 @@ impl Command {
self
}

/// Flatten subcommand help into the current command's help
///
/// This shows a summary of subcommands within the usage and help for the current command, similar to
/// `git stash --help` showing information on `push`, `pop`, etc.
/// To see more information, a user can still pass `--help` to the individual subcommands.
#[inline]
#[must_use]
pub fn flatten_help(self, yes: bool) -> Self {
if yes {
self.setting(AppSettings::FlattenHelp)
} else {
self.unset_setting(AppSettings::FlattenHelp)
}
}

/// Set the default section heading for future args.
///
/// This will be used for any arg that hasn't had [`Arg::help_heading`] called.
Expand Down Expand Up @@ -3430,6 +3445,12 @@ impl Command {
self.long_about.as_ref()
}

/// Get the custom section heading specified via [`Command::flatten_help`].
#[inline]
pub fn is_flatten_help_set(&self) -> bool {
self.is_set(AppSettings::FlattenHelp)
}

/// Get the custom section heading specified via [`Command::next_help_heading`].
///
/// [`Command::help_heading`]: Command::help_heading()
Expand Down
73 changes: 72 additions & 1 deletion clap_builder/src/output/help_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,11 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {
.filter_map(|arg| arg.get_help_heading())
.collect::<FlatSet<_>>();

let flatten = self.cmd.is_flatten_help_set();

let mut first = true;

if subcmds {
if subcmds && !flatten {
if !first {
self.writer.push_str("\n\n");
}
Expand Down Expand Up @@ -474,6 +476,11 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {
}
}
}
if flatten {
let mut cmd = self.cmd.clone();
cmd.build();
self.write_flat_subcommands(&cmd, &mut first);
}
}

/// Sorts arguments by length and display order and write their help to the wrapped stream.
Expand Down Expand Up @@ -873,6 +880,70 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {

/// Subcommand handling
impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {
/// Writes help for subcommands of a Parser Object to the wrapped stream.
fn write_flat_subcommands(&mut self, cmd: &Command, first: &mut bool) {
debug!(
"HelpTemplate::write_flat_subcommands, cmd={}, first={}",
cmd.get_name(),
*first
);
use std::fmt::Write as _;
let header = &self.styles.get_header();

let mut ord_v = Vec::new();
for subcommand in cmd
.get_subcommands()
.filter(|subcommand| should_show_subcommand(subcommand))
{
ord_v.push((
subcommand.get_display_order(),
subcommand.get_name(),
subcommand,
));
}
ord_v.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
for (_, _, subcommand) in ord_v {
if !*first {
self.writer.push_str("\n\n");
}
*first = false;

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

let _ = write!(
self.writer,
"{}{heading}:{}\n",
header.render(),
header.render_reset()
);
if !about.is_empty() {
let _ = write!(self.writer, "{about}\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,
};
let args = subcommand
.get_arguments()
.filter(|arg| should_show_arg(self.use_long, arg) && !arg.is_global_set())
.collect::<Vec<_>>();
sub_help.write_args(&args, heading, option_sort_key);
if subcommand.is_flatten_help_set() {
sub_help.write_flat_subcommands(subcommand, first);
}
}
}

/// Writes help for subcommands of a Parser Object to the wrapped stream.
fn write_subcommands(&mut self, cmd: &Command) {
debug!("HelpTemplate::write_subcommands");
Expand Down
24 changes: 22 additions & 2 deletions clap_builder/src/output/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,29 @@ impl<'cmd> Usage<'cmd> {
// Creates a usage string for display in help messages (i.e. not for errors)
fn write_help_usage(&self, styled: &mut StyledStr) {
debug!("Usage::write_help_usage");
use std::fmt::Write;

self.write_arg_usage(styled, &[], true);
self.write_subcommand_usage(styled);
if self.cmd.is_flatten_help_set() {
if !self.cmd.is_subcommand_required_set()
|| self.cmd.is_args_conflicts_with_subcommands_set()
{
self.write_arg_usage(styled, &[], true);
styled.trim_end();
let _ = write!(styled, "{}", USAGE_SEP);
}
let mut cmd = self.cmd.clone();
cmd.build();
for (i, sub) in cmd.get_subcommands().enumerate() {
if i != 0 {
styled.trim_end();
let _ = write!(styled, "{}", USAGE_SEP);
}
Usage::new(sub).write_usage_no_title(styled, &[]);
}
} else {
self.write_arg_usage(styled, &[], true);
self.write_subcommand_usage(styled);
}
}

// Creates a context aware usage string, or "smart usage" from currently used
Expand Down
26 changes: 19 additions & 7 deletions examples/git-derive.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,30 @@ Default subcommand:
```console
$ git-derive stash -h
Usage: git-derive[EXE] stash [OPTIONS]
git-derive[EXE] stash <COMMAND>

Commands:
push
pop
apply
help Print this message or the help of the given subcommand(s)
git-derive[EXE] stash push [OPTIONS]
git-derive[EXE] stash pop [STASH]
git-derive[EXE] stash apply [STASH]
git-derive[EXE] stash help [COMMAND]...

Options:
-m, --message <MESSAGE>
-h, --help Print help

git-derive[EXE] stash push:
-m, --message <MESSAGE>
-h, --help Print help

git-derive[EXE] stash pop:
-h, --help Print help
[STASH]

git-derive[EXE] stash apply:
-h, --help Print help
[STASH]

git-derive[EXE] stash help:
[COMMAND]... Print help for the subcommand(s)

$ git-derive stash push -h
Usage: git-derive[EXE] stash push [OPTIONS]

Expand Down
1 change: 1 addition & 0 deletions examples/git-derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl std::fmt::Display for ColorWhen {

#[derive(Debug, Args)]
#[command(args_conflicts_with_subcommands = true)]
#[command(flatten_help = true)]
struct StashArgs {
#[command(subcommand)]
command: Option<StashCommands>,
Expand Down
26 changes: 19 additions & 7 deletions examples/git.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,30 @@ Default subcommand:
```console
$ git stash -h
Usage: git[EXE] stash [OPTIONS]
git[EXE] stash <COMMAND>

Commands:
push
pop
apply
help Print this message or the help of the given subcommand(s)
git[EXE] stash push [OPTIONS]
git[EXE] stash pop [STASH]
git[EXE] stash apply [STASH]
git[EXE] stash help [COMMAND]...

Options:
-m, --message <MESSAGE>
-h, --help Print help

git[EXE] stash push:
-m, --message <MESSAGE>
-h, --help Print help

git[EXE] stash pop:
-h, --help Print help
[STASH]

git[EXE] stash apply:
-h, --help Print help
[STASH]

git[EXE] stash help:
[COMMAND]... Print help for the subcommand(s)

$ git stash push -h
Usage: git[EXE] stash push [OPTIONS]

Expand Down
1 change: 1 addition & 0 deletions examples/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fn cli() -> Command {
.subcommand(
Command::new("stash")
.args_conflicts_with_subcommands(true)
.flatten_help(true)
.args(push_args())
.subcommand(Command::new("push").args(push_args()))
.subcommand(Command::new("pop").arg(arg!([STASH])))
Expand Down
Loading

0 comments on commit 6b2a2cc

Please sign in to comment.