Repository: google/argh (rust)

Issue:
Offer ability to short circuit help output (Ex: -h)
Currently, passing `-h` doesn't work:
> Unrecognized argument: -h

Passing `-h` should work the same as `--help`, unless one of the options/switches uses `short = 'h'`.

---
And in the Usage output, it would then show 
> -h, --help            display usage information

instead of just
> --help            display usage information

Pull request:
feat: add support for custom help triggers
Fixes #30
Related to #53 
Related to #119 

This adds support for custom help triggers using `#[argh(help_triggers("-h", "--help", "help"))]`.

For example, 

```rs
/// Height options
#[derive(FromArgs)]
#[argh(help_triggers("-h", "--help", "help"))]
struct Height {
    /// how high to go
    #[argh(option)]
    height: usize,
}
```

Allows triggering help with `-h`, `--help` or `help`

```
Usage: test_arg_0 --height <height>

Height options

Options:
  --height          how high to go
  -h, --help, help  display usage information
```

Patch:
diff --git a/README.md b/README.md
index 7368162..8a03f94 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ Options:
   -j, --jump        whether or not to jump
   --height          how high to go
   --pilot-nickname  an optional nickname for the pilot
-  --help            display usage information
+  --help, help      display usage information
 ```
 
 The resulting program can then be used in any of these ways:
diff --git a/argh/src/lib.rs b/argh/src/lib.rs
index 3cbfa5f..46b1fc0 100644
--- a/argh/src/lib.rs
+++ b/argh/src/lib.rs
@@ -45,7 +45,7 @@
 //!   -j, --jump        whether or not to jump
 //!   --height          how high to go
 //!   --pilot-nickname  an optional nickname for the pilot
-//!   --help            display usage information
+//!   --help, help      display usage information
 //! ```
 //!
 //! The resulting program can then be used in any of these ways:
@@ -70,6 +70,7 @@
 //!
 //! #[derive(FromArgs)]
 //! /// Reach new heights.
+//! #[argh(help_triggers("-h", "--help", "help"))]
 //! struct GoUp {
 //!     /// an optional nickname for the pilot
 //!     #[argh(option)]
@@ -421,7 +422,7 @@ pub trait FromArgs: Sized {
     /// Command to manage a classroom.
     ///
     /// Options:
-    ///   --help            display usage information
+    ///   --help, help      display usage information
     ///
     /// Commands:
     ///   list              list all the classes.
@@ -445,7 +446,7 @@ pub trait FromArgs: Sized {
     ///
     /// Options:
     ///   --teacher-name    list classes for only this teacher.
-    ///   --help            display usage information
+    ///   --help, help      display usage information
     /// "#.to_string(),
     ///        status: Ok(()),
     ///     },
@@ -587,7 +588,7 @@ pub trait FromArgs: Sized {
     /// Command to manage a classroom.
     ///
     /// Options:
-    ///   --help            display usage information
+    ///   --help, help      display usage information
     ///
     /// Commands:
     ///   list              list all the classes.
@@ -912,7 +913,7 @@ pub fn parse_struct_args(
 
     'parse_args: while let Some(&next_arg) = remaining_args.first() {
         remaining_args = &remaining_args[1..];
-        if (next_arg == "--help" || next_arg == "help") && !options_ended {
+        if (parse_options.help_triggers.contains(&next_arg)) && !options_ended {
             help = true;
             continue;
         }
@@ -959,6 +960,9 @@ pub struct ParseStructOptions<'a> {
 
     /// The storage for argument output data.
     pub slots: &'a mut [ParseStructOption<'a>],
+
+    /// help triggers is a list of strings that trigger printing of help
+    pub help_triggers: &'a [&'a str],
 }
 
 impl<'a> ParseStructOptions<'a> {
diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs
index 68a427a..3f52331 100644
--- a/argh_derive/src/help.rs
+++ b/argh_derive/src/help.rs
@@ -26,6 +26,7 @@ pub(crate) fn help(
     ty_attrs: &TypeAttrs,
     fields: &[StructField<'_>],
     subcommand: Option<&StructField<'_>>,
+    help_triggers: &[String],
 ) -> TokenStream {
     let mut format_lit = "Usage: {command_name}".to_string();
 
@@ -83,8 +84,12 @@ pub(crate) fn help(
     for option in options {
         option_description(errors, &mut format_lit, option);
     }
-    // Also include "help"
-    option_description_format(&mut format_lit, None, "--help", "display usage information");
+    option_description_format(
+        &mut format_lit,
+        None,
+        &help_triggers.join(", "),
+        "display usage information",
+    );
 
     let subcommand_calculation;
     let subcommand_format_arg;
diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs
index 6eff785..4cbfcc3 100644
--- a/argh_derive/src/lib.rs
+++ b/argh_derive/src/lib.rs
@@ -370,10 +370,12 @@ fn impl_from_args_struct_from_args<'a>(
         quote_spanned! { impl_span => None }
     };
 
+    let help_triggers = get_help_triggers(type_attrs);
+
     let help = if cfg!(feature = "help") {
         // Identifier referring to a value containing the name of the current command as an `&[&str]`.
         let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span);
-        help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand)
+        help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand, &help_triggers)
     } else {
         quote! { String::new() }
     };
@@ -392,6 +394,7 @@ fn impl_from_args_struct_from_args<'a>(
                 argh::ParseStructOptions {
                     arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ],
                     slots: &mut [ #( #flag_output_table, )* ],
+                    help_triggers: &[ #( #help_triggers ),* ],
                 },
                 argh::ParseStructPositionals {
                     positionals: &mut [
@@ -424,6 +427,29 @@ fn impl_from_args_struct_from_args<'a>(
     method_impl
 }
 
+/// get help triggers vector from type_attrs.help_triggers as a [`Vec<String>`]
+///
+/// Defaults to vec!["--help", "help"] if type_attrs.help_triggers is None
+fn get_help_triggers(type_attrs: &TypeAttrs) -> Vec<String> {
+    let help_triggers = type_attrs.help_triggers.as_ref().map_or_else(
+        || vec!["--help".to_owned(), "help".to_owned()],
+        |s| {
+            s.iter()
+                .filter_map(|s| {
+                    let trigger = s.value();
+                    let trigger_trimmed = trigger.trim().to_owned();
+                    if trigger_trimmed.is_empty() {
+                        None
+                    } else {
+                        Some(trigger_trimmed)
+                    }
+                })
+                .collect::<Vec<_>>()
+        },
+    );
+    help_triggers
+}
+
 fn impl_from_args_struct_redact_arg_values<'a>(
     errors: &Errors,
     type_attrs: &TypeAttrs,
@@ -494,10 +520,12 @@ fn impl_from_args_struct_redact_arg_values<'a>(
         quote! { "no subcommand name" }
     };
 
+    let help_triggers = get_help_triggers(type_attrs);
+
     let help = if cfg!(feature = "help") {
         // Identifier referring to a value containing the name of the current command as an `&[&str]`.
         let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span);
-        help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand)
+        help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand, &help_triggers)
     } else {
         quote! { String::new() }
     };
