Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1588,12 +1588,12 @@ above are where it lands.
clap#5925's fallback across several env names preserves declaration
order. This is parser behavior rather than command/flag presentation, so
it remains separate from the deprecation metadata above.
- [ ] **A group as an enum in the derive** (clap#2621, 102 votes — tied for
- [x] **A group as an enum in the derive** (clap#2621, 102 votes — tied for
clap's most-requested) — mutually exclusive flags declared as enum
variants, lowering to the `group`/`conflicts` vocabulary the spec already
has. Derive ergonomics rather than new spec surface, and clap has sat on
it since 2021. **Decided (2026-08-21): `#[derive(usage::ArgGroup)]` on the
enum**, held by an `Option<Mode>` field for an optional group and a bare
it since 2021. **`#[derive(usage::ArgGroup)]` on the enum**, held by an
`Option<Mode>` field for an optional group and a bare
`Mode` field for a required one. A new derive rather than an overloaded
`ValueEnum`, because the same enum would otherwise lower two entirely
different ways depending on the field holding it; and an enum rather than a
Expand All @@ -1608,7 +1608,10 @@ above are where it lands.
generators to describe. **Two members on one command line is an error**,
matching clap's `ArgGroup` and the `conflicts` vocabulary this lowers to;
exclusivity is the point, so a typo is reported rather than silently
resolved to whichever came last.
resolved to whichever came last. Nothing new reaches the spec: the enum
emits the `group` node and the switches it names, so KDL, usage-lib, help,
docs and completions all read an ordinary group, and one field holding the
enum is what a command declares.
- [x] **Recursive help** (clap#4813) — `ArgAction::HelpAll` renders long help
for the selected command and every visible descendant in one depth-first
output. Typed Rust, portable KDL, usage-lib, and generated Go retain the
Expand Down
91 changes: 91 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ pub struct Spec<'a> {
/// An exact usage synopsis, including the `Usage:` prefix, when the generated
/// shape needs alternatives that cannot be inferred from one command grammar.
pub usage: Option<&'a str>,
/// How every page in this CLI is laid out, as named sections.
///
/// A template names the six pre-rendered sections — `{{about}}`, `{{usage}}`,
/// `{{commands}}`, `{{args}}`, `{{flags}}`, `{{after_help}}` — and may reorder,
/// omit or wrap them. See [`crate::help::SECTIONS`] for what each one covers and
/// [`crate::help::unsupported_section`] for the rule an author's template is held to.
pub help_template: Option<&'a str>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// Which command the root falls back to when a word matches no subcommand.
/// mise uses this so `mise foo` completes as `mise run foo`.
pub default_subcommand: Option<&'a str>,
Expand Down Expand Up @@ -569,6 +576,7 @@ impl<'a> SpecView<'a> {
about: self.base.about,
long_about: self.base.long_about,
usage: self.base.usage,
help_template: self.base.help_template,
default_subcommand: self.base.default_subcommand,
multicall: self.base.multicall,
views: self.base.views,
Expand Down Expand Up @@ -600,6 +608,7 @@ impl Spec<'_> {
about: None,
long_about: None,
usage: None,
help_template: None,
default_subcommand: None,
multicall: false,
views: &[],
Expand Down Expand Up @@ -1344,6 +1353,9 @@ impl Spec<'_> {
if let Some(usage) = self.usage {
prop(out, "usage", usage)?;
}
if let Some(template) = self.help_template {
prop(out, "help_template", template)?;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Written only when it is not the default, so an ordinary spec stays quiet
// about it.
if self.root.cmd.unknown_flags == Some(UnknownFlags::Error) {
Expand Down Expand Up @@ -2883,6 +2895,85 @@ pub fn choice_matches(choices: &[&str], value: &str, ignore_case: bool) -> bool
.any(|choice| *choice == value || ignore_case && choice.eq_ignore_ascii_case(value))
}

/// An enum whose variants are one command's mutually exclusive switch flags.
///
/// What a CLI spells `--json` or `--yaml` and holds as a `Mode`, rather than as one `bool` per
/// member plus a `match` over which of them is set. Nothing new reaches the spec: the enum
/// lowers to a [`GroupMeta`] and the switches it names, so help, completions, and the
/// reference implementation read the declaration every hand-written group does.
///
/// A field holding one says `#[usage(arg_group)]`. `Option<Mode>` is a group that may be left
/// alone and a bare `Mode` is one that has to be given, which is how the rest of the derive
/// reads required-ness from a type. There is no default variant, so required-ness has exactly
/// one spelling.
pub trait ArgGroup: Sized {
/// What the group is called, in the emitted spec and in a failed check.
const NAME: &'static str;
/// One switch per variant, to splice into the holding command's parse table.
///
/// A `const` for the same reason [`CommandArgs::COMMAND`] is: the tables stay `static`
/// all the way down, so nothing is built at run time to start a parse.
const FLAGS: &'static [&'static Flag<'static>];
/// Metadata for [`FLAGS`](Self::FLAGS), in the same order.
const FLAG_METAS: &'static [FlagMeta<'static>];
/// The selectors naming [`FLAGS`](Self::FLAGS), for [`GroupMeta::members`].
const MEMBERS: &'static [&'static str];

/// Which members have been given so far. Partly-filled by construction, since a parse
/// can stop early.
type Partial: Default;

/// A fresh partial, with no member given.
///
/// Nothing to prepare, unlike [`CommandArgs::start`]: a group has no default variant, so
/// there is no value that has to be in place before parsing begins.
fn start() -> Self::Partial {
Self::Partial::default()
}

/// Take one event, and say whether it named one of this group's flags.
///
/// Keys are unique across a CLI, so an event that is not this group's is left for
/// whoever owns it.
fn apply(partial: &mut Self::Partial, event: &crate::Event<'_, '_, '_>) -> bool;

/// The first member this command line gave, if any.
///
/// Named rather than a bare `bool`, so a parent enforcing `exclusive` across the group
/// can say which flag it collided with — exactly as [`CommandArgs::any_given`] does.
fn any_given(partial: &Self::Partial) -> Option<&'static str>;

/// The first two members given together, in declaration order.
///
/// Exclusivity is the point of a group, so a second member is reported rather than
/// silently resolved to whichever came last, and the pair is what the user has to choose
/// between. Answered here rather than while binding for the same reason every other
/// relationship is: the second member may still be ahead of the first.
fn conflict(partial: &Self::Partial) -> Option<(&'static str, &'static str)>;

/// The variant that was selected, or `None` when no member was given.
fn build(partial: &Self::Partial) -> Option<Self>;

/// Find a member by any selector it accepts.
///
/// Parents use this for `requires` / `conflicts` / conditional defaults that name a
/// group member from beside the field — the same bridge [`CommandArgs::argument_state`]
/// is for flattened argument groups.
fn argument_state(partial: &Self::Partial, selector: &str) -> Option<ArgumentState>;

/// Whether a selected member is present as the given boolean value.
///
/// Members are switches, so only `"true"` / `"false"` are meaningful; anything else
/// reports not matching rather than inventing a value.
fn argument_matches(partial: &Self::Partial, selector: &str, value: &[u8]) -> Option<bool>;

/// Clear the member named by `selector` after an overriding token wins.
fn displace(partial: &mut Self::Partial, selector: &str) -> bool;

/// Whether this event binds the member named by `selector`.
fn event_matches(event: &crate::Event<'_, '_, '_>, selector: &str) -> bool;
}

/// One value a flag was given, in a vocabulary this crate can hold.
///
/// A settings layer is `usage-config`'s idea, and this crate does not know that crate exists —
Expand Down
Loading
Loading