Skip to content

Commit

Permalink
New feature: #[structopt(external_subcommand)]
Browse files Browse the repository at this point in the history
  • Loading branch information
CreepySkeleton committed Jan 11, 2020
1 parent 35cefe8 commit eec7171
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 33 deletions.
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

0 comments on commit eec7171

Please sign in to comment.