clap-rs / clap

A full featured, fast Command Line Argument Parser for Rust
docs.rs/clap
Apache License 2.0
14.32k stars 1.05k forks source link

Ability to customize the built-in `help` subcommand #5815

Open mr-briggs opened 2 days ago

mr-briggs commented 2 days ago

Please complete the following tasks

Clap Version

4.5.20

Describe your use case

It's currently fairly cumbersome to alter the about message for auto-generated help subcommands when using clap-derive. Consider the following example scenario:

use clap::{command, CommandFactory, FromArgMatches, Parser, Subcommand};

#[derive(Parser)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,
}

#[derive(Subcommand)]
pub enum Commands {
    Subcmd,
}

To use the parser as-is, we can run something like:

fn main() {
    let cli = Cli::parse();
    match &cli.command { /* ... */ }
}

But if we'd like to customize the about message for the generated help subcommand, we can no longer use the nice parse methods on Cli, and instead need to do something like:

fn main() -> Result<(), clap::Error> {
    let mut cmd = Cli::command();
    cmd.build(); // Need to build `cmd` to generate the `help` subcommand

    let cmd = cmd.mut_subcommand("help", |help_cmd| {
        help_cmd.about("A custom help message")
    });

    // Now we need to manually do what the `parse()` method would do, since we have a modified `Command`:
    let mut matches = cmd.get_matches();
    let cli = Cli::from_arg_matches(&mut matches)?;

    match &cli.command { /* ... */ }

    Ok(())
}

And for the sake of completeness, if we now run this with cargo run -- help, we'd get:

Usage: clap_example [COMMAND]

Commands:
  subcmd  
  help    A custom help message

Options:
  -h, --help  Print help

Describe the solution you'd like

One way to streamline this would be to provide a build() method on Command which returns self, so that we can directly call mut_subcommand in the #[command()] macro. For example, if Command had something like:

#[cfg(feature = "derive")]
pub fn build_for_derive(mut self) -> Self {
    self.build();
    self
}

Then we could do the following:

#[derive(Parser)]
#[command(
    build_for_derive(),
    mut_subcommand("help", |subcmd| subcmd.about("A custom help message"),
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,
}

// ... `enum Commands` elided

fn main() {
    // And now we can go back the straightforward setup:
    let cli = Cli::parse();
    match &cli.command { /* ... */ }
}

Note that trying to use build() in place of the build_for_derive() above doesn't work, as build() mutates the Command in-place and doesn't return anything, which breaks the Parser derive macro.

Alternatives, if applicable

No response

Additional Context

No response

epage commented 2 days ago

Note that changing things after a build is undefined from the APIs perspective. This isn't directly called out in #2911 but is related to that.

What we need is a way for users to get the built-in help behavior when they provide their own help. Help flags started by us auto-detecting a help flag being present (without us being told it shouldn't exist) and then keying off of that. We then switched to the ArgAction system so you could attach a Help action to any flag. Huh, I thought I had done some exploring on this idea but can only find #3773 which alludes to commands not being supported but thats it.

This could also be helpful for #1553.

Potentially related

mr-briggs commented 1 day ago

Oh interesting, thank you for the insight about build.

I had initially began this issue as "add help_about and version_about to Command" but after some digging, found that they did actually briefly exist back when it was called App. They were removed a few months later in favour of using mut_arg to modify them after building the App (see this comment), but I'm not sure how things have changed since then.

What we need is a way for users to get the built-in help behavior when they provide their own help.

Huh, that's probably more understandable for users, rather than modifying an auto-gen'd command that they might not even know about. One downside of specifying your own help command though is that it becomes a possible match arm (at least when deriving Subcommand on an enum) even though it's not a valid option under most circumstances (assuming the user-defined help would have the same behaviour of returning Err(ErrorKind::DisplayHelp ...).

That's possibly not a major concern though, compared to the upside of being able to customize the help message.