@@ -512,6 +540,7 @@ fn impl_from_args_struct_redact_arg_values<'a>(
                 argh::ParseStructOptions {
                     arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ],
                     slots: &mut [ #( #flag_output_table, )* ],
+                    help_triggers: &[ #( #help_triggers ),* ],
                 },
                 argh::ParseStructPositionals {
                     positionals: &mut [
diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs
index a83ce5e..39d2fba 100644
--- a/argh_derive/src/parse_attrs.rs
+++ b/argh_derive/src/parse_attrs.rs
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+use syn::{parse::Parser, punctuated::Punctuated};
+
 use {
     crate::errors::Errors,
     proc_macro2::Span,
@@ -271,6 +273,8 @@ pub struct TypeAttrs {
     pub examples: Vec<syn::LitStr>,
     pub notes: Vec<syn::LitStr>,
     pub error_codes: Vec<(syn::LitInt, syn::LitStr)>,
+    /// Arguments that trigger printing of the help message
+    pub help_triggers: Option<Vec<syn::LitStr>>,
 }
 
 impl TypeAttrs {
@@ -317,6 +321,10 @@ impl TypeAttrs {
                     {
                         this.parse_attr_subcommand(errors, ident);
                     }
+                } else if name.is_ident("help_triggers") {
+                    if let Some(m) = errors.expect_meta_list(&meta) {
+                        Self::parse_help_triggers(m, errors, &mut this);
+                    }
                 } else {
                     errors.err(
                         &meta,
@@ -405,6 +413,24 @@ impl TypeAttrs {
             self.is_subcommand = Some(ident.clone());
         }
     }
+
+    // get the list of arguments that trigger printing of the help message as a vector of strings (help_arguments("-h", "--help", "help"))
+    fn parse_help_triggers(m: &syn::MetaList, errors: &Errors, this: &mut TypeAttrs) {
+        let parser = Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
+        match parser.parse(m.tokens.clone().into()) {
+            Ok(args) => {
+                let mut triggers = Vec::new();
+                for arg in args {
+                    if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = arg {
+                        triggers.push(lit_str);
+                    }
+                }
+
+                this.help_triggers = Some(triggers);
+            }
+            Err(err) => errors.push(err),
+        }
+    }
 }
 
 /// Represents an enum variant's attributes.
@@ -604,7 +630,8 @@ fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Op
 /// Checks that a `#![derive(FromArgs)]` enum has an `#[argh(subcommand)]`
 /// attribute and that it does not have any other type-level `#[argh(...)]` attributes.
 pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span) {
-    let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes } = type_attrs;
+    let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes, help_triggers } =
+        type_attrs;
 
     // Ensure that `#[argh(subcommand)]` is present.
     if is_subcommand.is_none() {
@@ -635,6 +662,11 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span:
     if let Some(err_code) = error_codes.first() {
         err_unused_enum_attr(errors, &err_code.0);
     }
+    if let Some(triggers) = help_triggers {
+        if let Some(trigger) = triggers.first() {
+            err_unused_enum_attr(errors, trigger);
+        }
+    }
 }
 
 fn err_unused_enum_attr(errors: &Errors, location: &impl syn::spanned::Spanned) {


Test patch:
diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs
index 0f3998a..d6a2fef 100644
--- a/argh/tests/lib.rs
+++ b/argh/tests/lib.rs
@@ -81,6 +81,29 @@ fn custom_from_str_example() {
     assert_eq!(f.five, 5);
 }
 
+#[test]
+fn help_trigger_example() {
+    /// Height options
+    #[derive(FromArgs)]
+    #[argh(help_triggers("-h", "--help", "help"))]
+    struct Height {
+        /// how high to go
+        #[argh(option)]
+        _height: usize,
+    }
+
+    assert_help_string::<Height>(
+        r#"Usage: test_arg_0 --height <height>
+
+Height options
+
+Options:
+  --height          how high to go
+  -h, --help, help  display usage information
+"#,
+    );
+}
+
 #[test]
 fn nested_from_str_example() {
     #[derive(FromArgs)]
@@ -298,7 +321,7 @@ Short description
 Options:
   --s               a switch with a description that is spread across a number
                     of lines of comments.
-  --help            display usage information
+  --help, help      display usage information
 "###,
     );
 }
@@ -322,7 +345,7 @@ A \description: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~\
 
 Options:
   --s               a \description: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~\
-  --help            display usage information
+  --help, help      display usage information
 "###,
     );
 }
@@ -459,7 +482,7 @@ Woot
 
 Options:
   -n, --n           fooey
-  --help            display usage information
+  --help, help      display usage information
 "###,
         );
     }
