diff --git a/Cargo.toml b/Cargo.toml index e787c873..e18bbbb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "structopt" -version = "0.0.5" +version = "0.1.0" authors = ["Guillaume Pinot "] description = "Parse command line argument by defining a struct." documentation = "https://docs.rs/structopt" @@ -17,6 +17,6 @@ travis-ci = { repository = "TeXitoi/structopt" } clap = "2.20" [dev-dependencies] -structopt-derive = { path = "structopt-derive", version = "0.0.5" } +structopt-derive = { path = "structopt-derive", version = "0.1.0" } [workspace] diff --git a/README.md b/README.md index daaf1b7e..64dc3f79 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Find it on Docs.rs: [structopt-derive](https://docs.rs/structopt-derive) and [st ## Example -Add `structopt` and `structop-derive` to your dependencies of your `Cargo.toml`: +Add `structopt` and `structopt-derive` to your dependencies of your `Cargo.toml`: ```toml [dependencies] -structopt = "0.0.3" -structopt-derive = "0.0.3" +structopt = "0.1.0" +structopt-derive = "0.1.0" ``` And then, in your rust file: diff --git a/examples/git.rs b/examples/git.rs new file mode 100644 index 00000000..f25d66ce --- /dev/null +++ b/examples/git.rs @@ -0,0 +1,40 @@ +//! `git.rs` serves as a demonstration of how to use subcommands, +//! as well as a demonstration of adding documentation to subcommands. +//! Documentation can be added either through doc comments or the +//! `about` attribute. + +extern crate structopt; +#[macro_use] extern crate structopt_derive; + +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "git")] +/// the stupid content tracker +enum Opt { + #[structopt(name = "fetch")] + /// fetch branches from remote repository + Fetch { + #[structopt(long = "dry-run")] + dry_run: bool, + #[structopt(long = "all")] + all: bool, + #[structopt(default_value = "origin")] + repository: String + }, + #[structopt(name = "add")] + /// add files to the staging area + Add { + #[structopt(short = "i")] + interactive: bool, + #[structopt(short = "a")] + all: bool, + files: Vec + } +} + +fn main() { + let matches = Opt::from_args(); + + println!("{:?}", matches); +} diff --git a/src/lib.rs b/src/lib.rs index e6de4a53..36477c37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,9 @@ //! `StructOpt` trait definition //! //! This crate defines the `StructOpt` trait. Alone, this crate is of -//! little interest. See the `structopt-derive` crate to -//! automatically generate implementation of this trait. +//! little interest. See the +//! [`structopt-derive`](https://docs.rs/structopt-derive) crate to +//! automatically generate an implementation of this trait. extern crate clap as _clap; diff --git a/structopt-derive/Cargo.toml b/structopt-derive/Cargo.toml index 77fa9d6d..32892387 100644 --- a/structopt-derive/Cargo.toml +++ b/structopt-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "structopt-derive" -version = "0.0.5" +version = "0.1.0" authors = ["Guillaume Pinot "] description = "Parse command line argument by defining a struct, derive crate." documentation = "https://docs.rs/structopt-derive" diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 62a127b6..3db8e81f 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -5,9 +5,9 @@ // Version 2, as published by Sam Hocevar. See the COPYING file for // more details. -//! How to `derive(StructOpt)` +//! ## How to `derive(StructOpt)` //! -//! First, look at an example: +//! First, let's look at an example: //! //! ```ignore //! #[derive(StructOpt)] @@ -24,28 +24,30 @@ //! } //! ``` //! -//! So, `derive(StructOpt)` do the job, and `structopt` attribute is +//! So `derive(StructOpt)` tells Rust to generate a command line parser, +//! and the various `structopt` attributes are simply //! used for additional parameters. //! //! First, define a struct, whatever its name. This structure will //! correspond to a `clap::App`. Every method of `clap::App` in the -//! form of `fn function_name(self, &str)` can be use in the form of -//! attributes. Our example call for example something like -//! `app.about("An example of StructOpt usage.")`. There is some -//! special attributes: +//! form of `fn function_name(self, &str)` can be use through attributes +//! placed on the struct. In our example above, the `about` attribute +//! will become an `.about("An example of StructOpt usage.")` call on the +//! generated `clap::App`. There are a few attributes that will default +//! if not specified: //! -//! - `name`: correspond to the creation of the `App` object. Our -//! example does `clap::App::new("example")`. Default to -//! the crate name given by cargo. -//! - `version`: default to the crate version given by cargo. -//! - `author`: default to the crate version given by cargo. -//! - `about`: default to the crate version given by cargo. +//! - `name`: The binary name displayed in help messages. Defaults +//! to the crate name given by Cargo. +//! - `version`: Defaults to the crate version given by Cargo. +//! - `author`: Defaults to the crate author name given by Cargo. +//! - `about`: Defaults to the crate description given by Cargo. //! -//! Then, each field of the struct correspond to a `clap::Arg`. As -//! for the struct attributes, every method of `clap::Arg` in the form -//! of `fn function_name(self, &str)` can be use in the form of -//! attributes. The `name` attribute can be used to customize the -//! `Arg::with_name()` call (default to the field name). +//! Then, each field of the struct not marked as a subcommand corresponds +//! to a `clap::Arg`. As with the struct attributes, every method of +//! `clap::Arg` in the form of `fn function_name(self, &str)` can be used +//! through specifying it as an attribute. +//! The `name` attribute can be used to customize the +//! `Arg::with_name()` call (defaults to the field name). //! //! The type of the field gives the kind of argument: //! @@ -55,10 +57,10 @@ //! `u64` | number of params | `.takes_value(false).multiple(true)` //! `Option` | optional argument | `.takes_value(true).multiple(false)` //! `Vec` | list of arguments | `.takes_value(true).multiple(true)` -//! `T: FromStr` | required argument | `.takes_value(true).multiple(false).required(!has_default)` +//! `T: FromStr` | required argument | `.takes_value(true).multiple(false).required(!has_default)` //! //! The `FromStr` trait is used to convert the argument to the given -//! type, and the `Arg::validator` method is setted to a method using +//! type, and the `Arg::validator` method is set to a method using //! `FromStr::Error::description()`. //! //! Thus, the `speed` argument is generated as: @@ -74,6 +76,152 @@ //! .help("Set speed") //! .default_value("42") //! ``` +//! +//! ## Help messages +//! +//! Help messages for the whole binary or individual arguments can be +//! specified using the `about` attribute on the struct/field, as we've +//! already seen. For convenience, they can also be specified using +//! doc comments. For example: +//! +//! ```ignore +//! #[derive(StructOpt)] +//! #[structopt(name = "foo")] +//! /// The help message that will be displayed when passing `--help`. +//! struct Foo { +//! ... +//! #[structopt(short = "b")] +//! /// The description for the arg that will be displayed when passing `--help`. +//! bar: String +//! ... +//! } +//! ``` +//! +//! ## Subcommands +//! +//! Some applications, like `git`, support "subcommands;" an extra command that +//! is used to differentiate what the application should do. With `git`, these +//! would be `add`, `init`, `fetch`, `commit`, for a few examples. +//! +//! `clap` has this functionality, so `structopt` supports this through enums: +//! +//! ```ignore +//! #[derive(StructOpt)] +//! #[structopt(name = "git", about = "the stupid content tracker")] +//! enum Git { +//! #[structopt(name = "add")] +//! Add { +//! #[structopt(short = "i")] +//! interactive: bool, +//! #[structopt(short = "p")] +//! patch: bool, +//! files: Vec +//! }, +//! #[structopt(name = "fetch")] +//! Fetch { +//! #[structopt(long = "dry-run")] +//! dry_run: bool, +//! #[structopt(long = "all")] +//! all: bool, +//! repository: Option +//! }, +//! #[structopt(name = "commit")] +//! Commit { +//! #[structopt(short = "m")] +//! message: Option, +//! #[structopt(short = "a")] +//! all: bool +//! } +//! } +//! ``` +//! +//! Using `derive(StructOpt)` on an enum instead of a struct will produce +//! a `clap::App` that only takes subcommands. So `git add`, `git fetch`, +//! and `git commit` would be commands allowed for the above example. +//! +//! `structopt` also provides support for applications where certain flags +//! need to apply to all subcommands, as well as nested subcommands: +//! +//! ```ignore +//! #[derive(StructOpt)] +//! #[structopt(name = "make-cookie")] +//! struct MakeCookie { +//! #[structopt(name = "supervisor", default_value = "Puck", required = false, long = "supervisor")] +//! supervising_faerie: String, +//! #[structopt(name = "tree")] +//! /// The faerie tree this cookie is being made in. +//! tree: Option, +//! #[structopt(subcommand)] // Note that we mark a field as a subcommand +//! cmd: Command +//! } +//! +//! #[derive(StructOpt)] +//! enum Command { +//! #[structopt(name = "pound")] +//! /// Pound acorns into flour for cookie dough. +//! Pound { +//! acorns: u32 +//! }, +//! #[structopt(name = "sparkle")] +//! /// Add magical sparkles -- the secret ingredient! +//! Sparkle { +//! #[structopt(short = "m")] +//! magicality: u64, +//! #[structopt(short = "c")] +//! color: String +//! }, +//! #[structopt(name = "finish")] +//! Finish { +//! #[structopt(short = "t")] +//! time: u32, +//! #[structopt(subcommand)] // Note that we mark a field as a subcommand +//! type: FinishType +//! } +//! } +//! +//! #[derive(StructOpt)] +//! enum FinishType { +//! #[structopt(name = "glaze")] +//! Glaze { +//! applications: u32 +//! }, +//! #[structopt(name = "powder")] +//! Powder { +//! flavor: String, +//! dips: u32 +//! } +//! } +//! ``` +//! +//! Marking a field with `structopt(subcommand)` will add the subcommands of the +//! designated enum to the current `clap::App`. The designated enum *must* also +//! be derived `StructOpt`. So the above example would take the following +//! commands: +//! +//! + `make-cookie pound 50` +//! + `make-cookie sparkle -mmm --color "green"` +//! + `make-cookie finish 130 glaze 3` +//! +//! ### Optional subcommands +//! +//! A nested subcommand can be marked optional: +//! +//! ```ignore +//! #[derive(StructOpt)] +//! #[structopt(name = "foo")] +//! struct Foo { +//! file: String, +//! #[structopt(subcommand)] +//! cmd: Option +//! } +//! +//! #[derive(StructOpt)] +//! enum Command { +//! Bar, +//! Baz, +//! Quux +//! } +//! ``` extern crate proc_macro; extern crate syn; @@ -92,6 +240,7 @@ pub fn structopt(input: TokenStream) -> TokenStream { gen.parse().unwrap() } +#[derive(Copy, Clone)] enum Ty { Bool, U64, @@ -181,6 +330,124 @@ fn from_attr_or_env(attrs: &[(Ident, Lit)], key: &str, env: &str) -> Lit { .unwrap_or_else(|| Lit::Str(default, StrStyle::Cooked)) } +fn is_subcommand(field: &Field) -> bool { + field.attrs.iter() + .map(|attr| &attr.value) + .any(|meta| if let MetaItem::List(ref i, ref l) = *meta { + if i != "structopt" { return false; } + match l.first() { + Some(&NestedMetaItem::MetaItem(MetaItem::Word(ref inner))) => inner == "subcommand", + _ => false + } + } else { + false + }) +} + +/// Generate a block of code to add arguments/subcommands corresponding to +/// the `fields` to an app. +fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens { + let subcmds: Vec = fields.iter() + .filter(|&field| is_subcommand(field)) + .map(|field| { + let cur_type = ty(&field.ty); + let subcmd_type = match (cur_type, sub_type(&field.ty)) { + (Ty::Option, Some(sub_type)) => sub_type, + _ => &field.ty + }; + + quote!( let #app_var = #subcmd_type ::augment_clap( #app_var ); ) + }) + .collect(); + + assert!(subcmds.len() <= 1, "cannot have more than one nested subcommand"); + + let args = fields.iter() + .filter(|&field| !is_subcommand(field)) + .map(|field| { + let name = gen_name(field); + let cur_type = ty(&field.ty); + let convert_type = match cur_type { + Ty::Vec | Ty::Option => sub_type(&field.ty).unwrap_or(&field.ty), + _ => &field.ty, + }; + let validator = quote! { + validator(|s| s.parse::<#convert_type>() + .map(|_| ()) + .map_err(|e| e.description().into())) + }; + let modifier = match cur_type { + Ty::Bool => quote!( .takes_value(false).multiple(false) ), + Ty::U64 => quote!( .takes_value(false).multiple(true) ), + Ty::Option => quote!( .takes_value(true).multiple(false).#validator ), + Ty::Vec => quote!( .takes_value(true).multiple(true).#validator ), + Ty::Other => { + let required = extract_attrs(&field.attrs, AttrSource::Field) + .find(|&(ref i, _)| i.as_ref() == "default_value") + .is_none(); + quote!( .takes_value(true).multiple(false).required(#required).#validator ) + }, + }; + let from_attr = extract_attrs(&field.attrs, AttrSource::Field) + .filter(|&(ref i, _)| i.as_ref() != "name") + .map(|(i, l)| quote!(.#i(#l))); + quote!( .arg(_structopt::clap::Arg::with_name(stringify!(#name)) #modifier #(#from_attr)*) ) + }); + + quote! {{ + use std::error::Error; + let #app_var = #app_var #( #args )* ; + #( #subcmds )* + #app_var + }} +} + +fn gen_constructor(fields: &[Field]) -> quote::Tokens { + let fields = fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + let name = gen_name(field); + if is_subcommand(field) { + let cur_type = ty(&field.ty); + let subcmd_type = match (cur_type, sub_type(&field.ty)) { + (Ty::Option, Some(sub_type)) => sub_type, + _ => &field.ty + }; + let unwrapper = match cur_type { + Ty::Option => quote!(), + _ => quote!( .unwrap() ) + }; + quote!( #field_name: #subcmd_type ::from_subcommand(matches.subcommand()) #unwrapper ) + } else { + let convert = match ty(&field.ty) { + Ty::Bool => quote!(is_present(stringify!(#name))), + Ty::U64 => quote!(occurrences_of(stringify!(#name))), + Ty::Option => quote! { + value_of(stringify!(#name)) + .as_ref() + .map(|s| s.parse().unwrap()) + }, + Ty::Vec => quote! { + values_of(stringify!(#name)) + .map(|v| v.map(|s| s.parse().unwrap()).collect()) + .unwrap_or_else(Vec::new) + }, + Ty::Other => quote! { + value_of(stringify!(#name)) + .as_ref() + .unwrap() + .parse() + .unwrap() + }, + }; + quote!( #field_name: matches.#convert ) + } + }); + + quote! {{ + #( #fields ),* + }} +} + fn gen_name(field: &Field) -> Ident { extract_attrs(&field.attrs, AttrSource::Field) .filter(|&(ref i, _)| i.as_ref() == "name") @@ -192,108 +459,216 @@ fn gen_name(field: &Field) -> Ident { .unwrap_or(field.ident.as_ref().unwrap().clone()) } -fn gen_from_clap(struct_name: &Ident, s: &[Field]) -> quote::Tokens { - let fields = s.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); - let name = gen_name(field); - let convert = match ty(&field.ty) { - Ty::Bool => quote!(is_present(stringify!(#name))), - Ty::U64 => quote!(occurrences_of(stringify!(#name))), - Ty::Option => quote! { - value_of(stringify!(#name)) - .as_ref() - .map(|s| s.parse().unwrap()) - }, - Ty::Vec => quote! { - values_of(stringify!(#name)) - .map(|v| v.map(|s| s.parse().unwrap()).collect()) - .unwrap_or_else(Vec::new) - }, - Ty::Other => quote! { - value_of(stringify!(#name)) - .as_ref() - .unwrap() - .parse() - .unwrap() - }, - }; - quote!( #field_name: matches.#convert, ) - }); +fn gen_from_clap(struct_name: &Ident, fields: &[Field]) -> quote::Tokens { + let field_block = gen_constructor(fields); + quote! { fn from_clap(matches: _structopt::clap::ArgMatches) -> Self { - #struct_name { - #( #fields )* - } + #struct_name #field_block } } } -fn gen_clap(ast: &DeriveInput, s: &[Field]) -> quote::Tokens { - let struct_attrs: Vec<_> = extract_attrs(&ast.attrs, AttrSource::Struct).collect(); +fn gen_clap(struct_attrs: &[Attribute], subcmd_required: bool) -> quote::Tokens { + let struct_attrs: Vec<_> = extract_attrs(struct_attrs, AttrSource::Struct).collect(); let name = from_attr_or_env(&struct_attrs, "name", "CARGO_PKG_NAME"); let version = from_attr_or_env(&struct_attrs, "version", "CARGO_PKG_VERSION"); let author = from_attr_or_env(&struct_attrs, "author", "CARGO_PKG_AUTHORS"); let about = from_attr_or_env(&struct_attrs, "about", "CARGO_PKG_DESCRIPTION"); + let setting = if subcmd_required { + quote!( .setting(_structopt::clap::AppSettings::SubcommandRequired) ) + } else { + quote!() + }; + + quote! { + fn clap<'a, 'b>() -> _structopt::clap::App<'a, 'b> { + let app = _structopt::clap::App::new(#name) + .version(#version) + .author(#author) + .about(#about) + #setting + ; + Self::augment_clap(app) + } + } +} + +fn gen_augment_clap(fields: &[Field]) -> quote::Tokens { + let app_var = Ident::new("app"); + let augmentation = gen_augmentation(fields, &app_var); + quote! { + fn augment_clap<'a, 'b>(#app_var: _structopt::clap::App<'a, 'b>) -> _structopt::clap::App<'a, 'b> { + #augmentation + } + } +} + +fn gen_clap_enum(enum_attrs: &[Attribute]) -> quote::Tokens { + let enum_attrs: Vec<_> = extract_attrs(enum_attrs, AttrSource::Struct).collect(); + let name = from_attr_or_env(&enum_attrs, "name", "CARGO_PKG_NAME"); + let version = from_attr_or_env(&enum_attrs, "version", "CARGO_PKG_VERSION"); + let author = from_attr_or_env(&enum_attrs, "author", "CARGO_PKG_AUTHORS"); + let about = from_attr_or_env(&enum_attrs, "about", "CARGO_PKG_DESCRIPTION"); - let args = s.iter().map(|field| { - let name = gen_name(field); - let cur_type = ty(&field.ty); - let convert_type = match cur_type { - Ty::Vec | Ty::Option => sub_type(&field.ty).unwrap_or(&field.ty), - _ => &field.ty, - }; - let validator = quote! { - validator(|s| s.parse::<#convert_type>() - .map(|_| ()) - .map_err(|e| e.description().into())) - }; - let modifier = match cur_type { - Ty::Bool => quote!( .takes_value(false).multiple(false) ), - Ty::U64 => quote!( .takes_value(false).multiple(true) ), - Ty::Option => quote!( .takes_value(true).multiple(false).#validator ), - Ty::Vec => quote!( .takes_value(true).multiple(true).#validator ), - Ty::Other => { - let required = extract_attrs(&field.attrs, AttrSource::Field) - .find(|&(ref i, _)| i.as_ref() == "default_value") - .is_none(); - quote!( .takes_value(true).multiple(false).required(#required).#validator ) - }, - }; - let from_attr = extract_attrs(&field.attrs, AttrSource::Field) - .filter(|&(ref i, _)| i.as_ref() != "name") - .map(|(i, l)| quote!(.#i(#l))); - quote!( .arg(_structopt::clap::Arg::with_name(stringify!(#name)) #modifier #(#from_attr)*) ) - }); quote! { fn clap<'a, 'b>() -> _structopt::clap::App<'a, 'b> { - use std::error::Error; - _structopt::clap::App::new(#name) + let app = _structopt::clap::App::new(#name) .version(#version) .author(#author) .about(#about) - #( #args )* + .setting(_structopt::clap::AppSettings::SubcommandRequired); + Self::augment_clap(app) } } } -fn impl_structopt(ast: &syn::DeriveInput) -> quote::Tokens { +fn gen_augment_clap_enum(variants: &[Variant]) -> quote::Tokens { + let subcommands = variants.iter().map(|variant| { + let name = extract_attrs(&variant.attrs, AttrSource::Struct) + .filter_map(|attr| match attr { + (ref i, Lit::Str(ref s, ..)) if i == "name" => + Some(s.to_string()), + _ => None + }) + .next() + .unwrap_or_else(|| variant.ident.to_string()); + let app_var = Ident::new("subcommand"); + let arg_block = match variant.data { + VariantData::Struct(ref fields) => gen_augmentation(fields, &app_var), + VariantData::Unit => quote!( #app_var ), + _ => unreachable!() + }; + let from_attr = extract_attrs(&variant.attrs, AttrSource::Struct) + .filter(|&(ref i, _)| i != "name") + .map(|(i, l)| quote!( .#i(#l) )); + + quote! { + .subcommand({ + let #app_var = _structopt::clap::SubCommand::with_name( #name ) + #( #from_attr )* ; + #arg_block + }) + } + }); + + quote! { + fn augment_clap<'a, 'b>(app: _structopt::clap::App<'a, 'b>) -> _structopt::clap::App<'a, 'b> { + app #( #subcommands )* + } + } +} + +fn gen_from_clap_enum(name: &Ident) -> quote::Tokens { + quote! { + fn from_clap(matches: _structopt::clap::ArgMatches) -> Self { + #name ::from_subcommand(matches.subcommand()) + .unwrap() + } + } +} + +fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { + let match_arms = variants.iter().map(|variant| { + let sub_name = extract_attrs(&variant.attrs, AttrSource::Struct) + .filter_map(|attr| match attr { + (ref i, Lit::Str(ref s, ..)) if i == "name" => + Some(s.to_string()), + _ => None + }) + .next() + .unwrap_or_else(|| variant.ident.as_ref().to_string()); + let variant_name = &variant.ident; + let constructor_block = match variant.data { + VariantData::Struct(ref fields) => gen_constructor(fields), + VariantData::Unit => quote!(), // empty + _ => unreachable!() + }; + + quote! { + (#sub_name, Some(matches)) => + Some(#name :: #variant_name #constructor_block) + } + }); + + quote! { + fn from_subcommand<'a, 'b>(sub: (&'b str, Option<&'b _structopt::clap::ArgMatches<'a>>)) -> Option { + match sub { + #( #match_arms ),*, + _ => None + } + } + } +} + +fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute]) -> quote::Tokens { + let subcmd_required = fields.iter().any(|field| { + let cur_type = ty(&field.ty); + match cur_type { + Ty::Option => false, + _ => is_subcommand(field) + } + }); + let clap = gen_clap(attrs, subcmd_required); + let augment_clap = gen_augment_clap(fields); + let from_clap = gen_from_clap(name, fields); + + quote! { + impl _structopt::StructOpt for #name { + #clap + #from_clap + } + + impl #name { + #augment_clap + } + } +} + +fn impl_structopt_for_enum(name: &Ident, variants: &[Variant], attrs: &[Attribute]) -> quote::Tokens { + if variants.iter().any(|variant| { + if let VariantData::Tuple(..) = variant.data { true } else { false } + }) + { + panic!("enum variants cannot be tuples"); + } + + let clap = gen_clap_enum(attrs); + let augment_clap = gen_augment_clap_enum(variants); + let from_clap = gen_from_clap_enum(name); + let from_subcommand = gen_from_subcommand(name, variants); + + quote! { + impl _structopt::StructOpt for #name { + #clap + #from_clap + } + + impl #name { + #augment_clap + #from_subcommand + } + } +} + +fn impl_structopt(ast: &DeriveInput) -> quote::Tokens { let struct_name = &ast.ident; - let s = match ast.body { - Body::Struct(VariantData::Struct(ref s)) => s, - _ => panic!("Only struct is supported"), + let inner_impl = match ast.body { + Body::Struct(VariantData::Struct(ref fields)) => + impl_structopt_for_struct(struct_name, fields, &ast.attrs), + Body::Enum(ref variants) => + impl_structopt_for_enum(struct_name, variants, &ast.attrs), + _ => panic!("structopt only supports non-tuple structs and enums") }; - let clap = gen_clap(ast, s); - let from_clap = gen_from_clap(struct_name, s); let dummy_const = Ident::new(format!("_IMPL_STRUCTOPT_FOR_{}", struct_name)); quote! { - #[allow(non_upper_case_globals, unused_attributes, unused_imports)] + #[allow(non_upper_case_globals)] + #[allow(unused_attributes, unused_imports, unused_variables)] const #dummy_const: () = { extern crate structopt as _structopt; - impl _structopt::StructOpt for #struct_name { - #clap - #from_clap - } + use structopt::StructOpt; + #inner_impl }; } } diff --git a/tests/nested-subcommands.rs b/tests/nested-subcommands.rs new file mode 100644 index 00000000..d978982e --- /dev/null +++ b/tests/nested-subcommands.rs @@ -0,0 +1,116 @@ +// Copyright (c) 2017 Guillaume Pinot +// +// This work is free. You can redistribute it and/or modify it under +// the terms of the Do What The Fuck You Want To Public License, +// Version 2, as published by Sam Hocevar. See the COPYING file for +// more details. + +extern crate structopt; +#[macro_use] extern crate structopt_derive; + +use structopt::StructOpt; + +#[derive(StructOpt, PartialEq, Debug)] +struct Opt { + #[structopt(short = "f", long = "force")] + force: bool, + #[structopt(short = "v", long = "verbose")] + verbose: u64, + #[structopt(subcommand)] + cmd: Sub +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Sub { + #[structopt(name = "fetch")] + Fetch {}, + #[structopt(name = "add")] + Add {} +} + +#[derive(StructOpt, PartialEq, Debug)] +struct Opt2 { + #[structopt(short = "f", long = "force")] + force: bool, + #[structopt(short = "v", long = "verbose")] + verbose: u64, + #[structopt(subcommand)] + cmd: Option +} + +#[test] +fn test_no_cmd() { + let result = Opt::clap().get_matches_from_safe(&["test"]); + assert!(result.is_err()); + + assert_eq!(Opt2 { force: false, verbose: 0, cmd: None }, + Opt2::from_clap(Opt2::clap().get_matches_from(&["test"]))); +} + +#[test] +fn test_fetch() { + assert_eq!(Opt { force: false, verbose: 3, cmd: Sub::Fetch {} }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "-vvv", "fetch"]))); + assert_eq!(Opt { force: true, verbose: 0, cmd: Sub::Fetch {} }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "--force", "fetch"]))); +} + +#[test] +fn test_add() { + assert_eq!(Opt { force: false, verbose: 0, cmd: Sub::Add {} }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "add"]))); + assert_eq!(Opt { force: false, verbose: 2, cmd: Sub::Add {} }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "-vv", "add"]))); +} + +#[test] +fn test_badinput() { + let result = Opt::clap().get_matches_from_safe(&["test", "badcmd"]); + assert!(result.is_err()); + let result = Opt::clap().get_matches_from_safe(&["test", "add", "--verbose"]); + assert!(result.is_err()); + let result = Opt::clap().get_matches_from_safe(&["test", "--badopt", "add"]); + assert!(result.is_err()); + let result = Opt::clap().get_matches_from_safe(&["test", "add", "--badopt"]); + assert!(result.is_err()); +} + +#[derive(StructOpt, PartialEq, Debug)] +struct Opt3 { + #[structopt(short = "a", long = "all")] + all: bool, + #[structopt(subcommand)] + cmd: Sub2 +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Sub2 { + #[structopt(name = "foo")] + Foo { + file: String, + #[structopt(subcommand)] + cmd: Sub3 + }, + #[structopt(name = "bar")] + Bar { + } +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Sub3 { + #[structopt(name = "baz")] + Baz {}, + #[structopt(name = "quux")] + Quux {} +} + +#[test] +fn test_subsubcommand() { + assert_eq!( + Opt3 { + all: true, + cmd: Sub2::Foo { file: "lib.rs".to_string(), cmd: Sub3::Quux {} } + }, + Opt3::from_clap(Opt3::clap().get_matches_from(&["test", "--all", "foo", "lib.rs", "quux"])) + ); +} diff --git a/tests/subcommands.rs b/tests/subcommands.rs new file mode 100644 index 00000000..14ad5413 --- /dev/null +++ b/tests/subcommands.rs @@ -0,0 +1,90 @@ +// Copyright (c) 2017 Guillaume Pinot +// +// This work is free. You can redistribute it and/or modify it under +// the terms of the Do What The Fuck You Want To Public License, +// Version 2, as published by Sam Hocevar. See the COPYING file for +// more details. + +extern crate structopt; +#[macro_use] extern crate structopt_derive; + +use structopt::StructOpt; + +#[derive(StructOpt, PartialEq, Debug)] +enum Opt { + #[structopt(name = "fetch", about = "Fetch stuff from GitHub.")] + Fetch { + #[structopt(long = "all")] + all: bool, + #[structopt(short = "f", long = "force")] + /// Overwrite local branches. + force: bool, + repo: String + }, + + #[structopt(name = "add")] + Add { + #[structopt(short = "i", long = "interactive")] + interactive: bool, + #[structopt(short = "v", long = "verbose")] + verbose: bool + } +} + +#[test] +fn test_fetch() { + assert_eq!(Opt::Fetch { all: true, force: false, repo: "origin".to_string() }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "fetch", "--all", "origin"]))); + assert_eq!(Opt::Fetch { all: false, force: true, repo: "origin".to_string() }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "fetch", "-f", "origin"]))); +} + +#[test] +fn test_add() { + assert_eq!(Opt::Add { interactive: false, verbose: false }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "add"]))); + assert_eq!(Opt::Add { interactive: true, verbose: true }, + Opt::from_clap(Opt::clap().get_matches_from(&["test", "add", "-i", "-v"]))); +} + +#[test] +fn test_no_parse() { + let result = Opt::clap().get_matches_from_safe(&["test", "badcmd", "-i", "-v"]); + assert!(result.is_err()); + + let result = Opt::clap().get_matches_from_safe(&["test", "add", "--badoption"]); + assert!(result.is_err()); +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Opt2 { + #[structopt(name = "do-something")] + DoSomething { + arg: String + } +} + +#[test] +/// This test is specifically to make sure that hyphenated subcommands get +/// processed correctly. +fn test_hyphenated_subcommands() { + assert_eq!(Opt2::DoSomething { arg: "blah".to_string() }, + Opt2::from_clap(Opt2::clap().get_matches_from(&["test", "do-something", "blah"]))); +} + +#[derive(StructOpt, PartialEq, Debug)] +enum Opt3 { + #[structopt(name = "add")] + Add, + #[structopt(name = "init")] + Init, + #[structopt(name = "fetch")] + Fetch +} + +#[test] +fn test_null_commands() { + assert_eq!(Opt3::Add, Opt3::from_clap(Opt3::clap().get_matches_from(&["test", "add"]))); + assert_eq!(Opt3::Init, Opt3::from_clap(Opt3::clap().get_matches_from(&["test", "init"]))); + assert_eq!(Opt3::Fetch, Opt3::from_clap(Opt3::clap().get_matches_from(&["test", "fetch"]))); +}