#6520·clap

exclusive(true) waives a required arg but not a required group, making the exclusive arg unusable

Author: datrixlabCreated Sep 10, 2026Updated Sep 11, 2026
LabelsC-bugE-mediumA-validators

Please complete the following tasks

Rust Version

rustc 1.97.1 (8bab26f4f 2026-07-14)

Clap Version

4.6.6 (main, af30442)

Minimal reproducible code

rust
use clap::{Arg, ArgAction, ArgGroup, Command};

fn main() {
    // A required ARG next to an exclusive flag: the exclusive flag works.
    let arg_case = Command::new("prog")
        .arg(Arg::new("excl").long("excl").exclusive(true).action(ArgAction::SetTrue))
        .arg(Arg::new("a").long("a").required(true).action(ArgAction::SetTrue))
        .try_get_matches_from(vec!["prog", "--excl"]);
    println!("ARG-CASE   -> {:?}", arg_case.map(|_| "Ok").map_err(|e| e.kind()));

    // A required GROUP instead of a required arg: the exclusive flag stops working.
    let group_case = Command::new("prog")
        .arg(Arg::new("excl").long("excl").exclusive(true).action(ArgAction::SetTrue))
        .arg(Arg::new("a").long("a").action(ArgAction::SetTrue))
        .arg(Arg::new("b").long("b").action(ArgAction::SetTrue))
        .group(ArgGroup::new("ab").args(["a", "b"]).required(true))
        .try_get_matches_from(vec!["prog", "--excl"]);
    println!("GROUP-CASE -> {:?}", group_case.map(|_| "Ok").map_err(|e| e.kind()));

    // Following the usage line clap prints for the group case.
    let obey = Command::new("prog")
        .arg(Arg::new("excl").long("excl").exclusive(true).action(ArgAction::SetTrue))
        .arg(Arg::new("a").long("a").action(ArgAction::SetTrue))
        .arg(Arg::new("b").long("b").action(ArgAction::SetTrue))
        .group(ArgGroup::new("ab").args(["a", "b"]).required(true))
        .try_get_matches_from(vec!["prog", "--excl", "--a"]);
    println!("OBEY-USAGE -> {:?}", obey.map(|_| "Ok").map_err(|e| e.kind()));
}

Steps to reproduce the bug with the above code

cargo run

Actual Behaviour

ARG-CASE   -> Ok("Ok")
GROUP-CASE -> Err(MissingRequiredArgument)
OBEY-USAGE -> Err(ArgumentConflict)

The error printed for the group case is:

error: the following required arguments were not provided:
Usage: prog --excl <--a|--b>

That usage line tells the user to pass --excl together with one of the group's members, which is exactly what exclusive(true) forbids. So no invocation using --excl succeeds: alone it is MissingRequiredArgument, and with --a it is ArgumentConflict. The exclusive flag cannot be used at all.

Expected Behaviour

GROUP-CASE -> Ok, matching ARG-CASE. An exclusive argument that is present should waive a required group the same way it waives a required arg.

Additional Context

Validator::validate_required computes is_exclusive_present once and then consults it in two of the three places that can report something missing:

  • clap_builder/src/parser/validator.rs:247 — the arg branch: if !is_exclusive_present && !self.is_missing_required_ok(arg, conflicts)
  • clap_builder/src/parser/validator.rs:313 — the conditionally-required branch: if !is_exclusive_present && required
  • the group branch in between (roughly :255-:270) does not consult it at all

grep -rn is_exclusive_present clap_builder/src/ returns only the definition and those two uses, so the group branch is the odd one out rather than a deliberate exception.

On whether waiving is the intended behaviour at all: #5957 raised the arg case and the conclusion there was that the parser is working correctly and the confusion belonged to the derive layer — so waiving a required arg is intended, which is what makes the group branch look like an oversight.

I do not think this overlaps #6050 (draft, for #4707): that one is about requires being bypassed by mutually exclusive groups, a different direction, though it does touch the same function.

Happy to open a PR adding !is_exclusive_present && to the group branch with tests, if that is the fix you would want. I held off because #5957 carries M-breaking-change / S-waiting-on-decision and this changes when an error is reported, so it seemed worth agreeing on first.

Debug Output

Not attached — the reproduction above is a plain try_get_matches_from on main, no debug feature needed. Happy to add --features debug output if useful.