Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ struct Opt {
#[structopt(short = "d", long = "debug")]
debug: bool,

/// Verbose mode
#[structopt(short = "v", long = "verbose")]
/// Verbose mode (-v, -vv, -vvv, etc.)
#[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u64,

/// Set speed
Expand Down
102 changes: 59 additions & 43 deletions structopt-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@
//! extern crate structopt;
//! #[macro_use]
//! extern crate structopt_derive;
//!
//!
//! use structopt::StructOpt;
//!
//!
//! #[derive(StructOpt, Debug)]
//! #[structopt(setting_raw = "clap::AppSettings::ColoredHelp")]
//! struct Opt {
Expand All @@ -61,7 +61,7 @@
//! #[structopt(short = "d")]
//! debug: bool,
//! }
//!
//!
//! fn main() {
//! let opt = Opt::from_args();
//! println!("{:?}", opt);
Expand All @@ -82,7 +82,6 @@
//! Type | Effect | Added method call to `clap::Arg`
//! ---------------------|---------------------------------------------------|--------------------------------------
//! `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)`
//! `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)`
Expand Down Expand Up @@ -197,7 +196,7 @@
//! #[structopt(name = "sparkle")]
//! /// Add magical sparkles -- the secret ingredient!
//! Sparkle {
//! #[structopt(short = "m")]
//! #[structopt(short = "m", parse(from_occurrences))]
//! magicality: u64,
//! #[structopt(short = "c")]
//! color: String
Expand Down Expand Up @@ -278,17 +277,24 @@
//! }
//! ```
//!
//! There are four kinds custom string parsers:
//! There are five kinds of custom parsers:
//!
//! | Kind | Signature | Default |
//! |-------------------|---------------------------------------|---------------------------------|
//! | `from_str` | `fn(&str) -> T` | `::std::convert::From::from` |
//! | `try_from_str` | `fn(&str) -> Result<T, E>` | `::std::str::FromStr::from_str` |
//! | `from_os_str` | `fn(&OsStr) -> T` | `::std::convert::From::from` |
//! | `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
//! specially:
//! The `from_occurrences` parser is special. Using `parse(from_occurrences)`
//! 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`
//! ------------|-------------------|--------------------------------------
Expand Down Expand Up @@ -321,7 +327,6 @@ pub fn structopt(input: TokenStream) -> TokenStream {
#[derive(Copy, Clone, PartialEq)]
enum Ty {
Bool,
U64,
Vec,
Option,
Other,
Expand All @@ -331,7 +336,6 @@ fn ty(t: &syn::Ty) -> Ty {
if let syn::Ty::Path(None, syn::Path { segments: ref segs, .. }) = *t {
match segs.last().unwrap().ident.as_ref() {
"bool" => Ty::Bool,
"u64" => Ty::U64,
"Option" => Ty::Option,
"Vec" => Ty::Vec,
_ => Ty::Other,
Expand Down Expand Up @@ -359,7 +363,7 @@ fn sub_type(t: &syn::Ty) -> Option<&syn::Ty> {
#[derive(Debug, Clone, Copy)]
enum AttrSource { Struct, Field, }

#[derive(Debug)]
#[derive(Debug, PartialEq)]
enum Parser {
/// Parse an option to using a `fn(&str) -> T` function. The function should never fail.
FromStr,
Expand All @@ -370,6 +374,9 @@ enum Parser {
FromOsStr,
/// Parse an option to using a `fn(&OsStr) -> Result<T, OsString>` function.
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> {
Expand Down Expand Up @@ -470,30 +477,25 @@ fn get_parser(field: &Field) -> Option<(Parser, quote::Tokens)> {
match *attr {
NestedMetaItem::MetaItem(MetaItem::NameValue(ref i, Lit::Str(ref v, _))) => {
let function = parse_path(v).expect("parser function path");
let parser = if i == "from_str" {
Parser::FromStr
} else if i == "try_from_str" {
Parser::TryFromStr
} else if i == "from_os_str" {
Parser::FromOsStr
} else if i == "try_from_os_str" {
Parser::TryFromOsStr
} else {
panic!("unsupported parser {}", i);
let parser = match i.as_ref() {
"from_str" => Parser::FromStr,
"try_from_str" => Parser::TryFromStr,
"from_os_str" => Parser::FromOsStr,
"try_from_os_str" => Parser::TryFromOsStr,
"from_occurrences" => Parser::FromOccurrences,
_ => panic!("unsupported parser {}", i)
};

(parser, quote!(#function))
}
NestedMetaItem::MetaItem(MetaItem::Word(ref i)) => {
if i == "from_str" {
(Parser::FromStr, quote!(::std::convert::From::from))
} else if i == "try_from_str" {
(Parser::TryFromStr, quote!(::std::str::FromStr::from_str))
} else if i == "from_os_str" {
(Parser::FromOsStr, quote!(::std::convert::From::from))
} else if i == "try_from_os_str" {
panic!("cannot omit parser function name with `try_from_os_str`")
} else {
panic!("unsupported parser {}", i);
match i.as_ref() {
"from_str" => (Parser::FromStr, quote!(::std::convert::From::from)),
"try_from_str" => (Parser::TryFromStr, quote!(::std::str::FromStr::from_str)),
"from_os_str" => (Parser::FromOsStr, quote!(::std::convert::From::from)),
"try_from_os_str" => panic!("cannot omit parser function name with `try_from_os_str`"),
"from_occurrences" => (Parser::FromOccurrences, quote!({|v| v as _})),
_ => panic!("unsupported parser {}", i)
}
}
_ => panic!("unknown value parser specification"),
Expand All @@ -504,7 +506,7 @@ fn get_parser(field: &Field) -> Option<(Parser, quote::Tokens)> {

fn convert_with_custom_parse(cur_type: Ty) -> Ty {
match cur_type {
Ty::Bool | Ty::U64 => Ty::Other,
Ty::Bool => Ty::Other,
rest => rest,
}
}
Expand Down Expand Up @@ -545,10 +547,13 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens {
_ => &field.ty,
};

let mut occurences = false;
let parser = get_parser(field);
if parser.is_some() {
if let Some((ref parser, _)) = parser {
cur_type = convert_with_custom_parse(cur_type);
occurences = *parser == Parser::FromOccurrences;
}

let validator = match parser.unwrap_or_else(get_default_parser) {
(Parser::TryFromStr, f) => quote! {
.validator(|s| {
Expand All @@ -565,9 +570,9 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens {

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 if occurences => quote!( .takes_value(false).multiple(true) ),
Ty::Other => {
let required = extract_attrs(&field.attrs, AttrSource::Field)
.find(|&(ref i, _)| i.as_ref() == "default_value"
Expand Down Expand Up @@ -621,13 +626,15 @@ fn gen_constructor(fields: &[Field]) -> quote::Tokens {
};
quote!( #field_name: #subcmd_type ::from_subcommand(matches.subcommand()) #unwrapper )
} 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);
if parser.is_some() {
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) => (
quote!(value_of),
quote!(values_of),
Expand All @@ -648,28 +655,37 @@ fn gen_constructor(fields: &[Field]) -> quote::Tokens {
quote!(values_of_os),
quote!(|s| #f(s).unwrap()),
),
(Parser::FromOccurrences, f) => (
quote!(occurrences_of),
quote!(),
f,
),
};

let convert = match cur_type {
Ty::Bool => quote!(is_present(stringify!(#name))),
Ty::U64 => quote!(occurrences_of(stringify!(#name))),
let occurences = parser.0 == Parser::FromOccurrences;
let field_value = match cur_type {
Ty::Bool => quote!(matches.is_present(stringify!(#name))),
Ty::Option => quote! {
#value_of(stringify!(#name))
matches.#value_of(stringify!(#name))
.as_ref()
.map(#parse)
},
Ty::Vec => quote! {
#values_of(stringify!(#name))
matches.#values_of(stringify!(#name))
.map(|v| v.map(#parse).collect())
.unwrap_or_else(Vec::new)
},
Ty::Other if occurences => quote! {
#parse(matches.#value_of(stringify!(#name)))
},
Ty::Other => quote! {
#value_of(stringify!(#name))
matches.#value_of(stringify!(#name))
.map(#parse)
.unwrap()
},
};
quote!( #field_name: matches.#convert )

quote!( #field_name: #field_value )
}
});

Expand Down
42 changes: 42 additions & 0 deletions tests/custom-string-parsers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]))
);
}
20 changes: 11 additions & 9 deletions tests/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ fn unique_flag() {
fn multiple_flag() {
#[derive(StructOpt, PartialEq, Debug)]
struct Opt {
#[structopt(short = "a", long = "alice")]
#[structopt(short = "a", long = "alice", parse(from_occurrences))]
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"])));
assert_eq!(Opt { alice: 1 },
assert_eq!(Opt { alice: 1, bob: 0 },
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"])));
assert_eq!(Opt { alice: 2 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a", "--alice"])));
assert_eq!(Opt { alice: 3 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-aaa"])));
assert_eq!(Opt { alice: 2, bob: 2 },
Opt::from_clap(Opt::clap().get_matches_from(&["test", "-a", "--alice", "-bb"])));
assert_eq!(Opt { alice: 3, bob: 1 },
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", "-a", "foo"]).is_err());
}
Expand All @@ -59,7 +61,7 @@ fn combined_flags() {
struct Opt {
#[structopt(short = "a", long = "alice")]
alice: bool,
#[structopt(short = "b", long = "bob")]
#[structopt(short = "b", long = "bob", parse(from_occurrences))]
bob: u64,
}

Expand Down
4 changes: 2 additions & 2 deletions tests/nested-subcommands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use structopt::StructOpt;
struct Opt {
#[structopt(short = "f", long = "force")]
force: bool,
#[structopt(short = "v", long = "verbose")]
#[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u64,
#[structopt(subcommand)]
cmd: Sub
Expand All @@ -32,7 +32,7 @@ enum Sub {
struct Opt2 {
#[structopt(short = "f", long = "force")]
force: bool,
#[structopt(short = "v", long = "verbose")]
#[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u64,
#[structopt(subcommand)]
cmd: Option<Sub>
Expand Down