-
Notifications
You must be signed in to change notification settings - Fork 150
Implement custom string parser from either &str or &OsStr.
#28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,8 +63,9 @@ | |
| //! | ||
| //! The `FromStr` trait is used to convert the argument to the given | ||
| //! type, and the `Arg::validator` method is set to a method using | ||
| //! `FromStr::Err::description()` (`FromStr::Err` must implement | ||
| //! `std::Error::Error`). | ||
| //! `to_string()` (`FromStr::Err` must implement `std::fmt::Display`). | ||
| //! If you would like to use a custom string parser other than `FromStr`, see | ||
| //! the [same titled section](#custom-string-parsers) below. | ||
| //! | ||
| //! Thus, the `speed` argument is generated as: | ||
| //! | ||
|
|
@@ -227,6 +228,52 @@ | |
| //! Quux | ||
| //! } | ||
| //! ``` | ||
| //! | ||
| //! ## Custom string parsers | ||
| //! | ||
| //! If the field type does not have a `FromStr` implementation, or you would | ||
| //! like to provide a custom parsing scheme other than `FromStr`, you may | ||
| //! provide a custom string parser using `parse(...)` like this: | ||
| //! | ||
| //! ```ignore | ||
| //! use std::num::ParseIntError; | ||
| //! use std::path::PathBuf; | ||
| //! | ||
| //! fn parse_hex(src: &str) -> Result<u32, ParseIntError> { | ||
| //! u32::from_str_radix(src, 16) | ||
| //! } | ||
| //! | ||
| //! #[derive(StructOpt)] | ||
| //! struct HexReader { | ||
| //! #[structopt(short = "n", parse(try_from_str = "parse_hex"))] | ||
| //! number: u32, | ||
| //! #[structopt(short = "o", parse(from_os_str))] | ||
| //! output: PathBuf, | ||
| //! } | ||
| //! ``` | ||
| //! | ||
| //! There are four kinds custom string 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) | | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe say somewhere what we do with the error (call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tricky thing is that fn validator_os<F>(self, f: F) -> Self
where
F: Fn(&OsStr) -> Result<(), OsString> + 'static, Maybe it's fine to ignore the lossy conversion from
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK then, better to not lose things. |
||
| //! | ||
| //! When supplying a custom string parser, `bool` and `u64` will not be treated | ||
| //! specially: | ||
| //! | ||
| //! Type | Effect | Added method call to `clap::Arg` | ||
| //! ------------|-------------------|-------------------------------------- | ||
| //! `Option<T>` | optional argument | `.takes_value(true).multiple(false)` | ||
| //! `Vec<T>` | list of arguments | `.takes_value(true).multiple(true)` | ||
| //! `T` | required argument | `.takes_value(true).multiple(false).required(!has_default)` | ||
| //! | ||
| //! In the `try_from_*` variants, the function will run twice on valid input: | ||
| //! once to validate, and once to parse. Hence, make sure the function is | ||
| //! side-effect-free. | ||
|
|
||
|
|
||
| extern crate proc_macro; | ||
| extern crate syn; | ||
|
|
@@ -286,6 +333,19 @@ fn sub_type(t: &syn::Ty) -> Option<&syn::Ty> { | |
| #[derive(Debug, Clone, Copy)] | ||
| enum AttrSource { Struct, Field, } | ||
|
|
||
| #[derive(Debug)] | ||
| enum Parser { | ||
| /// Parse an option to using a `fn(&str) -> T` function. The function should never fail. | ||
| FromStr, | ||
| /// Parse an option to using a `fn(&str) -> Result<T, E>` function. The error will be | ||
| /// converted to a string using `.to_string()`. | ||
| TryFromStr, | ||
| /// Parse an option to using a `fn(&OsStr) -> T` function. The function should never fail. | ||
| FromOsStr, | ||
| /// Parse an option to using a `fn(&OsStr) -> Result<T, OsString>` function. | ||
| TryFromOsStr, | ||
| } | ||
|
|
||
| fn extract_attrs<'a>(attrs: &'a [Attribute], attr_source: AttrSource) -> Box<Iterator<Item = (Ident, Lit)> + 'a> { | ||
| let settings_attrs = attrs.iter() | ||
| .filter_map(|attr| match attr.value { | ||
|
|
@@ -355,6 +415,71 @@ fn is_subcommand(field: &Field) -> bool { | |
| }) | ||
| } | ||
|
|
||
| fn get_default_parser() -> (Parser, quote::Tokens) { | ||
| (Parser::TryFromStr, quote!(::std::str::FromStr::from_str)) | ||
| } | ||
|
|
||
| fn get_parser(field: &Field) -> Option<(Parser, quote::Tokens)> { | ||
| field.attrs.iter() | ||
| .flat_map(|attr| { | ||
| if let MetaItem::List(ref i, ref l) = attr.value { | ||
| if i == "structopt" { | ||
| return &**l; | ||
| } | ||
| } | ||
| &[] | ||
| }) | ||
| .filter_map(|attr| { | ||
| if let NestedMetaItem::MetaItem(MetaItem::List(ref i, ref l)) = *attr { | ||
| if i == "parse" { | ||
| return l.first(); | ||
| } | ||
| } | ||
| None | ||
| }) | ||
| .map(|attr| { | ||
| 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); | ||
| }; | ||
| (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); | ||
| } | ||
| } | ||
| _ => panic!("unknown value parser specification"), | ||
| } | ||
| }) | ||
| .next() | ||
| } | ||
|
|
||
| fn convert_with_custom_parse(cur_type: Ty) -> Ty { | ||
| match cur_type { | ||
| Ty::Bool | Ty::U64 => Ty::Other, | ||
| rest => rest, | ||
| } | ||
| } | ||
|
|
||
| /// 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 { | ||
|
|
@@ -377,27 +502,41 @@ fn gen_augmentation(fields: &[Field], app_var: &Ident) -> quote::Tokens { | |
| .filter(|&field| !is_subcommand(field)) | ||
| .map(|field| { | ||
| let name = gen_name(field); | ||
| let cur_type = ty(&field.ty); | ||
| let mut 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 parser = get_parser(field); | ||
| if parser.is_some() { | ||
| cur_type = convert_with_custom_parse(cur_type); | ||
| } | ||
| let validator = match parser.unwrap_or_else(get_default_parser) { | ||
| (Parser::TryFromStr, f) => quote! { | ||
| .validator(|s| { | ||
| #f(&s) | ||
| .map(|_: #convert_type| ()) | ||
| .map_err(|e| e.to_string()) | ||
| }) | ||
| }, | ||
| (Parser::TryFromOsStr, f) => quote! { | ||
| .validator_os(|s| #f(&s).map(|_: #convert_type| ())) | ||
| }, | ||
| _ => quote! {}, | ||
| }; | ||
|
|
||
| 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::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" | ||
| || i.as_ref() == "default_value_raw") | ||
| .is_none(); | ||
| quote!( .takes_value(true).multiple(false).required(#required).#validator ) | ||
| quote!( .takes_value(true).multiple(false).required(#required) #validator ) | ||
| }, | ||
| }; | ||
| let from_attr = extract_attrs(&field.attrs, AttrSource::Field) | ||
|
|
@@ -445,24 +584,51 @@ fn gen_constructor(fields: &[Field]) -> quote::Tokens { | |
| }; | ||
| quote!( #field_name: #subcmd_type ::from_subcommand(matches.subcommand()) #unwrapper ) | ||
| } else { | ||
| let convert = match ty(&field.ty) { | ||
| let mut cur_type = ty(&field.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) { | ||
| (Parser::FromStr, f) => ( | ||
| quote!(value_of), | ||
| quote!(values_of), | ||
| f, | ||
| ), | ||
| (Parser::TryFromStr, f) => ( | ||
| quote!(value_of), | ||
| quote!(values_of), | ||
| quote!(|s| #f(s).unwrap()), | ||
| ), | ||
| (Parser::FromOsStr, f) => ( | ||
| quote!(value_of_os), | ||
| quote!(values_of_os), | ||
| f, | ||
| ), | ||
| (Parser::TryFromOsStr, f) => ( | ||
| quote!(value_of_os), | ||
| quote!(values_of_os), | ||
| quote!(|s| #f(s).unwrap()), | ||
| ), | ||
| }; | ||
|
|
||
| let convert = match cur_type { | ||
| Ty::Bool => quote!(is_present(stringify!(#name))), | ||
| Ty::U64 => quote!(occurrences_of(stringify!(#name))), | ||
| Ty::Option => quote! { | ||
| value_of(stringify!(#name)) | ||
| #value_of(stringify!(#name)) | ||
| .as_ref() | ||
| .map(|s| s.parse().unwrap()) | ||
| .map(#parse) | ||
| }, | ||
| Ty::Vec => quote! { | ||
| values_of(stringify!(#name)) | ||
| .map(|v| v.map(|s| s.parse().unwrap()).collect()) | ||
| #values_of(stringify!(#name)) | ||
| .map(|v| v.map(#parse).collect()) | ||
| .unwrap_or_else(Vec::new) | ||
| }, | ||
| Ty::Other => quote! { | ||
| value_of(stringify!(#name)) | ||
| .as_ref() | ||
| .unwrap() | ||
| .parse() | ||
| #value_of(stringify!(#name)) | ||
| .map(#parse) | ||
| .unwrap() | ||
| }, | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a bit confusing that
::std::str::FromStr::from_stris the default fortry_from_strand notfrom_strbut I can't find a better idea.