From 67ad41a0af76f30e2eaee904a9374870f6cc8e9f Mon Sep 17 00:00:00 2001 From: William Yao Date: Wed, 28 Jun 2017 19:16:31 -0500 Subject: [PATCH 01/29] separate creating app and adding arguments --- src/lib.rs | 3 +++ structopt-derive/src/lib.rs | 27 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e6de4a53..b67860d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ pub trait StructOpt { /// Returns the corresponding `clap::App`. fn clap<'a, 'b>() -> clap::App<'a, 'b>; + /// Add this app's arguments/subcommands to another `clap::App`. + fn augment_clap<'a, 'b>(clap::App<'a, 'b>) -> clap::App<'a, 'b>; + /// Creates the struct from `clap::ArgMatches`. fn from_clap(clap::ArgMatches) -> Self; diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 62a127b6..403ec4da 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -228,13 +228,25 @@ fn gen_from_clap(struct_name: &Ident, s: &[Field]) -> quote::Tokens { } } -fn gen_clap(ast: &DeriveInput, s: &[Field]) -> quote::Tokens { +fn gen_clap(ast: &DeriveInput) -> quote::Tokens { let struct_attrs: Vec<_> = extract_attrs(&ast.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"); + quote! { + fn clap<'a, 'b>() -> _structopt::clap::App<'a, 'b> { + let app = _structopt::clap::App::new(#name) + .version(#version) + .author(#author) + .about(#about); + Self::augment_clap(app) + } + } +} + +fn gen_augment_clap(s: &[Field]) -> quote::Tokens { let args = s.iter().map(|field| { let name = gen_name(field); let cur_type = ty(&field.ty); @@ -264,14 +276,11 @@ fn gen_clap(ast: &DeriveInput, s: &[Field]) -> quote::Tokens { .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> { + fn augment_clap<'a, 'b>(app: _structopt::clap::App<'a, 'b>) -> _structopt::clap::App<'a, 'b> { use std::error::Error; - _structopt::clap::App::new(#name) - .version(#version) - .author(#author) - .about(#about) - #( #args )* + app #( #args )* } } } @@ -283,7 +292,8 @@ fn impl_structopt(ast: &syn::DeriveInput) -> quote::Tokens { _ => panic!("Only struct is supported"), }; - let clap = gen_clap(ast, s); + let clap = gen_clap(ast); + let augment_clap = gen_augment_clap(s); let from_clap = gen_from_clap(struct_name, s); let dummy_const = Ident::new(format!("_IMPL_STRUCTOPT_FOR_{}", struct_name)); quote! { @@ -292,6 +302,7 @@ fn impl_structopt(ast: &syn::DeriveInput) -> quote::Tokens { extern crate structopt as _structopt; impl _structopt::StructOpt for #struct_name { #clap + #augment_clap #from_clap } }; From d20465721a5753b279db2858128034e6da20784e Mon Sep 17 00:00:00 2001 From: William Yao Date: Wed, 28 Jun 2017 19:33:32 -0500 Subject: [PATCH 02/29] split `impl_structopt` into struct and enum branches --- structopt-derive/src/lib.rs | 43 +++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 403ec4da..48c45342 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -228,8 +228,8 @@ fn gen_from_clap(struct_name: &Ident, s: &[Field]) -> quote::Tokens { } } -fn gen_clap(ast: &DeriveInput) -> quote::Tokens { - let struct_attrs: Vec<_> = extract_attrs(&ast.attrs, AttrSource::Struct).collect(); +fn gen_clap(struct_attrs: &[Attribute]) -> 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"); @@ -285,26 +285,41 @@ fn gen_augment_clap(s: &[Field]) -> quote::Tokens { } } -fn impl_structopt(ast: &syn::DeriveInput) -> quote::Tokens { +fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute]) -> quote::Tokens { + let clap = gen_clap(attrs); + let augment_clap = gen_augment_clap(fields); + let from_clap = gen_from_clap(name, fields); + + quote! { + impl _structopt::StructOpt for #name { + #clap + #augment_clap + #from_clap + } + } +} + +fn impl_structopt_for_enum(_variants: &[Variant]) -> quote::Tokens { + quote!() +} + +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(variants), + _ => panic!("structopt only supports non-tuple structs and enums") }; - let clap = gen_clap(ast); - let augment_clap = gen_augment_clap(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)] const #dummy_const: () = { extern crate structopt as _structopt; - impl _structopt::StructOpt for #struct_name { - #clap - #augment_clap - #from_clap - } + use structopt::StructOpt; + #inner_impl }; } } From 4112eabd62c4f289214d3ea292220e474976da11 Mon Sep 17 00:00:00 2001 From: William Yao Date: Wed, 28 Jun 2017 19:40:55 -0500 Subject: [PATCH 03/29] skeleton of enum impl --- structopt-derive/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 48c45342..a4c441d4 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -300,7 +300,15 @@ fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute] } fn impl_structopt_for_enum(_variants: &[Variant]) -> quote::Tokens { - quote!() + quote! { + impl _structopt::StructOpt for #name { + } + + impl #name { + fn from_subcommand<'a, 'b>(sub: (&'b str, Option<&'b _structopt::clap::ArgMatches<'a, 'b>>)) -> Option { + } + } + } } fn impl_structopt(ast: &DeriveInput) -> quote::Tokens { From dc2a1fbeeaf9c995fb95655e4b616d5b5e302822 Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 09:00:04 -0500 Subject: [PATCH 04/29] factor out code generation that's needed for enums --- structopt-derive/src/lib.rs | 120 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index a4c441d4..8ef9145c 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -181,19 +181,48 @@ fn from_attr_or_env(attrs: &[(Ident, Lit)], key: &str, env: &str) -> Lit { .unwrap_or_else(|| Lit::Str(default, StrStyle::Cooked)) } -fn gen_name(field: &Field) -> Ident { - extract_attrs(&field.attrs, AttrSource::Field) - .filter(|&(ref i, _)| i.as_ref() == "name") - .last() - .and_then(|(_, ref l)| match l { - &Lit::Str(ref s, _) => Some(Ident::new(s.clone())), - _ => None, - }) - .unwrap_or(field.ident.as_ref().unwrap().clone()) +/// 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 args = fields.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! {{ + use std::error::Error; + let #app_var = #app_var #( #args )* ; + #app_var + }} } -fn gen_from_clap(struct_name: &Ident, s: &[Field]) -> quote::Tokens { - let fields = s.iter().map(|field| { +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); let convert = match ty(&field.ty) { @@ -217,13 +246,31 @@ fn gen_from_clap(struct_name: &Ident, s: &[Field]) -> quote::Tokens { .unwrap() }, }; - quote!( #field_name: matches.#convert, ) + 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") + .last() + .and_then(|(_, ref l)| match l { + &Lit::Str(ref s, _) => Some(Ident::new(s.clone())), + _ => None, + }) + .unwrap_or(field.ident.as_ref().unwrap().clone()) +} + +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 } } } @@ -246,41 +293,12 @@ fn gen_clap(struct_attrs: &[Attribute]) -> quote::Tokens { } } -fn gen_augment_clap(s: &[Field]) -> quote::Tokens { - 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)*) ) - }); - +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: _structopt::clap::App<'a, 'b>) -> _structopt::clap::App<'a, 'b> { - use std::error::Error; - app #( #args )* + fn augment_clap<'a, 'b>(#app_var: _structopt::clap::App<'a, 'b>) -> _structopt::clap::App<'a, 'b> { + #augmentation } } } @@ -299,7 +317,7 @@ fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute] } } -fn impl_structopt_for_enum(_variants: &[Variant]) -> quote::Tokens { +fn impl_structopt_for_enum(name: &Ident, _variants: &[Variant]) -> quote::Tokens { quote! { impl _structopt::StructOpt for #name { } @@ -317,7 +335,7 @@ fn impl_structopt(ast: &DeriveInput) -> quote::Tokens { Body::Struct(VariantData::Struct(ref fields)) => impl_structopt_for_struct(struct_name, fields, &ast.attrs), Body::Enum(ref variants) => - impl_structopt_for_enum(variants), + impl_structopt_for_enum(struct_name, variants), _ => panic!("structopt only supports non-tuple structs and enums") }; From eebf9fabb5dd82ab51e0309104ca8720401101d9 Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 10:25:07 -0500 Subject: [PATCH 05/29] prototype enum stuff? --- structopt-derive/src/lib.rs | 127 ++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 8ef9145c..0c3525f9 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -181,6 +181,20 @@ 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 { @@ -303,6 +317,97 @@ fn gen_augment_clap(fields: &[Field]) -> quote::Tokens { } } +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"); + + quote! { + fn clap<'a, 'b>() -> _structopt::clap::App<'a, 'b> { + let app = _structopt::clap::App::new(#name) + .version(#version) + .author(#author) + .about(#about) + .setting(_structopt::clap::AppSettings::SubcommandRequired); + Self::augment_clap(app) + } + } +} + +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(Ident::new(s as &str)), + _ => None + }) + .next() + .unwrap_or_else(|| variant.ident.clone()); + let app_var = Ident::new("subcommand"); + let arg_block = match variant.data { + VariantData::Struct(ref fields) => gen_augmentation(fields, &app_var), + _ => unreachable!() + }; + + quote! { + .subcommand({ + let #app_var = _structopt::clap::SubCommand::with_name( stringify!(#name) ); + #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(Ident::new(s as &str)), + _ => None + }) + .next() + .unwrap_or_else(|| variant.ident.clone()); + let variant_name = &variant.ident; + let constructor_block = match variant.data { + VariantData::Struct(ref fields) => gen_constructor(fields), + _ => unreachable!() + }; + + quote! { + (stringify!(#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, 'b>>)) -> Option { + match sub { + #( #match_arms ),*, + _ => None + } + } + } +} + fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute]) -> quote::Tokens { let clap = gen_clap(attrs); let augment_clap = gen_augment_clap(fields); @@ -317,14 +422,28 @@ fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute] } } -fn impl_structopt_for_enum(name: &Ident, _variants: &[Variant]) -> quote::Tokens { +fn impl_structopt_for_enum(name: &Ident, variants: &[Variant], attrs: &[Attribute]) -> quote::Tokens { + if variants.iter().any(|variant| { + if let VariantData::Struct(..) = variant.data { false } else { true } + }) + { + panic!("enum variants must use curly braces"); + } + + 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 + #augment_clap + #from_clap } impl #name { - fn from_subcommand<'a, 'b>(sub: (&'b str, Option<&'b _structopt::clap::ArgMatches<'a, 'b>>)) -> Option { - } + #from_subcommand } } } @@ -335,7 +454,7 @@ fn impl_structopt(ast: &DeriveInput) -> quote::Tokens { 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), + impl_structopt_for_enum(struct_name, variants, &ast.attrs), _ => panic!("structopt only supports non-tuple structs and enums") }; From 79295f810969a5e82d8dffcc648f8859f28caf9e Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 10:52:01 -0500 Subject: [PATCH 06/29] lifetime fix --- structopt-derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 0c3525f9..184ec5d6 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -399,7 +399,7 @@ fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { }); quote! { - fn from_subcommand<'a, 'b>(sub: (&'b str, Option<&'b _structopt::clap::ArgMatches<'a, 'b>>)) -> Option { + fn from_subcommand<'a, 'b>(sub: (&'b str, Option<&'b _structopt::clap::ArgMatches<'a>>)) -> Option { match sub { #( #match_arms ),*, _ => None From f321bcade898ef3a3f14562be345af7bbf142946 Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 10:52:11 -0500 Subject: [PATCH 07/29] add test for subcommand parsing (texitoi/structopt#1) --- tests/subcommands.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/subcommands.rs diff --git a/tests/subcommands.rs b/tests/subcommands.rs new file mode 100644 index 00000000..e2aa4423 --- /dev/null +++ b/tests/subcommands.rs @@ -0,0 +1,57 @@ +// 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")] + 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()); +} From fdb1b9cb49e56546b4af20fd6a43b1e1acb97ffd Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 19:37:52 -0500 Subject: [PATCH 08/29] add ability to put in nested subcommands (texitoi/structopt#1) --- structopt-derive/src/lib.rs | 147 +++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 54 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 184ec5d6..62bc8b39 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -92,6 +92,7 @@ pub fn structopt(input: TokenStream) -> TokenStream { gen.parse().unwrap() } +#[derive(Copy, Clone)] enum Ty { Bool, U64, @@ -198,39 +199,56 @@ fn is_subcommand(field: &Field) -> bool { /// 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 args = fields.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)*) ) - }); + 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(); + 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)*) ) + }); + + assert!(subcmds.len() <= 1, "cannot have more than one nested subcommand"); quote! {{ use std::error::Error; let #app_var = #app_var #( #args )* ; + #( #subcmds )* #app_var }} } @@ -239,28 +257,41 @@ 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); - 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 ) + 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! {{ @@ -289,19 +320,26 @@ fn gen_from_clap(struct_name: &Ident, fields: &[Field]) -> quote::Tokens { } } -fn gen_clap(struct_attrs: &[Attribute]) -> quote::Tokens { +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); + .about(#about) + #setting + ; Self::augment_clap(app) } } @@ -409,7 +447,8 @@ fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { } fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute]) -> quote::Tokens { - let clap = gen_clap(attrs); + let subcmd_required = fields.iter().any(is_subcommand); + let clap = gen_clap(attrs, subcmd_required); let augment_clap = gen_augment_clap(fields); let from_clap = gen_from_clap(name, fields); From 21df5bcacdd9dabd9f8416eb97d10738a514e787 Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 19:44:34 -0500 Subject: [PATCH 09/29] ignore unused matches for empty subcommands (texitoi/structopt#1) --- structopt-derive/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 62bc8b39..abd0db0c 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -499,7 +499,8 @@ fn impl_structopt(ast: &DeriveInput) -> quote::Tokens { 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; use structopt::StructOpt; From 5ab334abcb7ae732846e89246960b04ed2931e71 Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 19:53:07 -0500 Subject: [PATCH 10/29] allow for optional subcommands (texitoi/structopt#1) --- structopt-derive/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index abd0db0c..92b0b619 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -447,7 +447,13 @@ fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { } fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute]) -> quote::Tokens { - let subcmd_required = fields.iter().any(is_subcommand); + 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); From d5f029a6d32a5a46e6a135f66167aa0770c6dfae Mon Sep 17 00:00:00 2001 From: William Yao Date: Thu, 29 Jun 2017 20:02:59 -0500 Subject: [PATCH 11/29] add test suite for nested subcommands (texitoi/structopt#1) --- tests/nested-subcommands.rs | 116 ++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/nested-subcommands.rs 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"])) + ); +} From ae7048bc733d5a534872ee01b8c9668acc076896 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 13:29:37 -0500 Subject: [PATCH 12/29] fix hyphenated subcommands --- structopt-derive/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 92b0b619..1c198d2f 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -379,11 +379,11 @@ fn gen_augment_clap_enum(variants: &[Variant]) -> quote::Tokens { let name = extract_attrs(&variant.attrs, AttrSource::Struct) .filter_map(|attr| match attr { (ref i, Lit::Str(ref s, ..)) if i == "name" => - Some(Ident::new(s as &str)), + Some(s.to_string()), _ => None }) .next() - .unwrap_or_else(|| variant.ident.clone()); + .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), @@ -392,7 +392,7 @@ fn gen_augment_clap_enum(variants: &[Variant]) -> quote::Tokens { quote! { .subcommand({ - let #app_var = _structopt::clap::SubCommand::with_name( stringify!(#name) ); + let #app_var = _structopt::clap::SubCommand::with_name( #name ); #arg_block }) } From 650e3bdeeeea041085b19d471fd1e7dc5788dfdd Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 13:36:36 -0500 Subject: [PATCH 13/29] add link to structopt-derive documentation from structopt crate --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b67860d6..68905506 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; From 54372b7ad721b63c17e2a30deb38b1015f2ad546 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 13:44:17 -0500 Subject: [PATCH 14/29] preliminary editing of structopt-derive documentation --- structopt-derive/src/lib.rs | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 1c198d2f..11f666e8 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: From 86611bccbb4f8218ecf7c3202a964a27e8362071 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 14:18:31 -0500 Subject: [PATCH 15/29] add documentation in structopt-derive about how to use subcommands --- structopt-derive/src/lib.rs | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 11f666e8..80370abc 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -76,6 +76,111 @@ //! .help("Set speed") //! .default_value("42") //! ``` +//! +//! ## Subcomamnds +//! +//! 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")] +//! supervising_faerie: Option, +//! #[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` extern crate proc_macro; extern crate syn; From f2c0cdb38d7134596c24b487277041636b7085f7 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 14:22:17 -0500 Subject: [PATCH 16/29] note about optional subcommands --- structopt-derive/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 80370abc..07c758e7 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -181,6 +181,25 @@ //! + `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: +//! +//! #[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; From c64cc1eeeb4f5d2585e0cd1b6337e290aea1205a Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 14:26:03 -0500 Subject: [PATCH 17/29] add note about using doc comments for help messages (texitoi/structopt#1) --- structopt-derive/src/lib.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 07c758e7..a29b7628 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -37,7 +37,7 @@ //! if not specified: //! //! - `name`: The binary name displayed in help messages. Defaults - to the crate name given by Cargo. +//! 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. @@ -77,6 +77,26 @@ //! .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 +//! ... +//! } +//! ``` +//! //! ## Subcomamnds //! //! Some applications, like `git`, support "subcommands;" an extra command that From 0418cb623696861127d1d4923fc4650a570a62e5 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 14:30:14 -0500 Subject: [PATCH 18/29] bump versions of structopt and structopt-derive --- Cargo.toml | 2 +- structopt-derive/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e787c873..461f5f7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "structopt" -version = "0.0.5" +version = "0.0.6" authors = ["Guillaume Pinot "] description = "Parse command line argument by defining a struct." documentation = "https://docs.rs/structopt" diff --git a/structopt-derive/Cargo.toml b/structopt-derive/Cargo.toml index 77fa9d6d..830448c2 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.0.6" authors = ["Guillaume Pinot "] description = "Parse command line argument by defining a struct, derive crate." documentation = "https://docs.rs/structopt-derive" From 7b8066c677fea81819e624b3a1460fc053b63195 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 14:49:01 -0500 Subject: [PATCH 19/29] add missing backticks (oops) --- structopt-derive/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index a29b7628..bc104669 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -206,6 +206,7 @@ //! //! A nested subcommand can be marked optional: //! +//! ```ignore //! #[derive(StructOpt)] //! #[structopt(name = "foo")] //! struct Foo { @@ -220,6 +221,7 @@ //! Baz {}, //! Quux {} //! } +//! ``` extern crate proc_macro; extern crate syn; From 72d9d3560ec24af369ed36f4d1603c0b875b5dc4 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 14:59:09 -0500 Subject: [PATCH 20/29] bump versions of structopt in README, fix typo --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index daaf1b7e..e0acfe57 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.0.6" +structopt-derive = "0.0.6" ``` And then, in your rust file: From 7cbf7669d465687eb9ca6519ed9084f1a2961bf5 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 15:22:53 -0500 Subject: [PATCH 21/29] bump versions to 0.1.0 --- Cargo.toml | 4 ++-- README.md | 4 ++-- structopt-derive/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 461f5f7a..e18bbbb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "structopt" -version = "0.0.6" +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 e0acfe57..64dc3f79 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Find it on Docs.rs: [structopt-derive](https://docs.rs/structopt-derive) and [st Add `structopt` and `structopt-derive` to your dependencies of your `Cargo.toml`: ```toml [dependencies] -structopt = "0.0.6" -structopt-derive = "0.0.6" +structopt = "0.1.0" +structopt-derive = "0.1.0" ``` And then, in your rust file: diff --git a/structopt-derive/Cargo.toml b/structopt-derive/Cargo.toml index 830448c2..32892387 100644 --- a/structopt-derive/Cargo.toml +++ b/structopt-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "structopt-derive" -version = "0.0.6" +version = "0.1.0" authors = ["Guillaume Pinot "] description = "Parse command line argument by defining a struct, derive crate." documentation = "https://docs.rs/structopt-derive" From fc9a8aefa2936f46ba420f75209388a28a0a8920 Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 16:01:00 -0500 Subject: [PATCH 22/29] remove unnecessary Option in documentation [ci skip] --- structopt-derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index bc104669..d8e9ab32 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -147,7 +147,7 @@ //! #[structopt(name = "make-cookie")] //! struct MakeCookie { //! #[structopt(name = "supervisor", default_value = "Puck")] -//! supervising_faerie: Option, +//! supervising_faerie: String, //! #[structopt(name = "tree")] //! /// The faerie tree this cookie is being made in. //! tree: Option, From 9a5ea9ce586d1043daa12ce6d3b7113aa8c766aa Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 16:05:17 -0500 Subject: [PATCH 23/29] fix typoes --- structopt-derive/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index d8e9ab32..305fafff 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -44,7 +44,7 @@ //! //! 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 +//! `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). @@ -97,7 +97,7 @@ //! } //! ``` //! -//! ## Subcomamnds +//! ## Subcommands //! //! Some applications, like `git`, support "subcommands;" an extra command that //! is used to differentiate what the application should do. With `git`, these From cb3bc99c53e60ad9a783b443eab43eaa577a3c3f Mon Sep 17 00:00:00 2001 From: William Yao Date: Mon, 3 Jul 2017 16:08:18 -0500 Subject: [PATCH 24/29] add extra Arg methods in doc so examples still work --- structopt-derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 305fafff..1423b39e 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -146,7 +146,7 @@ //! #[derive(StructOpt)] //! #[structopt(name = "make-cookie")] //! struct MakeCookie { -//! #[structopt(name = "supervisor", default_value = "Puck")] +//! #[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. From d7daced67c3fb94336cd2c3f7ffee38827ed843d Mon Sep 17 00:00:00 2001 From: William Yao Date: Tue, 4 Jul 2017 16:59:27 -0500 Subject: [PATCH 25/29] allow for documentation to be placed on subcommands --- examples/git.rs | 40 +++++++++++++++++++++++++++++++++++++ structopt-derive/src/lib.rs | 12 +++++++---- tests/subcommands.rs | 18 ++++++++++++++++- 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 examples/git.rs 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/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 1423b39e..700524b8 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -537,10 +537,14 @@ fn gen_augment_clap_enum(variants: &[Variant]) -> quote::Tokens { VariantData::Struct(ref fields) => gen_augmentation(fields, &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 ); + let #app_var = _structopt::clap::SubCommand::with_name( #name ) + #( #from_attr )* ; #arg_block }) } @@ -567,11 +571,11 @@ fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { let sub_name = extract_attrs(&variant.attrs, AttrSource::Struct) .filter_map(|attr| match attr { (ref i, Lit::Str(ref s, ..)) if i == "name" => - Some(Ident::new(s as &str)), + Some(s.to_string()), _ => None }) .next() - .unwrap_or_else(|| variant.ident.clone()); + .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), @@ -579,7 +583,7 @@ fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { }; quote! { - (stringify!(#sub_name), Some(matches)) => + (#sub_name, Some(matches)) => Some(#name :: #variant_name #constructor_block) } }); diff --git a/tests/subcommands.rs b/tests/subcommands.rs index e2aa4423..999c8ddd 100644 --- a/tests/subcommands.rs +++ b/tests/subcommands.rs @@ -12,7 +12,7 @@ use structopt::StructOpt; #[derive(StructOpt, PartialEq, Debug)] enum Opt { - #[structopt(name = "fetch")] + #[structopt(name = "fetch", about = "Fetch stuff from GitHub.")] Fetch { #[structopt(long = "all")] all: bool, @@ -55,3 +55,19 @@ fn test_no_parse() { 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"]))); +} From 4ff50e68cdf0810f133ba5f0a9237fb13550587d Mon Sep 17 00:00:00 2001 From: William Yao Date: Tue, 4 Jul 2017 17:09:54 -0500 Subject: [PATCH 26/29] add support for unit-variant subcommands --- structopt-derive/src/lib.rs | 6 ++++-- tests/subcommands.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 700524b8..e79d9df3 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -535,6 +535,7 @@ fn gen_augment_clap_enum(variants: &[Variant]) -> quote::Tokens { 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) @@ -579,6 +580,7 @@ fn gen_from_subcommand(name: &Ident, variants: &[Variant]) -> quote::Tokens { let variant_name = &variant.ident; let constructor_block = match variant.data { VariantData::Struct(ref fields) => gen_constructor(fields), + VariantData::Unit => quote!(), // empty _ => unreachable!() }; @@ -621,10 +623,10 @@ fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute] fn impl_structopt_for_enum(name: &Ident, variants: &[Variant], attrs: &[Attribute]) -> quote::Tokens { if variants.iter().any(|variant| { - if let VariantData::Struct(..) = variant.data { false } else { true } + if let VariantData::Tuple(..) = variant.data { true } else { false } }) { - panic!("enum variants must use curly braces"); + panic!("enum variants cannot be tuples"); } let clap = gen_clap_enum(attrs); diff --git a/tests/subcommands.rs b/tests/subcommands.rs index 999c8ddd..14ad5413 100644 --- a/tests/subcommands.rs +++ b/tests/subcommands.rs @@ -71,3 +71,20 @@ 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"]))); +} From 42529fbb4f6718cdc744f28fc578032041db09bb Mon Sep 17 00:00:00 2001 From: William Yao Date: Tue, 4 Jul 2017 17:11:30 -0500 Subject: [PATCH 27/29] update structopt-derive documentation to reflect unit subcommands change --- structopt-derive/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index e79d9df3..6535669d 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -217,9 +217,9 @@ //! //! #[derive(StructOpt)] //! enum Command { -//! Bar {}, -//! Baz {}, -//! Quux {} +//! Bar, +//! Baz, +//! Quux //! } //! ``` From b329f0135c985e88c380cf5ad6b03b2853c2679c Mon Sep 17 00:00:00 2001 From: William Yao Date: Fri, 7 Jul 2017 12:40:10 -0500 Subject: [PATCH 28/29] move `augment_clap` into inherent impl --- src/lib.rs | 3 --- structopt-derive/src/lib.rs | 7 +++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 68905506..36477c37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,9 +26,6 @@ pub trait StructOpt { /// Returns the corresponding `clap::App`. fn clap<'a, 'b>() -> clap::App<'a, 'b>; - /// Add this app's arguments/subcommands to another `clap::App`. - fn augment_clap<'a, 'b>(clap::App<'a, 'b>) -> clap::App<'a, 'b>; - /// Creates the struct from `clap::ArgMatches`. fn from_clap(clap::ArgMatches) -> Self; diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 6535669d..9178842e 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -615,9 +615,12 @@ fn impl_structopt_for_struct(name: &Ident, fields: &[Field], attrs: &[Attribute] quote! { impl _structopt::StructOpt for #name { #clap - #augment_clap #from_clap } + + impl #name { + #augment_clap + } } } @@ -637,11 +640,11 @@ fn impl_structopt_for_enum(name: &Ident, variants: &[Variant], attrs: &[Attribut quote! { impl _structopt::StructOpt for #name { #clap - #augment_clap #from_clap } impl #name { + #augment_clap #from_subcommand } } From 8060f31cb01cbc11a683b9dee7417d40757d5a3b Mon Sep 17 00:00:00 2001 From: William Yao Date: Fri, 7 Jul 2017 12:41:55 -0500 Subject: [PATCH 29/29] move assert --- structopt-derive/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/structopt-derive/src/lib.rs b/structopt-derive/src/lib.rs index 9178842e..3db8e81f 100644 --- a/structopt-derive/src/lib.rs +++ b/structopt-derive/src/lib.rs @@ -359,6 +359,9 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens { 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| { @@ -391,8 +394,6 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens { quote!( .arg(_structopt::clap::Arg::with_name(stringify!(#name)) #modifier #(#from_attr)*) ) }); - assert!(subcmds.len() <= 1, "cannot have more than one nested subcommand"); - quote! {{ use std::error::Error; let #app_var = #app_var #( #args )* ;