@@ -481,7 +504,7 @@ Woot
 
 Options:
   --option-name     fooey
-  --help            display usage information
+  --help, help      display usage information
 "###,
         );
     }
@@ -519,7 +542,7 @@ Positional Arguments:
   b                 fooey
 
 Options:
-  --help            display usage information
+  --help, help      display usage information
 "###,
         );
     }
@@ -603,7 +626,7 @@ Positional Arguments:
 Options:
   --b               woo
   --c               stuff
-  --help            display usage information
+  --help, help      display usage information
 "###,
         );
     }
@@ -1012,7 +1035,7 @@ mod fuchsia_commandline_tools_rubric {
 A type for testing `--help`/`help`
 
 Options:
-  --help            display usage information
+  --help, help      display usage information
 
 Commands:
   first             First subcommmand for testing `help`.
@@ -1023,7 +1046,7 @@ Commands:
 First subcommmand for testing `help`.
 
 Options:
-  --help            display usage information
+  --help, help      display usage information
 
 Commands:
   second            Second subcommand for testing `help`.
@@ -1034,7 +1057,7 @@ Commands:
 Second subcommand for testing `help`.
 
 Options:
-  --help            display usage information
+  --help, help      display usage information
 "###;
 
     #[test]
@@ -1215,7 +1238,7 @@ Options:
                     documentation
   -s, --scribble    write <scribble> repeatedly
   -v, --verbose     say more. Defaults to $BLAST_VERBOSE.
-  --help            display usage information
+  --help, help      display usage information
 
 Commands:
   blow-up           explosively separate
@@ -1255,7 +1278,7 @@ Positional Arguments:
   name
 
 Options:
-  --help            display usage information
+  --help, help      display usage information
 "###,
         );
     }
@@ -1285,7 +1308,7 @@ Positional Arguments:
   two               this one is real
 
 Options:
-  --help            display usage information
+  --help, help      display usage information
 "###,
         );
     }
@@ -1648,7 +1671,7 @@ Woot
 
 Options:
   -n, --n           fooey
-  --help            display usage information
+  --help, help      display usage information
 "###
             .to_owned(),
             status: Ok(()),