Repository: clap-rs/clap
Pull request #5574 (2024-07-08 22:33:02)

feat: Show user defined help flags in hints
Discussion at https://github.com/clap-rs/clap/discussions/5573

The first commit adds test cases, the second updates the "try 'help'" hints to use user-defined help flags.



Patch:
diff --git a/clap_builder/src/error/format.rs b/clap_builder/src/error/format.rs
index 0f8644f6..8696c79b 100644
--- a/clap_builder/src/error/format.rs
+++ b/clap_builder/src/error/format.rs
@@ -3,6 +3,8 @@
 #![cfg_attr(not(feature = "error-context"), allow(dead_code))]
 #![cfg_attr(not(feature = "error-context"), allow(unused_imports))]
 
+use std::borrow::Cow;
+
 use crate::builder::Command;
 use crate::builder::StyledStr;
 use crate::builder::Styles;
@@ -12,6 +14,7 @@ use crate::error::ContextKind;
 use crate::error::ContextValue;
 use crate::error::ErrorKind;
 use crate::output::TAB;
+use crate::ArgAction;
 
 /// Defines how to format an error for displaying to the user
 pub trait ErrorFormatter: Sized {
@@ -120,7 +123,7 @@ impl ErrorFormatter for RichFormatter {
             put_usage(&mut styled, usage);
         }
 
-        try_help(&mut styled, styles, error.inner.help_flag);
+        try_help(&mut styled, styles, error.inner.help_flag.as_deref());
 
         styled
     }
@@ -461,7 +464,7 @@ pub(crate) fn format_error_message(
         put_usage(&mut styled, usage);
     }
     if let Some(cmd) = cmd {
-        try_help(&mut styled, styles, get_help_flag(cmd));
+        try_help(&mut styled, styles, get_help_flag(cmd).as_deref());
     }
     styled
 }
@@ -480,16 +483,34 @@ fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
     styled.push_styled(usage);
 }
 
-pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> {
+pub(crate) fn get_help_flag(cmd: &Command) -> Option<Cow<'static, str>> {
     if !cmd.is_disable_help_flag_set() {
-        Some("--help")
+        Some(Cow::Borrowed("--help"))
+    } else if let Some(flag) = get_user_help_flag(cmd) {
+        Some(Cow::Owned(flag))
     } else if cmd.has_subcommands() && !cmd.is_disable_help_subcommand_set() {
-        Some("help")
+        Some(Cow::Borrowed("help"))
     } else {
         None
     }
 }
 
