Repository: clap-rs/clap
Pull request #3690 (2022-05-04 17:26:50)

fix(parser): Exclusive overrides required
`Arg::exclusive` is just another way of defining conflicts, so a
present-exclusive arg should override required like other conflicts.

Instead of going through the message of enumerating all other arguments
as exclusive, I shortcutted it and special case exclusive in the
required check like we do with conflicts.  The big downside is the
implicit coupling between the code paths rather than having a consistent
abstraction for covering conflicts.

This isn't a breaking change because if someone defined an exclusive arg
as a sibling to a required arg, the exclusive arg could never be used,
it always errored, and so no valid application can be written with it.

Fixes https://github.com/clap-rs/clap/issues/3595


Patch:
diff --git a/src/parse/validator.rs b/src/parse/validator.rs
index 4a709efc..5b801de8 100644
--- a/src/parse/validator.rs
+++ b/src/parse/validator.rs
@@ -223,6 +223,11 @@ impl<'help, 'cmd> Validator<'help, 'cmd> {
         debug!("Validator::validate_exclusive");
         // Not bothering to filter for `check_explicit` since defaults shouldn't play into this
         let args_count = matcher.arg_names().count();
+        if args_count <= 1 {
+            // Nothing present to conflict with
+            return Ok(());
+        }
+
         matcher
             .arg_names()
             .filter_map(|name| {
@@ -474,11 +479,22 @@ impl<'help, 'cmd> Validator<'help, 'cmd> {
         debug!("Validator::validate_required: required={:?}", self.required);
         self.gather_requires(matcher);
 
+        let is_exclusive_present = matcher.arg_names().any(|name| {
+            self.cmd
+                .find(name)
+                .map(|arg| arg.is_exclusive_set())
+                .unwrap_or_default()
+        });
+        debug!(
+            "Validator::validate_required: is_exclusive_present={}",
+            is_exclusive_present
+        );
+
         for arg_or_group in self.required.iter().filter(|r| !matcher.contains(r)) {
             debug!("Validator::validate_required:iter:aog={:?}", arg_or_group);
             if let Some(arg) = self.cmd.find(arg_or_group) {
                 debug!("Validator::validate_required:iter: This is an arg");
-                if !self.is_missing_required_ok(arg, matcher, conflicts) {
+                if !is_exclusive_present && !self.is_missing_required_ok(arg, matcher, conflicts) {
                     return self.missing_required_error(matcher, vec![]);
                 }
             } else if let Some(group) = self.cmd.find_group(arg_or_group) {


Test patch:
diff --git a/tests/builder/conflicts.rs b/tests/builder/conflicts.rs
index f1fb64f1..bc244ab1 100644
--- a/tests/builder/conflicts.rs
+++ b/tests/builder/conflicts.rs
@@ -490,3 +490,20 @@ fn group_conflicts_with_default_arg() {
     assert_eq!(m.value_of("opt"), Some("default"));
     assert!(m.is_present("flag"));
 }
+
+#[test]
+fn exclusive_with_required() {
+    let cmd = Command::new("bug")
+        .arg(Arg::new("test").long("test").exclusive(true))
+        .arg(Arg::new("input").takes_value(true).required(true));
+
+    cmd.clone()
+        .try_get_matches_from(["bug", "--test", "required"])
+        .unwrap_err();
+
+    cmd.clone()
+        .try_get_matches_from(["bug", "required"])
+        .unwrap();
+
+    cmd.clone().try_get_matches_from(["bug", "--test"]).unwrap();
+}