Repository: clap-rs/clap (Rust)

Issue:
No way to set `required(false)` on `@group` in `clap_app!`

### Rust Version

* rustc 1.26.0-nightly (f5631d9ac 2018-03-24)

### Affected Version of clap

* 2.31.1

### Expected Behavior Summary

Have `required(false)` set on the `ArgGroup`  when using the `clap_app!()` macro

### Actual Behavior Summary

Error thrown:

`no rules expected the token 'app'`

### Steps to Reproduce the issue

Given something like this 
```
clap_app!(
        date =>
            (@group dates !required =>      // NOTE the `!required`
             (@arg date: -d --date [STRING]
              "display time described by STRING, not 'now'")
             (@arg file: -f --file [DATEFILE]
              "like --date; once for each line of DATEFILE"))
             })
```
See that it throws an error. It appears the macro only supports `!required` in `(@arg)` declarations and not in `(@group)`.

I also tried `(@attributes !required)` in the group, and while that parsed without throwing an error it had no effect.



Discussion:
Thanks for posting, I'll look into this as it's probably just a simple omission :wink: 



Patch:
diff --git a/src/macros.rs b/src/macros.rs
index 97aedb9405f..e55554b853f 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -626,6 +626,12 @@ macro_rules! app_from_crate {
 /// `Arg::conflicts_with("FOO")`, `Arg::conflicts_with("BAR")`, and `Arg::conflicts_with("BAZ")`
 /// (note the lack of quotes around the values in the macro)
 ///
+/// # Shorthand Syntax for Groups
+///
+/// * There are short hand syntaxes for `ArgGroup` methods that accept booleans
+///   * A plus sign will set that method to `true` such as `+required` = `ArgGroup::required(true)`
+///   * An exclamation will set that method to `false` such as `!required` = `ArgGroup::required(false)`
+///
 /// [`Arg::short`]: ./struct.Arg.html#method.short
 /// [`Arg::long`]: ./struct.Arg.html#method.long
 /// [`Arg::multiple(true)`]: ./struct.Arg.html#method.multiple
@@ -657,7 +663,7 @@ macro_rules! clap_app {
             $($tt)*
         }
     };
-// Treat the application builder as an argument to set it's attributes
+// Treat the application builder as an argument to set its attributes
     (@app ($builder:expr) (@attributes $($attr:tt)*) $($tt:tt)*) => {
         clap_app!{ @app (clap_app!{ @arg ($builder) $($attr)* }) $($tt)* }
     };
@@ -667,6 +673,18 @@ macro_rules! clap_app {
             $($tt)*
         }
     };