+fn get_user_help_flag(cmd: &Command) -> Option<String> {
+    let arg = cmd.get_arguments().find(|arg| match arg.get_action() {
+        ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong => true,
+        ArgAction::Append
+        | ArgAction::Count
+        | ArgAction::SetTrue
+        | ArgAction::SetFalse
+        | ArgAction::Set
+        | ArgAction::Version => false,
+    })?;
+
+    arg.get_long()
+        .map(|long| format!("--{long}"))
+        .or_else(|| arg.get_short().map(|short| format!("-{short}")))
+}
+
 fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
     if let Some(help) = help {
         use std::fmt::Write as _;
diff --git a/clap_builder/src/error/mod.rs b/clap_builder/src/error/mod.rs
index 1e8fcd57..cfba5a81 100644
--- a/clap_builder/src/error/mod.rs
+++ b/clap_builder/src/error/mod.rs
@@ -69,7 +69,7 @@ struct ErrorInner {
     context: FlatMap<ContextKind, ContextValue>,
     message: Option<Message>,
     source: Option<Box<dyn error::Error + Send + Sync>>,
-    help_flag: Option<&'static str>,
+    help_flag: Option<Cow<'static, str>>,
     styles: Styles,
     color_when: ColorChoice,
     color_help_when: ColorChoice,
@@ -319,7 +319,7 @@ impl<F: ErrorFormatter> Error<F> {
         self
     }
 
-    pub(crate) fn set_help_flag(mut self, help_flag: Option<&'static str>) -> Self {
+    pub(crate) fn set_help_flag(mut self, help_flag: Option<Cow<'static, str>>) -> Self {
         self.inner.help_flag = help_flag;
         self
     }


Test patch:
diff --git a/tests/builder/help.rs b/tests/builder/help.rs
index a66f5f0d..79afc396 100644
--- a/tests/builder/help.rs
+++ b/tests/builder/help.rs
@@ -81,7 +81,9 @@ fn help_multi_subcommand_error() {
         .try_get_matches_from(["ctest", "help", "subcmd", "multi", "foo"])
         .unwrap_err();
 
-    assert_data_eq!(err.to_string(), str![[r#"
+    assert_data_eq!(
+        err.to_string(),
+        str![[r#"
 error: unrecognized subcommand 'foo'
 
 Usage: ctest subcmd multi [OPTIONS]
@@ -351,6 +353,158 @@ Options:
     utils::assert_output(cmd, "ctest --help", DEFAULT_HELP, false);
 }
 
+#[test]
+fn try_help_default() {
+    static DEFAULT_HELP: &str = "\
+error: unexpected argument 'bar' found
+
+Usage: ctest
+
+For more information, try '--help'.
+";
+
+    let cmd = Command::new("ctest").version("1.0").term_width(0);
+    utils::assert_output(cmd, "ctest bar", DEFAULT_HELP, true);
+}
+
+#[test]
+fn try_help_custom_flag() {
+    static EXPECTED_HELP: &str = "\
+error: unexpected argument 'bar' found
+
+Usage: ctest
+
+For more information, try '--help'.
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .disable_help_flag(true)
+        .arg(
+            Arg::new("help")
+                .long("help")
+                .short('h')
+                .action(ArgAction::Help),
+        )
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", EXPECTED_HELP, true);
+}
+
+#[test]
+fn try_help_custom_flag_short() {
+    static EXPECTED_HELP: &str = "\
+error: unexpected argument 'bar' found
+
+Usage: ctest
+
+For more information, try '-h'.
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .disable_help_flag(true)
+        .arg(Arg::new("help").short('h').action(ArgAction::HelpShort))
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", EXPECTED_HELP, true);
+}
+
+#[test]
+fn try_help_custom_flag_long() {
+    static EXPECTED_HELP: &str = "\
+error: unexpected argument 'bar' found
+
+Usage: ctest
+
+For more information, try '--help'.
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .disable_help_flag(true)
+        .arg(Arg::new("help").long("help").action(ArgAction::HelpShort))
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", EXPECTED_HELP, true);
+}
+
+#[test]
+fn try_help_custom_flag_no_action() {
+    static EXPECTED_HELP: &str = "\
+error: unexpected argument 'bar' found
+
+Usage: ctest
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .disable_help_flag(true)
+        // Note `ArgAction::Help` is excluded
+        .arg(Arg::new("help").long("help").global(true))
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", EXPECTED_HELP, true);
+}
+
+#[test]
+fn try_help_subcommand_default() {
+    static DEFAULT_HELP: &str = "\
+error: unrecognized subcommand 'bar'
+
+Usage: ctest [COMMAND]
+
+For more information, try '--help'.
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .subcommand(Command::new("foo"))
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", DEFAULT_HELP, true);
+}
+
+#[test]
+fn try_help_subcommand_custom_flag() {
+    static EXPECTED_HELP: &str = "\
+error: unrecognized subcommand 'bar'
+
+Usage: ctest [COMMAND]
+
+For more information, try '--help'.
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .disable_help_flag(true)
+        .arg(
+            Arg::new("help")
+                .long("help")
+                .short('h')
+                .action(ArgAction::Help)
+                .global(true),
+        )
+        .subcommand(Command::new("foo"))
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", EXPECTED_HELP, true);
+}
+
+#[test]
+fn try_help_subcommand_custom_flag_no_action() {
+    static EXPECTED_HELP: &str = "\
+error: unrecognized subcommand 'bar'
+
+Usage: ctest [COMMAND]
+
+For more information, try 'help'.
+";
+
+    let cmd = Command::new("ctest")
+        .version("1.0")
+        .disable_help_flag(true)
+        // Note `ArgAction::Help` is excluded
+        .arg(Arg::new("help").long("help").global(true))
+        .subcommand(Command::new("foo"))
+        .term_width(0);
+    utils::assert_output(cmd, "ctest bar", EXPECTED_HELP, true);
+}
+
 #[test]
 #[cfg(feature = "wrap_help")]
 fn wrapped_help() {