diff --git a/src/lib.rs b/src/lib.rs index 3fe952d3..2591e590 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ //! - [Subcommands](#subcommands) //! - [Optional subcommands](#optional-subcommands) //! - [External subcommands](#external-subcommands) +//! - [Flattening subcommands](#flattening-subcommands) //! - [Flattening](#flattening) //! - [Custom string parsers](#custom-string-parsers) //! @@ -280,7 +281,7 @@ //! //! - [`flatten`](#flattening): `flatten` //! -//! Usable only on field-level. +//! Usable on field-level or single-typed tuple variants. //! //! - [`subcommand`](#subcommands): `subcommand` //! @@ -926,6 +927,36 @@ //! //! [`AppSettings::AllowExternalSubcommands`]: https://docs.rs/clap/2.32.0/clap/enum.AppSettings.html#variant.AllowExternalSubcommands //! +//! ### Flattening subcommands +//! +//! It is also possible to combine multiple enums of subcommands into one. +//! All the subcommands will be on the same level. +//! +//! ``` +//! # use structopt::StructOpt; +//! # fn main() {} +//! #[derive(StructOpt)] +//! enum BaseCli { +//! Ghost10 { +//! arg1: i32, +//! } +//! } +//! +//! #[derive(StructOpt)] +//! enum Opt { +//! #[structopt(flatten)] +//! BaseCli(BaseCli), +//! Dex { +//! arg2: i32, +//! } +//! } +//! ``` +//! +//! ```shell +//! cli ghost10 42 +//! cli dex 42 +//! ``` +//! //! ## Flattening //! //! It can sometimes be useful to group related arguments in a substruct, diff --git a/structopt-derive/src/attrs.rs b/structopt-derive/src/attrs.rs index c4e91e02..e88132c3 100644 --- a/structopt-derive/src/attrs.rs +++ b/structopt-derive/src/attrs.rs @@ -24,7 +24,7 @@ pub enum Kind { Arg(Sp), Subcommand(Sp), ExternalSubcommand, - FlattenStruct, + Flatten, Skip(Option), } @@ -282,7 +282,7 @@ impl Attrs { } Flatten(ident) => { - let kind = Sp::new(Kind::FlattenStruct, ident.span()); + let kind = Sp::new(Kind::Flatten, ident.span()); self.set_kind(kind); } @@ -406,9 +406,8 @@ impl Attrs { } match &*res.kind { Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"), - Kind::FlattenStruct => abort!(res.kind.span(), "flatten is only allowed on fields"), Kind::Skip(_) => abort!(res.kind.span(), "skip is only allowed on fields"), - Kind::Arg(_) | Kind::ExternalSubcommand => res, + Kind::Arg(_) | Kind::ExternalSubcommand | Kind::Flatten => res, } } @@ -431,7 +430,7 @@ impl Attrs { res.push_attrs(&field.attrs); match &*res.kind { - Kind::FlattenStruct => { + Kind::Flatten => { if res.has_custom_parser { abort!( res.parser.span(), diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 7843c0d8..16e7ca5e 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -121,7 +121,7 @@ fn gen_augmentation( "`external_subcommand` is only allowed on enum variants" ), Kind::Subcommand(_) | Kind::Skip(_) => None, - Kind::FlattenStruct => { + Kind::Flatten => { let ty = &field.ty; Some(quote_spanned! { kind.span()=> let #app_var = <#ty as ::structopt::StructOptInternal>::augment_clap(#app_var); @@ -270,7 +270,7 @@ fn gen_constructor(fields: &Punctuated, parent_attribute: &Attrs) } } - Kind::FlattenStruct => quote_spanned! { kind.span()=> + Kind::Flatten => quote_spanned! { kind.span()=> #field_name: ::structopt::StructOpt::from_clap(matches) }, @@ -468,45 +468,67 @@ fn gen_augment_clap_enum( parent_attribute.env_casing(), ); - if let Kind::ExternalSubcommand = *attrs.kind() { - return quote_spanned! { attrs.kind().span()=> - .setting(::structopt::clap::AppSettings::AllowExternalSubcommands) - }; - } + let kind = attrs.kind(); + match &*kind { + Kind::ExternalSubcommand => { + quote_spanned! { attrs.kind().span()=> + let app = app.setting( + ::structopt::clap::AppSettings::AllowExternalSubcommands + ); + } + }, - let app_var = Ident::new("subcommand", Span::call_site()); - let arg_block = match variant.fields { - Named(ref fields) => gen_augmentation(&fields.named, &app_var, &attrs), - Unit => quote!( #app_var ), - Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { - let ty = &unnamed[0]; - quote_spanned! { ty.span()=> - { - let #app_var = <#ty as ::structopt::StructOptInternal>::augment_clap( - #app_var - ); - if <#ty as ::structopt::StructOptInternal>::is_subcommand() { - #app_var.setting( - ::structopt::clap::AppSettings::SubcommandRequiredElseHelp - ) - } else { - #app_var + Kind::Flatten => { + match variant.fields { + Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { + let ty = &unnamed[0]; + quote! { + let app = <#ty as ::structopt::StructOptInternal>::augment_clap(app); } - } + }, + _ => abort!( + variant.span(), + "`flatten` is usable only with single-typed tuple variants" + ), } - } - Unnamed(..) => abort_call_site!("{}: tuple enums are not supported", variant.ident), - }; + }, - let name = attrs.cased_name(); - let from_attrs = attrs.top_level_methods(); - let version = attrs.version(); - quote! { - .subcommand({ - let #app_var = ::structopt::clap::SubCommand::with_name(#name); - let #app_var = #arg_block; - #app_var#from_attrs#version - }) + _ => { + let app_var = Ident::new("subcommand", Span::call_site()); + let arg_block = match variant.fields { + Named(ref fields) => gen_augmentation(&fields.named, &app_var, &attrs), + Unit => quote!( #app_var ), + Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { + let ty = &unnamed[0]; + quote_spanned! { ty.span()=> + { + let #app_var = <#ty as ::structopt::StructOptInternal>::augment_clap( + #app_var + ); + if <#ty as ::structopt::StructOptInternal>::is_subcommand() { + #app_var.setting( + ::structopt::clap::AppSettings::SubcommandRequiredElseHelp + ) + } else { + #app_var + } + } + } + } + Unnamed(..) => abort!(variant.span(), "non single-typed tuple enums are not supported"), + }; + + let name = attrs.cased_name(); + let from_attrs = attrs.top_level_methods(); + let version = attrs.version(); + quote! { + let app = app.subcommand({ + let #app_var = ::structopt::clap::SubCommand::with_name(#name); + let #app_var = #arg_block; + #app_var#from_attrs#version + }); + } + }, } }); @@ -516,7 +538,9 @@ fn gen_augment_clap_enum( fn augment_clap<'a, 'b>( app: ::structopt::clap::App<'a, 'b> ) -> ::structopt::clap::App<'a, 'b> { - app #app_methods #( #subcommands )* #version + let app = app #app_methods; + #( #subcommands )*; + app #version } } } @@ -539,7 +563,7 @@ fn gen_from_subcommand( let mut ext_subcmd = None; - let match_arms: Vec<_> = variants + let (flatten_variants, variants): (Vec<_>, Vec<_>) = variants .iter() .filter_map(|variant| { let attrs = Attrs::from_struct( @@ -551,7 +575,6 @@ fn gen_from_subcommand( parent_attribute.env_casing(), ); - let sub_name = attrs.cased_name(); let variant_name = &variant.ident; if let Kind::ExternalSubcommand = *attrs.kind() { @@ -601,59 +624,94 @@ fn gen_from_subcommand( ext_subcmd = Some((span, variant_name, str_ty, values_of)); None } else { - let constructor_block = match variant.fields { - Named(ref fields) => gen_constructor(&fields.named, &attrs), - Unit => quote!(), - Unnamed(ref fields) if fields.unnamed.len() == 1 => { - let ty = &fields.unnamed[0]; - quote!( ( <#ty as ::structopt::StructOpt>::from_clap(matches) ) ) - } - Unnamed(..) => { - abort_call_site!("{}: tuple enums are not supported", variant.ident) - } - }; - - Some(quote! { - (#sub_name, Some(matches)) => - Some(#name :: #variant_name #constructor_block) - }) + Some((variant, attrs)) } }) - .collect(); + .partition(|(_, attrs)| match &*attrs.kind() { + Kind::Flatten => true, + _ => false, + }); - let wildcard = match ext_subcmd { + let external = match ext_subcmd { Some((span, var_name, str_ty, values_of)) => quote_spanned! { span=> - ("", ::std::option::Option::None) => None, - - (external, Some(matches)) => { - ::std::option::Option::Some(#name::#var_name( - ::std::iter::once(#str_ty::from(external)) - .chain( - matches.#values_of("").unwrap().map(#str_ty::from) - ) - .collect::<::std::vec::Vec<_>>() - )) - } + match other { + ("", ::std::option::Option::None) => None, + + (external, Some(matches)) => { + ::std::option::Option::Some(#name::#var_name( + ::std::iter::once(#str_ty::from(external)) + .chain( + matches.#values_of("").unwrap().map(#str_ty::from) + ) + .collect::<::std::vec::Vec<_>>() + )) + } - (external, None) => { - ::std::option::Option::Some(#name::#var_name({ - let mut v = ::std::vec::Vec::with_capacity(1); - v.push(#str_ty::from(external)); - v - })) + (external, None) => { + ::std::option::Option::Some(#name::#var_name({ + ::std::iter::once(#str_ty::from(external)) + .collect::<::std::vec::Vec<_>>() + })) + } } }, - None => quote!(_ => None), + None => quote!(None), }; + let match_arms = variants.iter().map(|(variant, attrs)| { + let sub_name = attrs.cased_name(); + let variant_name = &variant.ident; + let constructor_block = match variant.fields { + Named(ref fields) => gen_constructor(&fields.named, &attrs), + Unit => quote!(), + Unnamed(ref fields) if fields.unnamed.len() == 1 => { + let ty = &fields.unnamed[0]; + quote!( ( <#ty as ::structopt::StructOpt>::from_clap(matches) ) ) + } + Unnamed(..) => abort!( + variant.ident.span(), + "non single-typed tuple enums are not supported" + ), + }; + + quote! { + (#sub_name, Some(matches)) => { + Some(#name :: #variant_name #constructor_block) + } + } + }); + + let child_subcommands = flatten_variants.iter().map(|(variant, _attrs)| { + let variant_name = &variant.ident; + match variant.fields { + Unnamed(ref fields) if fields.unnamed.len() == 1 => { + let ty = &fields.unnamed[0]; + quote! { + if let Some(res) = + <#ty as ::structopt::StructOptInternal>::from_subcommand(other) + { + return Some(#name :: #variant_name (res)); + } + } + } + _ => abort!( + variant.span(), + "`flatten` is usable only with single-typed tuple variants" + ), + } + }); + quote! { fn from_subcommand<'a, 'b>( sub: (&'b str, Option<&'b ::structopt::clap::ArgMatches<'a>>) ) -> Option { match sub { #( #match_arms, )* - #wildcard + other => { + #( #child_subcommands )else*; + #external + } } } } diff --git a/structopt-derive/src/spanned.rs b/structopt-derive/src/spanned.rs index 8abe3052..19dbe476 100644 --- a/structopt-derive/src/spanned.rs +++ b/structopt-derive/src/spanned.rs @@ -65,12 +65,18 @@ impl<'a> From> for Sp { } } -impl> PartialEq for Sp { - fn eq(&self, other: &U) -> bool { +impl PartialEq for Sp { + fn eq(&self, other: &T) -> bool { self.val == *other } } +impl PartialEq for Sp { + fn eq(&self, other: &Sp) -> bool { + self.val == **other + } +} + impl> AsRef for Sp { fn as_ref(&self) -> &str { self.val.as_ref() diff --git a/tests/flatten.rs b/tests/flatten.rs index 4983d86f..f01e44e7 100644 --- a/tests/flatten.rs +++ b/tests/flatten.rs @@ -93,3 +93,37 @@ fn flatten_in_subcommand() { Opt::from_iter(&["test", "add", "-i", "43"]) ); } + +#[test] +fn merge_subcommands_with_flatten() { + #[derive(StructOpt, PartialEq, Debug)] + enum BaseCli { + Command1(Command1), + } + + #[derive(StructOpt, PartialEq, Debug)] + struct Command1 { + arg1: i32, + } + + #[derive(StructOpt, PartialEq, Debug)] + struct Command2 { + arg2: i32, + } + + #[derive(StructOpt, PartialEq, Debug)] + enum Opt { + #[structopt(flatten)] + BaseCli(BaseCli), + Command2(Command2), + } + + assert_eq!( + Opt::BaseCli(BaseCli::Command1(Command1 { arg1: 42 })), + Opt::from_iter(&["test", "command1", "42"]) + ); + assert_eq!( + Opt::Command2(Command2 { arg2: 43 }), + Opt::from_iter(&["test", "command2", "43"]) + ); +} diff --git a/tests/ui/struct_flatten.rs b/tests/ui/enum_flatten.rs similarity index 84% rename from tests/ui/struct_flatten.rs rename to tests/ui/enum_flatten.rs index 2b205f16..768de763 100644 --- a/tests/ui/struct_flatten.rs +++ b/tests/ui/enum_flatten.rs @@ -9,10 +9,10 @@ use structopt::StructOpt; #[derive(StructOpt, Debug)] -#[structopt(name = "basic", flatten)] -struct Opt { - #[structopt(short)] - s: String, +#[structopt(name = "basic")] +enum Opt { + #[structopt(flatten)] + Variant1, } fn main() { diff --git a/tests/ui/enum_flatten.stderr b/tests/ui/enum_flatten.stderr new file mode 100644 index 00000000..5307cee7 --- /dev/null +++ b/tests/ui/enum_flatten.stderr @@ -0,0 +1,5 @@ +error: `flatten` is usable only with single-typed tuple variants + --> $DIR/enum_flatten.rs:14:5 + | +14 | #[structopt(flatten)] + | ^ diff --git a/tests/ui/struct_flatten.stderr b/tests/ui/struct_flatten.stderr deleted file mode 100644 index 7f0fc6d6..00000000 --- a/tests/ui/struct_flatten.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: flatten is only allowed on fields - --> $DIR/struct_flatten.rs:12:29 - | -12 | #[structopt(name = "basic", flatten)] - | ^^^^^^^