+    (@app ($builder:expr) (@group $name:ident !$ident:ident => $($tail:tt)*) $($tt:tt)*) => {
+        clap_app!{ @app
+            (clap_app!{ @group ($builder, $crate::ArgGroup::with_name(stringify!($name)).$ident(false)) $($tail)* })
+            $($tt)*
+        }
+    };
+    (@app ($builder:expr) (@group $name:ident +$ident:ident => $($tail:tt)*) $($tt:tt)*) => {
+        clap_app!{ @app
+            (clap_app!{ @group ($builder, $crate::ArgGroup::with_name(stringify!($name)).$ident(true)) $($tail)* })
+            $($tt)*
+        }
+    };
 // Handle subcommand creation
     (@app ($builder:expr) (@subcommand $name:ident => $($tail:tt)*) $($tt:tt)*) => {
         clap_app!{ @app
@@ -687,6 +705,7 @@ macro_rules! clap_app {
 
 // Add members to group and continue argument handling with the parent builder
     (@group ($builder:expr, $group:expr)) => { $builder.group($group) };
+    // Treat the group builder as an argument to set its attributes
     (@group ($builder:expr, $group:expr) (@attributes $($attr:tt)*) $($tt:tt)*) => {
         clap_app!{ @group ($builder, clap_app!{ @arg ($group) (-) $($attr)* }) $($tt)* }
     };


Test patch:
diff --git a/tests/macros.rs b/tests/macros.rs
index 97c421d6eba..fcd62c6f339 100644
--- a/tests/macros.rs
+++ b/tests/macros.rs
@@ -1,6 +1,8 @@
 #[macro_use]
 extern crate clap;
 
+use clap::ErrorKind;
+
 #[test]
 fn basic() {
     clap_app!(claptests =>
@@ -148,6 +150,105 @@ fn quoted_arg_name() {
     assert!(matches.is_present("option2"));
 }
 
+#[test]
+fn group_macro() {
+    let app = clap_app!(claptests =>
+        (version: "0.1")
+        (about: "tests clap library")
+        (author: "Kevin K. <kbknapp@gmail.com>")
+             (@group difficulty =>
+                 (@arg hard: -h --hard "Sets hard mode")
+                 (@arg normal: -n --normal "Sets normal mode")
+                 (@arg easy: -e --easy "Sets easy mode")
+             )
+    );
+
+    let result = app.get_matches_from_safe(vec!["bin_name", "--hard"]);
+    assert!(result.is_ok());
+    let matches = result.expect("Expected to successfully match the given args.");
+    assert!(matches.is_present("difficulty"));
+    assert!(matches.is_present("hard"));
+}
+
+#[test]
+fn group_macro_set_multiple() {
+    let app = clap_app!(claptests =>
+        (version: "0.1")
+        (about: "tests clap library")
+        (author: "Kevin K. <kbknapp@gmail.com>")
+             (@group difficulty +multiple =>
+                 (@arg hard: -h --hard "Sets hard mode")
+                 (@arg normal: -n --normal "Sets normal mode")
+                 (@arg easy: -e --easy "Sets easy mode")
+             )
+    );
+
+    let result = app.get_matches_from_safe(vec!["bin_name", "--hard", "--easy"]);
+    assert!(result.is_ok());
+    let matches = result.expect("Expected to successfully match the given args.");
+    assert!(matches.is_present("difficulty"));
+    assert!(matches.is_present("hard"));
+    assert!(matches.is_present("easy"));
+    assert!(!matches.is_present("normal"));
+}
+
+#[test]
+fn group_macro_set_not_multiple() {
+    let app = clap_app!(claptests =>
+        (version: "0.1")
+        (about: "tests clap library")
+        (author: "Kevin K. <kbknapp@gmail.com>")
+             (@group difficulty !multiple =>
+                 (@arg hard: -h --hard "Sets hard mode")
+                 (@arg normal: -n --normal "Sets normal mode")
+                 (@arg easy: -e --easy "Sets easy mode")
+             )
+    );
+
+    let result = app.get_matches_from_safe(vec!["bin_name", "--hard", "--easy"]);
+    assert!(result.is_err());
+    let err = result.unwrap_err();
+    assert_eq!(err.kind, ErrorKind::ArgumentConflict);
+}
+
+#[test]
+fn group_macro_set_required() {
+    let app = clap_app!(claptests =>
+        (version: "0.1")
+        (about: "tests clap library")
+        (author: "Kevin K. <kbknapp@gmail.com>")
+             (@group difficulty +required =>
+                 (@arg hard: -h --hard "Sets hard mode")
+                 (@arg normal: -n --normal "Sets normal mode")
+                 (@arg easy: -e --easy "Sets easy mode")
+             )
+    );
+
+    let result = app.get_matches_from_safe(vec!["bin_name"]);
+    assert!(result.is_err());
+    let err = result.unwrap_err();
+    assert_eq!(err.kind, ErrorKind::MissingRequiredArgument);
+}
+
+#[test]
+fn group_macro_set_not_required() {
+    let app = clap_app!(claptests =>
+        (version: "0.1")
+        (about: "tests clap library")
+        (author: "Kevin K. <kbknapp@gmail.com>")
+             (@group difficulty !required =>
+                 (@arg hard: -h --hard "Sets hard mode")
+                 (@arg normal: -n --normal "Sets normal mode")
+                 (@arg easy: -e --easy "Sets easy mode")
+             )
+    );
+
+    let result = app.get_matches_from_safe(vec!["bin_name"]);
+    assert!(result.is_ok());
+    let matches = result.expect("Expected to successfully match the given args.");
+    assert!(!matches.is_present("difficulty"));
+}
+
 #[test]
 fn arg_enum() {
     arg_enum!{