diff --git a/argv/src/spec.rs b/argv/src/spec.rs index 04e5d34a1..9f3385188 100644 --- a/argv/src/spec.rs +++ b/argv/src/spec.rs @@ -3036,7 +3036,7 @@ 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. +/// An enum whose variants are one command's mutually exclusive 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 @@ -3050,7 +3050,7 @@ pub fn choice_matches(choices: &[&str], value: &str, ignore_case: bool) -> bool 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. + /// One flag 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. @@ -3095,6 +3095,15 @@ pub trait ArgGroup: Sized { /// The variant that was selected, or `None` when no member was given. fn build(partial: &Self::Partial) -> Option; + /// Build the selected variant, reporting a payload that could not be converted. + /// + /// The default preserves hand-written implementations whose members are all switches. + /// Derived groups with value-carrying variants override this and keep their raw word in + /// [`Self::Partial`] until this step, just as an ordinary command field does. + fn try_build(partial: &Self::Partial) -> Result, crate::Error<'static, 'static>> { + Ok(Self::build(partial)) + } + /// Find a member by any selector it accepts. /// /// Parents use this for `requires` / `conflicts` / conditional defaults that name a diff --git a/conformance/tests/arg_group.rs b/conformance/tests/arg_group.rs index 0f153b9f7..270b3f5f3 100644 --- a/conformance/tests/arg_group.rs +++ b/conformance/tests/arg_group.rs @@ -10,7 +10,7 @@ use std::ffi::OsStr; use usage_argv::{help, Error}; -use usage_derive::{ArgGroup, Args, Cli, Subcommands}; +use usage_derive::{ArgGroup, Args, Cli, Subcommands, ValueEnum}; fn argv(tokens: [&str; N]) -> [&OsStr; N] { tokens.map(OsStr::new) @@ -38,6 +38,105 @@ enum Source { Clipboard, } +#[derive(Debug, PartialEq, ValueEnum)] +#[usage(ignore_case)] +enum MigrationSource { + Prettier, + Biome, +} + +/// What the formatter should do. +#[derive(ArgGroup, Debug, PartialEq)] +enum Mode { + /// Write files in place. + Write, + /// Check files without changing them. + Check, + /// Migrate a configuration. + #[usage(value_name = "SOURCE", value_enum)] + Migrate(MigrationSource), + /// Read source from standard input as this path. + StdinFilepath(std::path::PathBuf), +} + +#[derive(Cli)] +#[usage(bin = "valued-group")] +struct ValuedGroup { + #[usage(arg_group)] + mode: Option, + #[usage(long, required_if_eq("--migrate", "biome"))] + confirm: bool, +} + +#[test] +fn a_group_member_can_carry_a_typed_value() { + let a = argv(["--migrate", "BIOME", "--confirm"]); + assert_eq!( + ValuedGroup::parse_from(&a) + .expect("value-enum payload") + .mode, + Some(Mode::Migrate(MigrationSource::Biome)) + ); + + let a = argv(["--stdin-filepath", "src/input.ts"]); + assert_eq!( + ValuedGroup::parse_from(&a).expect("path payload").mode, + Some(Mode::StdinFilepath("src/input.ts".into())) + ); + + let a = argv(["--migrate", "unknown"]); + assert!(matches!( + ValuedGroup::parse_from(&a), + Err(Error::InvalidValue(error)) if error.name == "migrate" + )); +} + +#[test] +fn case_insensitive_group_values_match_relationships_the_same_way_they_parse() { + let a = argv(["--migrate", "BIOME"]); + assert!(ValuedGroup::parse_from(&a).is_err()); + + let a = argv(["--migrate", "BIOME", "--confirm"]); + assert!(ValuedGroup::parse_from(&a).is_ok()); +} + +#[test] +fn a_value_carrying_group_member_reaches_help_and_the_spec() { + let help = + help::render(ValuedGroup::spec(), ValuedGroup::spec().root.cmd, false).expect("a page"); + assert!(help.contains("--migrate "), "{help}"); + assert!(help.contains("[prettier, biome]"), "{help}"); + assert!(help.contains("--stdin-filepath "), "{help}"); + + let kdl = ValuedGroup::to_kdl(); + assert!( + kdl.contains("flag --migrate") + && kdl.contains("arg ") + && kdl.contains("choices ignore_case=#true") + && kdl.contains("choice prettier") + && kdl.contains("choice biome"), + "{kdl}" + ); + assert!( + kdl.contains("group mode --write --check --migrate --stdin-filepath"), + "{kdl}" + ); +} + +#[test] +fn update_reports_an_invalid_group_payload_without_changing_the_value() { + let mut parsed = ValuedGroup { + mode: Some(Mode::Write), + confirm: false, + }; + let a = argv(["--migrate", "unknown"]); + assert!(matches!( + parsed.try_update_from(&a), + Err(Error::InvalidValue(error)) if error.name == "migrate" + )); + assert_eq!(parsed.mode, Some(Mode::Write)); +} + /// A CLI whose format is optional and whose source is not. #[derive(Cli)] #[usage(bin = "grp")] diff --git a/derive/src/codegen.rs b/derive/src/codegen.rs index 187a90c2b..3721f51e9 100644 --- a/derive/src/codegen.rs +++ b/derive/src/codegen.rs @@ -4657,10 +4657,10 @@ fn field_value(field: &Field, omitter: Option<&TokenStream>) -> TokenStream { if let Kind::ArgGroup { ty, optional } = &field.kind { let group = quote!(<#ty as usage_argv::spec::ArgGroup>); return if *optional { - quote!(#group::build(&partial.#ident)) + quote!(#group::try_build(&partial.#ident)?) } else { quote! { - match #group::build(&partial.#ident) { + match #group::try_build(&partial.#ident)? { ::std::option::Option::Some(__usage_member) => __usage_member, ::std::option::Option::None => { return ::std::result::Result::Err( @@ -4965,7 +4965,7 @@ fn merge_fn(cli: &Cli) -> TokenStream { }; Some(quote! { if let ::std::option::Option::Some(__usage_member) = - #group::build(&partial.#field_ident) + #group::try_build(&partial.#field_ident)? { __usage_standing.#field_ident = #selected; } @@ -9718,6 +9718,11 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let cfg = &member.cfg_attrs; let long = &member.name; let shorts: Vec = member.short.map(|short| short as u8).into_iter().collect(); + let shape = if member.value_ty.is_some() { + quote!(usage_argv::Flag::VALUE) + } else { + quote!(usage_argv::Flag::BOOL) + }; quote! { #(#cfg)* pub static #table: usage_argv::Flag = usage_argv::Flag { @@ -9725,7 +9730,7 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { name: #long, longs: &[#long], shorts: &[#(#shorts),*], - ..usage_argv::Flag::BOOL + ..#shape }; } }); @@ -9741,6 +9746,24 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let help = option_str(member.help.as_deref()); let long_help = option_str(member.long_help.as_deref()); let hide = member.hide; + let value_name = option_str(member.value_name.as_deref()); + let (accepted_choices, choices, choice_aliases, choice_details, ignore_case) = + match (&member.value_ty, member.value_enum) { + (Some(ty), true) => ( + quote!(<#ty as usage_argv::spec::ValueEnum>::ACCEPTED_CHOICES), + quote!(<#ty as usage_argv::spec::ValueEnum>::CHOICES), + quote!(<#ty as usage_argv::spec::ValueEnum>::ALIASES), + quote!(<#ty as usage_argv::spec::ValueEnum>::DETAILS), + quote!(<#ty as usage_argv::spec::ValueEnum>::IGNORE_CASE), + ), + _ => ( + quote!(&[]), + quote!(&[]), + quote!(&[]), + quote!(&[]), + quote!(false), + ), + }; quote! { #(#cfg)* usage_argv::spec::FlagMeta { @@ -9748,6 +9771,12 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { help: #help, long_help: #long_help, hide: #hide, + value_name: #value_name, + accepted_choices: #accepted_choices, + choices: #choices, + choice_aliases: #choice_aliases, + choice_details: #choice_details, + ignore_case: #ignore_case, ..usage_argv::spec::FlagMeta::EMPTY } } @@ -9761,17 +9790,30 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let partial_fields = group.variants.iter().enumerate().map(|(i, member)| { let given = format_ident!("given_{i}"); let cfg = &member.cfg_attrs; - quote!(#(#cfg)* pub #given: bool,) + if member.value_ty.is_some() { + quote!(#(#cfg)* pub #given: ::std::option::Option<::std::vec::Vec>,) + } else { + quote!(#(#cfg)* pub #given: bool,) + } }); let apply_arms = group.variants.iter().enumerate().map(|(i, member)| { let table = format_ident!("FLAG_{i}"); let key = key_ident("FLAG", Some(i)); let given = format_ident!("given_{i}"); let cfg = &member.cfg_attrs; + let assign = if member.value_ty.is_some() { + quote! { + if let ::std::option::Option::Some(value) = value { + partial.#given = ::std::option::Option::Some(value.to_vec()); + } + } + } else { + quote!(partial.#given = true;) + }; quote! { #(#cfg)* #key if ::core::ptr::eq(*flag, &#table) => { - partial.#given = true; + #assign true } } @@ -9780,9 +9822,14 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let given = format_ident!("given_{i}"); let cfg = &member.cfg_attrs; let name = &member.name; + let is_given = if member.value_ty.is_some() { + quote!(partial.#given.is_some()) + } else { + quote!(partial.#given) + }; quote! { #(#cfg)* - if partial.#given { + if #is_given { return ::std::option::Option::Some(#name); } } @@ -9793,9 +9840,14 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let given = format_ident!("given_{i}"); let cfg = &member.cfg_attrs; let name = &member.name; + let is_given = if member.value_ty.is_some() { + quote!(partial.#given.is_some()) + } else { + quote!(partial.#given) + }; quote! { #(#cfg)* - if partial.#given { + if #is_given { if let ::std::option::Option::Some(__usage_earlier) = __usage_first { return ::std::option::Option::Some((__usage_earlier, #name)); } @@ -9807,10 +9859,83 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let given = format_ident!("given_{i}"); let cfg = &member.cfg_attrs; let variant = &member.ident; + let Some(ty) = &member.value_ty else { + return quote! { + #(#cfg)* + if partial.#given { + return ::std::result::Result::Ok( + ::std::option::Option::Some(Self::#variant), + ); + } + }; + }; + let name = &member.name; + let rendered = rendered_path(ty); + let converted = match rendered.as_str() { + "PathBuf" | "std::path::PathBuf" | "::std::path::PathBuf" => quote! { + match usage_argv::os_string_from_bytes(__usage_value) { + ::std::result::Result::Ok(value) => ::std::path::PathBuf::from(value), + ::std::result::Result::Err(bytes) => { + return ::std::result::Result::Err( + usage_argv::invalid_os_value(#name, bytes), + ); + } + } + }, + "OsString" | "std::ffi::OsString" | "::std::ffi::OsString" => quote! { + match usage_argv::os_string_from_bytes(__usage_value) { + ::std::result::Result::Ok(value) => value, + ::std::result::Result::Err(bytes) => { + return ::std::result::Result::Err( + usage_argv::invalid_os_value(#name, bytes), + ); + } + } + }, + _ if member.value_enum => quote! {{ + let __usage_text = match ::std::string::String::from_utf8(__usage_value) { + ::std::result::Result::Ok(text) => text, + ::std::result::Result::Err(bad) => { + return ::std::result::Result::Err( + usage_argv::invalid_utf8_value(#name, bad), + ); + } + }; + match <#ty as usage_argv::spec::ValueEnum>::from_choice(&__usage_text) { + ::std::option::Option::Some(value) => value, + ::std::option::Option::None => { + return ::std::result::Result::Err( + usage_argv::invalid_choice_value(#name, __usage_text), + ); + } + } + }}, + _ => quote! {{ + let __usage_text = match ::std::string::String::from_utf8(__usage_value) { + ::std::result::Result::Ok(text) => text, + ::std::result::Result::Err(bad) => { + return ::std::result::Result::Err( + usage_argv::invalid_utf8_value(#name, bad), + ); + } + }; + match ::std::str::FromStr::from_str(&__usage_text) { + ::std::result::Result::Ok(value) => value, + ::std::result::Result::Err(reason) => { + return ::std::result::Result::Err( + usage_argv::invalid_parsed_value(#name, __usage_text, &reason), + ); + } + } + }}, + }; quote! { #(#cfg)* - if partial.#given { - return ::std::option::Option::Some(Self::#variant); + if let ::std::option::Option::Some(__usage_value) = partial.#given.clone() { + let __usage_value: #ty = #converted; + return ::std::result::Result::Ok( + ::std::option::Option::Some(Self::#variant(__usage_value)), + ); } } }); @@ -9827,13 +9952,18 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let cfg = &member.cfg_attrs; let name = &member.name; let selectors = member_selectors(member); + let is_given = if member.value_ty.is_some() { + quote!(partial.#given.is_some()) + } else { + quote!(partial.#given) + }; quote! { #(#cfg)* #(#selectors)|* => { ::std::option::Option::Some(usage_argv::spec::ArgumentState { name: #name, - given: partial.#given, - satisfied: partial.#given, + given: #is_given, + satisfied: #is_given, }) } } @@ -9843,10 +9973,15 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let name = &member.name; let variant = &member.ident; let selectors = member_selectors(member); + let standing = if member.value_ty.is_some() { + quote!(::core::matches!(standing, Self::#variant(_))) + } else { + quote!(::core::matches!(standing, Self::#variant)) + }; quote! { #(#cfg)* #(#selectors)|* => { - let __usage_given = ::core::matches!(standing, Self::#variant); + let __usage_given = #standing; ::std::option::Option::Some(usage_argv::spec::ArgumentState { name: #name, given: __usage_given, @@ -9859,12 +9994,31 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let given = format_ident!("given_{i}"); let cfg = &member.cfg_attrs; let selectors = member_selectors(member); - // Members are SetTrue switches: presence is the value. Wrap like an ordinary - // bool flag's `#given && …` so a missing member does not "match" `"false"`. + let matches = if member.value_enum { + let ty = member + .value_ty + .as_ref() + .expect("value_enum members were checked to carry a value"); + quote! { + partial.#given.as_deref().is_some_and(|given| { + if <#ty as usage_argv::spec::ValueEnum>::IGNORE_CASE { + given.eq_ignore_ascii_case(value) + } else { + given == value + } + }) + } + } else if member.value_ty.is_some() { + quote!(partial.#given.as_deref().is_some_and(|given| given == value)) + } else { + // Switch presence is the value. Wrap like an ordinary bool flag so a missing + // member does not "match" `"false"`. + quote!(partial.#given && value == b"true") + }; quote! { #(#cfg)* #(#selectors)|* => { - return ::std::option::Option::Some(partial.#given && value == b"true"); + return ::std::option::Option::Some(#matches); } } }); @@ -9872,12 +10026,22 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { let cfg = &member.cfg_attrs; let variant = &member.ident; let selectors = member_selectors(member); + let Some(_) = &member.value_ty else { + return quote! { + #(#cfg)* + #(#selectors)|* => { + return ::std::option::Option::Some( + ::core::matches!(standing, Self::#variant) && value == b"true", + ); + } + }; + }; quote! { #(#cfg)* #(#selectors)|* => { - return ::std::option::Option::Some( - ::core::matches!(standing, Self::#variant) && value == b"true", - ); + // `FromStr` has no inverse. An update can retain the selected payload, but + // cannot reconstruct the bytes needed for a value-equality relationship. + return ::std::option::Option::None; } } }); @@ -9889,10 +10053,15 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { // selector is known to this type, so a parent that short-circuits on the // first true does not fall through to "unresolved" when the member was // simply not given. Clearing is what happens when it *was* given. + let clear = if member.value_ty.is_some() { + quote!(partial.#given = ::std::option::Option::None;) + } else { + quote!(partial.#given = false;) + }; quote! { #(#cfg)* #(#selectors)|* => { - partial.#given = false; + #clear return true; } } @@ -9946,7 +10115,7 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { event: &usage_argv::Event<'_, '_, '_>, ) -> bool { match event { - usage_argv::Event::Flag { flag, .. } => match flag.key { + usage_argv::Event::Flag { flag, value, .. } => match flag.key { #(#apply_arms)* // Another declaration's flag, left for whoever owns it. _ => false, @@ -9973,8 +10142,17 @@ pub fn emit_arg_group(group: &ArgGroup) -> TokenStream { } fn build(partial: &Self::Partial) -> ::std::option::Option { + Self::try_build(partial).ok().flatten() + } + + fn try_build( + partial: &Self::Partial, + ) -> ::std::result::Result< + ::std::option::Option, + usage_argv::Error<'static, 'static>, + > { #(#build_arms)* - ::std::option::Option::None + ::std::result::Result::Ok(::std::option::Option::None) } fn argument_state( diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 4fbd871a4..a5a95c8ba 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -577,11 +577,12 @@ pub fn derive_value_enum(input: TokenStream) -> TokenStream { /// Nothing new reaches the spec: the enum lowers to the `group` node and the flags it names, /// so `--json --yaml` is the same [`Error::ConflictingFlags`](usage_argv::Error::ConflictingFlags) /// a hand-written group produces, and a missing member of a required one is the same -/// [`Error::MissingGroup`](usage_argv::Error::MissingGroup). A member taking a value stays a -/// hand-written `conflicts` set, where the values have somewhere to land. +/// [`Error::MissingGroup`](usage_argv::Error::MissingGroup). A tuple variant with one field is a +/// value-taking member such as `Migrate(Source)`; `value_name` and `value_enum` describe that +/// payload exactly as they do on an ordinary flag. /// /// A variant's doc comment becomes its help. `help = "..."`, `long_help = "..."`, `hide`, and -/// `short = 'x'` are the rest of what a switch has; `cfg` and `cfg_attr` are copied to the +/// `short = 'x'` are the rest of what a member has; `cfg` and `cfg_attr` are copied to the /// variant's entries in the static tables, as [`ValueEnum`] copies them. #[proc_macro_derive(ArgGroup, attributes(usage, command, arg, value, group))] pub fn derive_arg_group(input: TokenStream) -> TokenStream { diff --git a/derive/src/model.rs b/derive/src/model.rs index 9a967b8d8..227ee57c2 100644 --- a/derive/src/model.rs +++ b/derive/src/model.rs @@ -5820,6 +5820,12 @@ pub struct ArgGroupMember { pub help: Option, pub long_help: Option, pub hide: bool, + /// A value carried by this member, for variants such as `Migrate(Source)`. + pub value_ty: Option, + /// The placeholder shown for [`Self::value_ty`]. + pub value_name: Option, + /// Bind the payload through its `ValueEnum` declaration rather than `FromStr`. + pub value_enum: bool, pub cfg_attrs: Vec, } @@ -5868,14 +5874,19 @@ impl ArgGroup { let mut variants: Vec = Vec::new(); for variant in &data.variants { - if !matches!(variant.fields, Fields::Unit) { - return Err(syn::Error::new_spanned( - &variant.fields, - "a group member is a switch, so each variant is a bare name: a member \ - taking a value stays a hand-written `conflicts` set, where the values \ - have somewhere to land", - )); - } + let value_ty = match &variant.fields { + Fields::Unit => None, + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + Some(fields.unnamed[0].ty.clone()) + } + _ => { + return Err(syn::Error::new_spanned( + &variant.fields, + "an argument-group member is either a switch or one value-taking flag: \ + use a unit variant or a tuple variant with exactly one field", + )); + } + }; let cfg_attrs = cfg_gate_attrs(&variant.attrs)?; let (doc_help, doc_long_help) = doc_comment(&variant.attrs, false)?; let mut member = ArgGroupMember { @@ -5885,6 +5896,9 @@ impl ArgGroup { help: doc_help, long_help: doc_long_help, hide: false, + value_ty, + value_name: None, + value_enum: false, cfg_attrs, }; for attr in attrs(&variant.attrs) { @@ -5899,6 +5913,16 @@ impl ArgGroup { "help" => member.help = Some(string_value(&meta)?), "long_help" => member.long_help = Some(string_value(&meta)?), "hide" => member.hide = flag_value(&meta)?, + "value_name" => member.value_name = Some(string_value(&meta)?), + "value_enum" => { + if !matches!(meta, Meta::Path(_)) { + return Err(syn::Error::new_spanned( + path, + "`value_enum` takes no value", + )); + } + member.value_enum = true; + } "default" | "default_value" | "default_value_t" => { return Err(syn::Error::new_spanned( path, @@ -5912,8 +5936,8 @@ impl ArgGroup { path, format!( "unknown option `{other}` on a group member; a variant \ - takes `long`, `name`, `short`, `help`, `long_help`, or \ - `hide` here" + takes `long`, `name`, `short`, `help`, `long_help`, \ + `hide`, `value_name`, or `value_enum` here" ), )); } @@ -5926,6 +5950,15 @@ impl ArgGroup { "a member with no long form would answer to nothing", )); } + if member.value_ty.is_none() && (member.value_name.is_some() || member.value_enum) { + return Err(syn::Error::new_spanned( + &variant.ident, + "`value_name` and `value_enum` need a tuple variant that carries one value", + )); + } + if member.value_ty.is_some() && member.value_name.is_none() { + member.value_name = Some(shout(&member.name)); + } if let Some(short) = member.short.filter(|short| !short.is_ascii()) { return Err(syn::Error::new_spanned( &variant.ident, @@ -7519,11 +7552,23 @@ mod tests { } #[test] - fn an_arg_group_member_is_a_switch_and_holds_nothing() { - let err = group_rejection("enum Format { Json, Wrapped(String) }"); - assert!(err.contains("bare name"), "unhelpful: {err}"); - // And the message says where a valued member belongs, which is the useful half. - assert!(err.contains("conflicts"), "unhelpful: {err}"); + fn an_arg_group_member_may_hold_one_value() { + let group = arg_group( + r#" + enum Mode { + Write, + #[usage(value_name = "SOURCE", value_enum)] + Migrate(Source), + } + "#, + ) + .expect("one tuple field is a value-taking flag"); + assert!(group.variants[0].value_ty.is_none()); + assert_eq!(group.variants[1].value_name.as_deref(), Some("SOURCE")); + assert!(group.variants[1].value_enum); + + let err = group_rejection("enum Format { Json, Pair(String, String) }"); + assert!(err.contains("exactly one field"), "unhelpful: {err}"); } #[test] diff --git a/docs/rust/validation.md b/docs/rust/validation.md index a81189788..bbb705d62 100644 --- a/docs/rust/validation.md +++ b/docs/rust/validation.md @@ -93,9 +93,9 @@ a positional — are compile errors. ## A group as an enum -When every member is a valueless flag, the group can be the type instead: an enum deriving -`ArgGroup`, whose variants are the flags. The code that reads it then matches on a variant -rather than working out which of several `bool`s is set. +The group can be the type instead: an enum deriving `ArgGroup`, whose variants are the flags. +The code that reads it then matches on a variant rather than working out which of several +fields is set. ```rust /// How to print the result @@ -119,10 +119,24 @@ struct Fmt { } ``` -The variants are bare: each is one switch, named by its own name in kebab-case unless `long` -or `name` says otherwise, with `short`, `hide`, `help` and `long_help` as the rest of what a -switch has. A doc comment is the help. Without `#[usage(name = "…")]` the group is named after -the type. +Unit variants are switches. A tuple variant with one field is a value-taking flag: + +```rust +#[derive(ArgGroup)] +enum Mode { + Write, + Check, + #[usage(value_name = "SOURCE", value_enum)] + Migrate(MigrationSource), + #[usage(value_name = "PATH")] + StdinFilepath(std::path::PathBuf), +} +``` + +Each member is named by its variant in kebab-case unless `long` or `name` says otherwise. +`short`, `hide`, `help` and `long_help` apply to both forms; `value_name` and `value_enum` +describe a tuple variant's payload. A doc comment is the help. Without +`#[usage(name = "…")]` the group is named after the type. The field's type says whether the group is required, exactly as it does everywhere else: `Option` is a group that may be left alone, and a bare `Format` is one that has to be @@ -135,9 +149,9 @@ Nothing new reaches the spec. The enum lowers to the same `group` node given is the same `MissingGroup`, and help, docs and completions list the member flags without knowing an enum was involved. -A member that takes a value stays a hand-written `conflicts` set or a -[`ValueEnum`](/rust/subcommands#value-enums) on one flag, where the values have somewhere to -land. +A payload converts through `FromStr`, preserving non-UTF-8 bytes for `PathBuf` and `OsString`. +With `value_enum`, its choices, aliases, help and case policy reach validation, help, generated +specs and completions from the same enum declaration. ## Exclusive flags