diff --git a/.gitignore b/.gitignore index ea63af4a..4467abb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ target Cargo.lock *~ +expanded.rs .idea/ .vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a4066553..433a1659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Just annotate the `enum` and the setting will be propagated down ([#242](https://github.com/TeXitoi/structopt/issues/242)). * [Auto-default](https://docs.rs/structopt/0.3/structopt/#default-values). +* [External subcommands](https://docs.rs/structopt/0.3/structopt/#external-subcommands). # v0.3.7 (2019-12-28) diff --git a/src/lib.rs b/src/lib.rs index 716e0e8c..e5eff07e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,7 @@ //! - [Skipping fields](#skipping-fields) //! - [Subcommands](#subcommands) //! - [Optional subcommands](#optional-subcommands) +//! - [External subcommands](#external-subcommands) //! - [Flattening](#flattening) //! - [Custom string parsers](#custom-string-parsers) //! @@ -285,6 +286,10 @@ //! //! Usable only on field-level. //! +//! - [`external_subcommand`](#external-subcommands) +//! +//! Usable only on enum variants. +//! //! - [`env`](#environment-variable-fallback): `env [= str_literal]` //! //! Usable only on field-level. @@ -867,6 +872,60 @@ //! } //! ``` //! +//! ### External subcommands +//! +//! Sometimes you want to support not only the set of well-known subcommands +//! but you also want to allow other, user-driven subcommands. `clap` supports +//! this via [`AppSettings::AllowExternalSubcommands`]. +//! +//! `structopt` provides it's own dedicated syntax for that: +//! +//! ``` +//! # use structopt::StructOpt; +//! #[derive(Debug, PartialEq, StructOpt)] +//! struct Opt { +//! #[structopt(subcommand)] +//! sub: Subcommands, +//! } +//! +//! #[derive(Debug, PartialEq, StructOpt)] +//! enum Subcommands { +//! // normal subcommand +//! Add, +//! +//! // `external_subcommand` tells structopt to put +//! // all the extra arguments into this Vec +//! #[structopt(external_subcommand)] +//! Other(Vec), +//! } +//! +//! // normal subcommand +//! assert_eq!( +//! Opt::from_iter(&["test", "add"]), +//! Opt { +//! sub: Subcommands::Add +//! } +//! ); +//! +//! assert_eq!( +//! Opt::from_iter(&["test", "git", "status"]), +//! Opt { +//! sub: Subcommands::Other(vec!["git".into(), "status".into()]) +//! } +//! ); +//! +//! // Please note that if you'd wanted to allow "no subcommands at all" case +//! // you should have used `sub: Option` above +//! assert!(Opt::from_iter_safe(&["test"]).is_err()); +//! ``` +//! +//! In other words, you just add an extra tuple variant marked with +//! `#[structopt(subcommand)]`, and its type must be either +//! `Vec` or `Vec`. `structopt` will detect `String` in this context +//! and use appropriate `clap` API. +//! +//! [`AppSettings::AllowExternalSubcommands`]: https://docs.rs/clap/2.32.0/clap/enum.AppSettings.html#variant.AllowExternalSubcommands +//! //! ## 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 3cba66d5..15d94979 100644 --- a/structopt-derive/src/attrs.rs +++ b/structopt-derive/src/attrs.rs @@ -23,6 +23,7 @@ use syn::{ pub enum Kind { Arg(Sp), Subcommand(Sp), + ExternalSubcommand, FlattenStruct, Skip(Option), } @@ -282,6 +283,10 @@ impl Attrs { self.set_kind(kind); } + ExternalSubcommand(ident) => { + self.kind = Sp::new(Kind::ExternalSubcommand, ident.span()); + } + Flatten(ident) => { let kind = Sp::new(Kind::FlattenStruct, ident.span()); self.set_kind(kind); @@ -407,7 +412,7 @@ impl Attrs { 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(_) => res, + Kind::Arg(_) | Kind::ExternalSubcommand => res, } } @@ -444,6 +449,9 @@ impl Attrs { ); } } + + Kind::ExternalSubcommand => {} + Kind::Subcommand(_) => { if res.has_custom_parser { abort!( diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 0975569a..bf04cdec 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -23,7 +23,7 @@ mod ty; use crate::{ attrs::{Attrs, CasingStyle, Kind, Name, ParserKind}, spanned::Sp, - ty::{sub_type, Ty}, + ty::{is_simple_ty, sub_type, subty_if_name, Ty}, }; use proc_macro2::{Span, TokenStream}; @@ -116,6 +116,10 @@ fn gen_augmentation( ); let kind = attrs.kind(); match &*kind { + Kind::ExternalSubcommand => abort!( + kind.span(), + "`external_subcommand` is only allowed on enum variants" + ), Kind::Subcommand(_) | Kind::Skip(_) => None, Kind::FlattenStruct => { let ty = &field.ty; @@ -244,6 +248,11 @@ fn gen_constructor(fields: &Punctuated, parent_attribute: &Attrs) let field_name = field.ident.as_ref().unwrap(); let kind = attrs.kind(); match &*kind { + Kind::ExternalSubcommand => abort!( + kind.span(), + "`external_subcommand` is allowed only on enum variants" + ), + Kind::Subcommand(ty) => { let subcmd_type = match (**ty, sub_type(&field.ty)) { (Ty::Option, Some(sub_type)) => sub_type, @@ -457,6 +466,13 @@ fn gen_augment_clap_enum( parent_attribute.casing(), parent_attribute.env_casing(), ); + + if let Kind::ExternalSubcommand = *attrs.kind() { + return quote_spanned! { attrs.kind().span()=> + .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), @@ -520,40 +536,123 @@ fn gen_from_subcommand( ) -> TokenStream { use syn::Fields::*; - let match_arms = variants.iter().map(|variant| { - let attrs = Attrs::from_struct( - variant.span(), - &variant.attrs, - Name::Derived(variant.ident.clone()), - Some(parent_attribute), - parent_attribute.casing(), - parent_attribute.env_casing(), - ); - 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) ) ) + let mut ext_subcmd = None; + + let match_arms: Vec<_> = variants + .iter() + .filter_map(|variant| { + let attrs = Attrs::from_struct( + variant.span(), + &variant.attrs, + Name::Derived(variant.ident.clone()), + Some(parent_attribute), + parent_attribute.casing(), + parent_attribute.env_casing(), + ); + + let sub_name = attrs.cased_name(); + let variant_name = &variant.ident; + + if let Kind::ExternalSubcommand = *attrs.kind() { + if ext_subcmd.is_some() { + abort!( + attrs.kind().span(), + "Only one variant can be marked with `external_subcommand`, \ + this is the second" + ); + } + + let ty = match variant.fields { + Unnamed(ref fields) if fields.unnamed.len() == 1 => &fields.unnamed[0].ty, + + _ => abort!( + variant.span(), + "The enum variant marked with `external_attribute` must be \ + a single-typed tuple, and the type must be either `Vec` \ + or `Vec`." + ), + }; + + let (span, str_ty, values_of) = match subty_if_name(ty, "Vec") { + Some(subty) => { + if is_simple_ty(subty, "String") { + ( + subty.span(), + quote!(::std::string::String), + quote!(values_of), + ) + } else { + ( + subty.span(), + quote!(::std::ffi::OsString), + quote!(values_of_os), + ) + } + } + + None => abort!( + ty.span(), + "The type must be either `Vec` or `Vec` \ + to be used with `external_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) + }) + } + }) + .collect(); + + let wildcard = 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<_>>() + )) } - Unnamed(..) => abort_call_site!("{}: tuple enums are not supported", variant.ident), - }; - quote! { - (#sub_name, Some(matches)) => - Some(#name :: #variant_name #constructor_block) - } - }); + (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 + })) + } + }, + + None => quote!(_ => None), + }; quote! { fn from_subcommand<'a, 'b>( sub: (&'b str, Option<&'b ::structopt::clap::ArgMatches<'a>>) ) -> Option { match sub { - #( #match_arms ),*, - _ => None + #( #match_arms, )* + #wildcard } } } @@ -616,13 +715,14 @@ fn impl_structopt_for_enum( attrs: &[Attribute], ) -> TokenStream { let basic_clap_app_gen = gen_clap_enum(attrs); + let clap_tokens = basic_clap_app_gen.tokens; + let attrs = basic_clap_app_gen.attrs; - let augment_clap = gen_augment_clap_enum(variants, &basic_clap_app_gen.attrs); + let augment_clap = gen_augment_clap_enum(variants, &attrs); let from_clap = gen_from_clap_enum(name); - let from_subcommand = gen_from_subcommand(name, variants, &basic_clap_app_gen.attrs); + let from_subcommand = gen_from_subcommand(name, variants, &attrs); let paw_impl = gen_paw_impl(name); - let clap_tokens = basic_clap_app_gen.tokens; quote! { #[allow(unknown_lints)] #[allow(unused_variables, dead_code, unreachable_code)] @@ -660,6 +760,8 @@ fn impl_structopt(input: &DeriveInput) -> TokenStream { unimplemented!() } } + + impl ::structopt::StructOptInternal for #struct_name {} }); match input.data { diff --git a/structopt-derive/src/parse.rs b/structopt-derive/src/parse.rs index 0386153f..fc422035 100644 --- a/structopt-derive/src/parse.rs +++ b/structopt-derive/src/parse.rs @@ -33,6 +33,7 @@ pub enum StructOptAttr { Env(Ident), Flatten(Ident), Subcommand(Ident), + ExternalSubcommand(Ident), NoVersion(Ident), VerbatimDocComment(Ident), @@ -185,6 +186,7 @@ impl Parse for StructOptAttr { "env" => Ok(Env(name)), "flatten" => Ok(Flatten(name)), "subcommand" => Ok(Subcommand(name)), + "external_subcommand" => Ok(ExternalSubcommand(name)), "no_version" => Ok(NoVersion(name)), "verbatim_doc_comment" => Ok(VerbatimDocComment(name)), diff --git a/structopt-derive/src/ty.rs b/structopt-derive/src/ty.rs index 06eb3ecf..89d8b00a 100644 --- a/structopt-derive/src/ty.rs +++ b/structopt-derive/src/ty.rs @@ -80,11 +80,11 @@ where }) } -fn subty_if_name<'a>(ty: &'a syn::Type, name: &str) -> Option<&'a syn::Type> { +pub fn subty_if_name<'a>(ty: &'a syn::Type, name: &str) -> Option<&'a syn::Type> { subty_if(ty, |seg| seg.ident == name) } -fn is_simple_ty(ty: &syn::Type, name: &str) -> bool { +pub fn is_simple_ty(ty: &syn::Type, name: &str) -> bool { only_last_segment(ty) .map(|segment| { if let PathArguments::None = segment.arguments { diff --git a/tests/subcommands.rs b/tests/subcommands.rs index 170c0da6..1fc8e76a 100644 --- a/tests/subcommands.rs +++ b/tests/subcommands.rs @@ -211,3 +211,93 @@ fn flatten_enum() { } ); } + +#[test] +fn external_subcommand() { + #[derive(Debug, PartialEq, StructOpt)] + struct Opt { + #[structopt(subcommand)] + sub: Subcommands, + } + + #[derive(Debug, PartialEq, StructOpt)] + enum Subcommands { + Add, + Remove, + #[structopt(external_subcommand)] + Other(Vec), + } + + assert_eq!( + Opt::from_iter(&["test", "add"]), + Opt { + sub: Subcommands::Add + } + ); + + assert_eq!( + Opt::from_iter(&["test", "remove"]), + Opt { + sub: Subcommands::Remove + } + ); + + assert_eq!( + Opt::from_iter(&["test", "git", "status"]), + Opt { + sub: Subcommands::Other(vec!["git".into(), "status".into()]) + } + ); + + assert!(Opt::from_iter_safe(&["test"]).is_err()); +} + +#[test] +fn external_subcommand_os_string() { + use std::ffi::OsString; + + #[derive(Debug, PartialEq, StructOpt)] + struct Opt { + #[structopt(subcommand)] + sub: Subcommands, + } + + #[derive(Debug, PartialEq, StructOpt)] + enum Subcommands { + #[structopt(external_subcommand)] + Other(Vec), + } + + assert_eq!( + Opt::from_iter(&["test", "git", "status"]), + Opt { + sub: Subcommands::Other(vec!["git".into(), "status".into()]) + } + ); + + assert!(Opt::from_iter_safe(&["test"]).is_err()); +} + +#[test] +fn external_subcommand_optional() { + #[derive(Debug, PartialEq, StructOpt)] + struct Opt { + #[structopt(subcommand)] + sub: Option, + } + + #[derive(Debug, PartialEq, StructOpt)] + enum Subcommands { + #[structopt(external_subcommand)] + Other(Vec), + } + + assert_eq!( + Opt::from_iter(&["test", "git", "status"]), + Opt { + sub: Some(Subcommands::Other(vec!["git".into(), "status".into()])) + } + ); + + assert_eq!(Opt::from_iter(&["test"]), Opt { sub: None }); +} diff --git a/tests/ui/external_subcommand_wrong_type.rs b/tests/ui/external_subcommand_wrong_type.rs new file mode 100644 index 00000000..ad62e733 --- /dev/null +++ b/tests/ui/external_subcommand_wrong_type.rs @@ -0,0 +1,19 @@ +use structopt::StructOpt; +use std::ffi::CString; + +#[derive(StructOpt, Debug)] +struct Opt { + #[structopt(subcommand)] + cmd: Command, +} + +#[derive(StructOpt, Debug)] +enum Command { + #[structopt(external_subcommand)] + Other(Vec) +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} \ No newline at end of file diff --git a/tests/ui/external_subcommand_wrong_type.stderr b/tests/ui/external_subcommand_wrong_type.stderr new file mode 100644 index 00000000..799ff1d7 --- /dev/null +++ b/tests/ui/external_subcommand_wrong_type.stderr @@ -0,0 +1,8 @@ +error[E0308]: mismatched types + --> $DIR/external_subcommand_wrong_type.rs:13:15 + | +13 | Other(Vec) + | ^^^^^^^ expected struct `std::ffi::CString`, found struct `std::ffi::OsString` + | + = note: expected type `std::vec::Vec` + found type `std::vec::Vec` diff --git a/tests/ui/multiple_external_subcommand.rs b/tests/ui/multiple_external_subcommand.rs new file mode 100644 index 00000000..986261bc --- /dev/null +++ b/tests/ui/multiple_external_subcommand.rs @@ -0,0 +1,21 @@ +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +struct Opt { + #[structopt(subcommand)] + cmd: Command, +} + +#[derive(StructOpt, Debug)] +enum Command { + #[structopt(external_subcommand)] + Run(Vec), + + #[structopt(external_subcommand)] + Other(Vec) +} + +fn main() { + let opt = Opt::from_args(); + println!("{:?}", opt); +} diff --git a/tests/ui/multiple_external_subcommand.stderr b/tests/ui/multiple_external_subcommand.stderr new file mode 100644 index 00000000..0c80c2ec --- /dev/null +++ b/tests/ui/multiple_external_subcommand.stderr @@ -0,0 +1,5 @@ +error: Only one variant can be marked with `external_subcommand`, this is the second + --> $DIR/multiple_external_subcommand.rs:14:17 + | +14 | #[structopt(external_subcommand)] + | ^^^^^^^^^^^^^^^^^^^