Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce #[structopt(external_subcommand)] #314

Merged
merged 3 commits into from
Jan 18, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
target
Cargo.lock
*~
expanded.rs

.idea/
.vscode/
55 changes: 55 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//!
Expand Down Expand Up @@ -867,6 +868,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<String>),
//! }
//!
//! // 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<Subcommands>` 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<String>` or `Vec<OsString>`. `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,
Expand Down
10 changes: 9 additions & 1 deletion structopt-derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use syn::{
pub enum Kind {
Arg(Sp<Ty>),
Subcommand(Sp<Ty>),
ExternalSubcommand,
FlattenStruct,
Skip(Option<Expr>),
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -444,6 +449,9 @@ impl Attrs {
);
}
}

Kind::ExternalSubcommand => {}

Kind::Subcommand(_) => {
if res.has_custom_parser {
abort!(
Expand Down
163 changes: 133 additions & 30 deletions structopt-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -116,9 +116,14 @@ 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;
// let settings = gen_subcommand_settings(&a);
Some(quote_spanned! { kind.span()=>
let #app_var = <#ty as ::structopt::StructOptInternal>::augment_clap(#app_var);
let #app_var = if <#ty as ::structopt::StructOptInternal>::is_subcommand() {
Expand Down Expand Up @@ -244,6 +249,11 @@ fn gen_constructor(fields: &Punctuated<Field, Comma>, 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,
Expand Down Expand Up @@ -457,6 +467,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),
Expand Down Expand Up @@ -520,40 +537,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<String>` \
or `Vec<OsString>`."
),
};

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<String>` or `Vec<OsString>` \
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<Self> {
match sub {
#( #match_arms ),*,
_ => None
#( #match_arms, )*
#wildcard
}
}
}
Expand Down Expand Up @@ -616,13 +716,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)]
Expand Down Expand Up @@ -660,6 +761,8 @@ fn impl_structopt(input: &DeriveInput) -> TokenStream {
unimplemented!()
}
}

impl ::structopt::StructOptInternal for #struct_name {}
});

match input.data {
Expand Down
2 changes: 2 additions & 0 deletions structopt-derive/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub enum StructOptAttr {
Env(Ident),
Flatten(Ident),
Subcommand(Ident),
ExternalSubcommand(Ident),
NoVersion(Ident),
VerbatimDocComment(Ident),

Expand Down Expand Up @@ -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)),

Expand Down
4 changes: 2 additions & 2 deletions structopt-derive/src/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading