Add 'parse(from_occurrences)' parser. Don't special case 'u64'. (#48)

This commit is contained in:
Sergio Benitez 2018-02-03 06:15:03 -08:00 committed by Guillaume P
parent 76821ba7b2
commit 35423c96c4
5 changed files with 116 additions and 56 deletions

View file

@ -19,8 +19,8 @@ struct Opt {
#[structopt(short = "d", long = "debug")] #[structopt(short = "d", long = "debug")]
debug: bool, debug: bool,
/// Verbose mode /// Verbose mode (-v, -vv, -vvv, etc.)
#[structopt(short = "v", long = "verbose")] #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u64, verbose: u64,
/// Set speed /// Set speed

View file

@ -50,9 +50,9 @@
//! extern crate structopt; //! extern crate structopt;
//! #[macro_use] //! #[macro_use]
//! extern crate structopt_derive; //! extern crate structopt_derive;
//! //!
//! use structopt::StructOpt; //! use structopt::StructOpt;
//! //!
//! #[derive(StructOpt, Debug)] //! #[derive(StructOpt, Debug)]
//! #[structopt(setting_raw = "clap::AppSettings::ColoredHelp")] //! #[structopt(setting_raw = "clap::AppSettings::ColoredHelp")]
//! struct Opt { //! struct Opt {
@ -61,7 +61,7 @@
//! #[structopt(short = "d")] //! #[structopt(short = "d")]
//! debug: bool, //! debug: bool,
//! } //! }
//! //!
//! fn main() { //! fn main() {
//! let opt = Opt::from_args(); //! let opt = Opt::from_args();
//! println!("{:?}", opt); //! println!("{:?}", opt);
@ -82,7 +82,6 @@
//! Type | Effect | Added method call to `clap::Arg` //! Type | Effect | Added method call to `clap::Arg`
//! ---------------------|---------------------------------------------------|-------------------------------------- //! ---------------------|---------------------------------------------------|--------------------------------------
//! `bool` | `true` if the flag is present | `.takes_value(false).multiple(false)` //! `bool` | `true` if the flag is present | `.takes_value(false).multiple(false)`
//! `u64` | number of times the flag is used | `.takes_value(false).multiple(true)`
//! `Option<T: FromStr>` | optional positional argument or option | `.takes_value(true).multiple(false)` //! `Option<T: FromStr>` | optional positional argument or option | `.takes_value(true).multiple(false)`
//! `Vec<T: FromStr>` | list of options or the other positional arguments | `.takes_value(true).multiple(true)` //! `Vec<T: FromStr>` | list of options or the other positional arguments | `.takes_value(true).multiple(true)`
//! `T: FromStr` | required option or positional argument | `.takes_value(true).multiple(false).required(!has_default)` //! `T: FromStr` | required option or positional argument | `.takes_value(true).multiple(false).required(!has_default)`
@ -197,7 +196,7 @@
//! #[structopt(name = "sparkle")] //! #[structopt(name = "sparkle")]
//! /// Add magical sparkles -- the secret ingredient! //! /// Add magical sparkles -- the secret ingredient!
//! Sparkle { //! Sparkle {
//! #[structopt(short = "m")] //! #[structopt(short = "m", parse(from_occurrences))]
//! magicality: u64, //! magicality: u64,
//! #[structopt(short = "c")] //! #[structopt(short = "c")]
//! color: String //! color: String
@ -278,7 +277,7 @@
//! } //! }
//! ``` //! ```
//! //!
//! There are four kinds custom string parsers: //! There are five kinds of custom parsers:
//! //!
//! | Kind | Signature | Default | //! | Kind | Signature | Default |
//! |-------------------|---------------------------------------|---------------------------------| //! |-------------------|---------------------------------------|---------------------------------|
@ -286,9 +285,16 @@
//! | `try_from_str` | `fn(&str) -> Result<T, E>` | `::std::str::FromStr::from_str` | //! | `try_from_str` | `fn(&str) -> Result<T, E>` | `::std::str::FromStr::from_str` |
//! | `from_os_str` | `fn(&OsStr) -> T` | `::std::convert::From::from` | //! | `from_os_str` | `fn(&OsStr) -> T` | `::std::convert::From::from` |
//! | `try_from_os_str` | `fn(&OsStr) -> Result<T, OsString>` | (no default function) | //! | `try_from_os_str` | `fn(&OsStr) -> Result<T, OsString>` | (no default function) |
//! | `from_occurrences`| `fn(u64) -> T` | `value as T` |
//! //!
//! When supplying a custom string parser, `bool` and `u64` will not be treated //! The `from_occurrences` parser is special. Using `parse(from_occurrences)`
//! specially: //! results in the _number of flags occurrences_ being stored in the relevant
//! field or being passed to the supplied function. In other words, it converts
//! something like `-vvv` to `3`. This is equivalent to
//! `.takes_value(false).multiple(true)`. Note that the default parser can only
//! be used with fields of integer types (`u8`, `usize`, `i64`, etc.).
//!
//! When supplying a custom string parser, `bool` will not be treated specially:
//! //!
//! Type | Effect | Added method call to `clap::Arg` //! Type | Effect | Added method call to `clap::Arg`
//! ------------|-------------------|-------------------------------------- //! ------------|-------------------|--------------------------------------
@ -321,7 +327,6 @@ pub fn structopt(input: TokenStream) -> TokenStream {
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
enum Ty { enum Ty {
Bool, Bool,
U64,
Vec, Vec,
Option, Option,
Other, Other,
@ -331,7 +336,6 @@ fn ty(t: &syn::Ty) -> Ty {
if let syn::Ty::Path(None, syn::Path { segments: ref segs, .. }) = *t { if let syn::Ty::Path(None, syn::Path { segments: ref segs, .. }) = *t {
match segs.last().unwrap().ident.as_ref() { match segs.last().unwrap().ident.as_ref() {
"bool" => Ty::Bool, "bool" => Ty::Bool,
"u64" => Ty::U64,
"Option" => Ty::Option, "Option" => Ty::Option,
"Vec" => Ty::Vec, "Vec" => Ty::Vec,
_ => Ty::Other, _ => Ty::Other,
@ -359,7 +363,7 @@ fn sub_type(t: &syn::Ty) -> Option<&syn::Ty> {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum AttrSource { Struct, Field, } enum AttrSource { Struct, Field, }
#[derive(Debug)] #[derive(Debug, PartialEq)]
enum Parser { enum Parser {
/// Parse an option to using a `fn(&str) -> T` function. The function should never fail. /// Parse an option to using a `fn(&str) -> T` function. The function should never fail.
FromStr, FromStr,
@ -370,6 +374,9 @@ enum Parser {
FromOsStr, FromOsStr,
/// Parse an option to using a `fn(&OsStr) -> Result<T, OsString>` function. /// Parse an option to using a `fn(&OsStr) -> Result<T, OsString>` function.
TryFromOsStr, TryFromOsStr,
/// Counts the number of flag occurrences. Parses using a `fn(u64) -> T` function. The function
/// should never fail.
FromOccurrences,
} }
fn extract_attrs<'a>(attrs: &'a [Attribute], attr_source: AttrSource) -> Box<Iterator<Item = (Ident, Lit)> + 'a> { fn extract_attrs<'a>(attrs: &'a [Attribute], attr_source: AttrSource) -> Box<Iterator<Item = (Ident, Lit)> + 'a> {
@ -470,30 +477,25 @@ fn get_parser(field: &Field) -> Option<(Parser, quote::Tokens)> {
match *attr { match *attr {
NestedMetaItem::MetaItem(MetaItem::NameValue(ref i, Lit::Str(ref v, _))) => { NestedMetaItem::MetaItem(MetaItem::NameValue(ref i, Lit::Str(ref v, _))) => {
let function = parse_path(v).expect("parser function path"); let function = parse_path(v).expect("parser function path");
let parser = if i == "from_str" { let parser = match i.as_ref() {
Parser::FromStr "from_str" => Parser::FromStr,
} else if i == "try_from_str" { "try_from_str" => Parser::TryFromStr,
Parser::TryFromStr "from_os_str" => Parser::FromOsStr,
} else if i == "from_os_str" { "try_from_os_str" => Parser::TryFromOsStr,
Parser::FromOsStr "from_occurrences" => Parser::FromOccurrences,
} else if i == "try_from_os_str" { _ => panic!("unsupported parser {}", i)
Parser::TryFromOsStr
} else {
panic!("unsupported parser {}", i);
}; };
(parser, quote!(#function)) (parser, quote!(#function))
} }
NestedMetaItem::MetaItem(MetaItem::Word(ref i)) => { NestedMetaItem::MetaItem(MetaItem::Word(ref i)) => {
if i == "from_str" { match i.as_ref() {
(Parser::FromStr, quote!(::std::convert::From::from)) "from_str" => (Parser::FromStr, quote!(::std::convert::From::from)),
} else if i == "try_from_str" { "try_from_str" => (Parser::TryFromStr, quote!(::std::str::FromStr::from_str)),
(Parser::TryFromStr, quote!(::std::str::FromStr::from_str)) "from_os_str" => (Parser::FromOsStr, quote!(::std::convert::From::from)),
} else if i == "from_os_str" { "try_from_os_str" => panic!("cannot omit parser function name with `try_from_os_str`"),
(Parser::FromOsStr, quote!(::std::convert::From::from)) "from_occurrences" => (Parser::FromOccurrences, quote!({|v| v as _})),
} else if i == "try_from_os_str" { _ => panic!("unsupported parser {}", i)
panic!("cannot omit parser function name with `try_from_os_str`")
} else {
panic!("unsupported parser {}", i);
} }
} }
_ => panic!("unknown value parser specification"), _ => panic!("unknown value parser specification"),
@ -504,7 +506,7 @@ fn get_parser(field: &Field) -> Option<(Parser, quote::Tokens)> {
fn convert_with_custom_parse(cur_type: Ty) -> Ty { fn convert_with_custom_parse(cur_type: Ty) -> Ty {
match cur_type { match cur_type {
Ty::Bool | Ty::U64 => Ty::Other, Ty::Bool => Ty::Other,
rest => rest, rest => rest,
} }
} }
@ -545,10 +547,13 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens {
_ => &field.ty, _ => &field.ty,
}; };
let mut occurences = false;
let parser = get_parser(field); let parser = get_parser(field);
if parser.is_some() { if let Some((ref parser, _)) = parser {
cur_type = convert_with_custom_parse(cur_type); cur_type = convert_with_custom_parse(cur_type);
occurences = *parser == Parser::FromOccurrences;
} }
let validator = match parser.unwrap_or_else(get_default_parser) { let validator = match parser.unwrap_or_else(get_default_parser) {
(Parser::TryFromStr, f) => quote! { (Parser::TryFromStr, f) => quote! {
.validator(|s| { .validator(|s| {
@ -565,9 +570,9 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens {
let modifier = match cur_type { let modifier = match cur_type {
Ty::Bool => quote!( .takes_value(false).multiple(false) ), 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::Option => quote!( .takes_value(true).multiple(false) #validator ),
Ty::Vec => quote!( .takes_value(true).multiple(true) #validator ), Ty::Vec => quote!( .takes_value(true).multiple(true) #validator ),
Ty::Other if occurences => quote!( .takes_value(false).multiple(true) ),
Ty::Other => { Ty::Other => {
let required = extract_attrs(&field.attrs, AttrSource::Field) let required = extract_attrs(&field.attrs, AttrSource::Field)
.find(|&(ref i, _)| i.as_ref() == "default_value" .find(|&(ref i, _)| i.as_ref() == "default_value"
@ -621,13 +626,15 @@ fn gen_constructor(fields: &[Field]) -> quote::Tokens {
}; };
quote!( #field_name: #subcmd_type ::from_subcommand(matches.subcommand()) #unwrapper ) quote!( #field_name: #subcmd_type ::from_subcommand(matches.subcommand()) #unwrapper )
} else { } else {
let mut cur_type = ty(&field.ty); let real_ty = &field.ty;
let mut cur_type = ty(real_ty);
let parser = get_parser(field); let parser = get_parser(field);
if parser.is_some() { if parser.is_some() {
cur_type = convert_with_custom_parse(cur_type); cur_type = convert_with_custom_parse(cur_type);
} }
let (value_of, values_of, parse) = match parser.unwrap_or_else(get_default_parser) { let parser = parser.unwrap_or_else(get_default_parser);
let (value_of, values_of, parse) = match parser {
(Parser::FromStr, f) => ( (Parser::FromStr, f) => (
quote!(value_of), quote!(value_of),
quote!(values_of), quote!(values_of),
@ -648,28 +655,37 @@ fn gen_constructor(fields: &[Field]) -> quote::Tokens {
quote!(values_of_os), quote!(values_of_os),
quote!(|s| #f(s).unwrap()), quote!(|s| #f(s).unwrap()),
), ),
(Parser::FromOccurrences, f) => (
quote!(occurrences_of),
quote!(),
f,
),
}; };
let convert = match cur_type { let occurences = parser.0 == Parser::FromOccurrences;
Ty::Bool => quote!(is_present(stringify!(#name))), let field_value = match cur_type {
Ty::U64 => quote!(occurrences_of(stringify!(#name))), Ty::Bool => quote!(matches.is_present(stringify!(#name))),
Ty::Option => quote! { Ty::Option => quote! {
#value_of(stringify!(#name)) matches.#value_of(stringify!(#name))
.as_ref() .as_ref()
.map(#parse) .map(#parse)
}, },
Ty::Vec => quote! { Ty::Vec => quote! {
#values_of(stringify!(#name)) matches.#values_of(stringify!(#name))
.map(|v| v.map(#parse).collect()) .map(|v| v.map(#parse).collect())
.unwrap_or_else(Vec::new) .unwrap_or_else(Vec::new)
}, },
Ty::Other if occurences => quote! {
#parse(matches.#value_of(stringify!(#name)))
},
Ty::Other => quote! { Ty::Other => quote! {
#value_of(stringify!(#name)) matches.#value_of(stringify!(#name))
.map(#parse) .map(#parse)
.unwrap() .unwrap()
}, },
}; };
quote!( #field_name: matches.#convert )
quote!( #field_name: #field_value )
} }
}); });

View file

@ -157,3 +157,45 @@ fn test_parser_with_default_value() {
])) ]))
); );
} }
#[derive(PartialEq, Debug)]
struct Foo(u8);
fn foo(value: u64) -> Foo {
Foo(value as u8)
}
#[derive(StructOpt, PartialEq, Debug)]
struct Occurrences {
#[structopt(short = "s", long = "signed", parse(from_occurrences))]
signed: i32,
#[structopt(short = "l", parse(from_occurrences))]
little_signed: i8,
#[structopt(short = "u", parse(from_occurrences))]
unsigned: usize,
#[structopt(short = "r", parse(from_occurrences))]
little_unsigned: u8,
#[structopt(short = "c", long = "custom", parse(from_occurrences = "foo"))]
custom: Foo,
}
#[test]
fn test_parser_occurrences() {
assert_eq!(
Occurrences {
signed: 3,
little_signed: 1,
unsigned: 0,
little_unsigned: 4,
custom: Foo(5),
},
Occurrences::from_clap(Occurrences::clap().get_matches_from(&[
"test", "-s", "--signed", "--signed", "-l", "-rrrr", "-cccc", "--custom"
]))
);
}

View file

@ -35,20 +35,22 @@ fn unique_flag() {
fn multiple_flag() { fn multiple_flag() {
#[derive(StructOpt, PartialEq, Debug)] #[derive(StructOpt, PartialEq, Debug)]
struct Opt { struct Opt {
#[structopt(short = "a", long = "alice")] #[structopt(short = "a", long = "alice", parse(from_occurrences))]
alice: u64, alice: u64,
#[structopt(short = "b", long = "bob", parse(from_occurrences))]
bob: u8,
} }
assert_eq!(Opt { alice: 0 }, assert_eq!(Opt { alice: 0, bob: 0 },
Opt::from_clap(Opt::clap().get_matches_from(&["test"]))); Opt::from_clap(Opt::clap().get_matches_from(&["test"])));
assert_eq!(Opt { alice: 1 }, assert_eq!(Opt { alice: 1, bob: 0 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a"]))); Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a"])));
assert_eq!(Opt { alice: 2 }, assert_eq!(Opt { alice: 2, bob: 0 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a", "-a"]))); Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a", "-a"])));
assert_eq!(Opt { alice: 2 }, assert_eq!(Opt { alice: 2, bob: 2 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a", "--alice"]))); Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a", "--alice", "-bb"])));
assert_eq!(Opt { alice: 3 }, assert_eq!(Opt { alice: 3, bob: 1 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-aaa"]))); Opt::from_clap(Opt::clap().get_matches_from(&["test", "-aaa", "--bob"])));
assert!(Opt::clap().get_matches_from_safe(&["test", "-i"]).is_err()); assert!(Opt::clap().get_matches_from_safe(&["test", "-i"]).is_err());
assert!(Opt::clap().get_matches_from_safe(&["test", "-a", "foo"]).is_err()); assert!(Opt::clap().get_matches_from_safe(&["test", "-a", "foo"]).is_err());
} }
@ -59,7 +61,7 @@ fn combined_flags() {
struct Opt { struct Opt {
#[structopt(short = "a", long = "alice")] #[structopt(short = "a", long = "alice")]
alice: bool, alice: bool,
#[structopt(short = "b", long = "bob")] #[structopt(short = "b", long = "bob", parse(from_occurrences))]
bob: u64, bob: u64,
} }

View file

@ -14,7 +14,7 @@ use structopt::StructOpt;
struct Opt { struct Opt {
#[structopt(short = "f", long = "force")] #[structopt(short = "f", long = "force")]
force: bool, force: bool,
#[structopt(short = "v", long = "verbose")] #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u64, verbose: u64,
#[structopt(subcommand)] #[structopt(subcommand)]
cmd: Sub cmd: Sub
@ -32,7 +32,7 @@ enum Sub {
struct Opt2 { struct Opt2 {
#[structopt(short = "f", long = "force")] #[structopt(short = "f", long = "force")]
force: bool, force: bool,
#[structopt(short = "v", long = "verbose")] #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u64, verbose: u64,
#[structopt(subcommand)] #[structopt(subcommand)]
cmd: Option<Sub> cmd: Option<Sub>