From 7bf392885629e2c4eda25894933a69f0d4fcad22 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 15 Jan 2021 16:11:04 -0800 Subject: [PATCH] feat: Auto-detect `FromStr`, `TryFrom<&OsStr>`, etc., in clap_derive Clap has several different options for parsing strings into values: `try_from_str`, `try_from_os_str`, `from_str`, `from_os_str`, each corresponding to a different way of parsing strings. This patch adds a new option: `auto`, which auto-detects which parsing traits a type supports, out of `clap::ArgEnum`, `TryFrom<&OsStr>`, `FromStr`, `TryFrom<&str>`, `From<&OsStr>`, and `From<&str>`, and selects the best one to use automatically. This way, users can use any type which implements any of these traits, and clap_derive automatically figures out what to do. It's implemented via [autoref specialization], a limited form of specialization which only works in macro contexts, but that turns out to be enough for clap_derive. This changes the default to `auto`. The previous default `try_from_str` uses the `FromStr` trait. `auto` auto-detects `FromStr`, so this is a mostly backwards-compatible change. [autoref specialization]: http://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html --- README.md | 2 + clap_derive/Cargo.toml | 1 + clap_derive/README.md | 4 +- clap_derive/examples/basic.rs | 4 +- clap_derive/examples/value_hints_derive.rs | 2 +- clap_derive/src/attrs.rs | 61 ++- clap_derive/src/derives/from_arg_matches.rs | 197 ++++++++-- clap_derive/src/derives/into_app.rs | 2 +- clap_derive/src/utils/ty.rs | 26 +- clap_derive/tests/auto.rs | 389 ++++++++++++++++++++ src/parse/matches/arg_matches.rs | 122 ++++++ 11 files changed, 722 insertions(+), 88 deletions(-) create mode 100644 clap_derive/tests/auto.rs diff --git a/README.md b/README.md index ed1df90f763..128b75012b0 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,8 @@ clap = "3.0.0-beta.2" The first example shows the simplest way to use `clap`, by defining a struct. If you're familiar with the `structopt` crate you're in luck, it's the same! (In fact it's the exact same code running under the covers!) +Clap introduces the additional convenience of auto-detecting whether a type implements `FromStr`, `From<&OsStr>`, `TryFrom<&OsStr>` and other standard parsing traits, so in many cases it's no longer necessary to use attributes such as `parse(try_from_str)`, `parse(from_os_str)`, `parse(try_from_os_str)`, and similar. + ```rust,no_run // (Full example with detailed comments in examples/01d_quick_example.rs) // diff --git a/clap_derive/Cargo.toml b/clap_derive/Cargo.toml index 84ad9663f63..56b93a82682 100644 --- a/clap_derive/Cargo.toml +++ b/clap_derive/Cargo.toml @@ -45,6 +45,7 @@ proc-macro-error = "1" [dev-dependencies] clap = { path = "../" } clap_generate = { path = "../clap_generate" } +os_str_bytes = { version = "2.4" } trybuild = "1.0" rustversion = "1" version-sync = "0.8" diff --git a/clap_derive/README.md b/clap_derive/README.md index ff7510c336a..1b406b3f41e 100644 --- a/clap_derive/README.md +++ b/clap_derive/README.md @@ -42,7 +42,7 @@ struct Opt { speed: f64, /// Output file - #[clap(short, long, parse(from_os_str), value_hint = ValueHint::FilePath)] + #[clap(short, long, value_hint = ValueHint::FilePath)] output: PathBuf, // the long option will be translated by default to kebab case, @@ -56,7 +56,7 @@ struct Opt { level: Vec, /// Files to process - #[clap(name = "FILE", parse(from_os_str), value_hint = ValueHint::AnyPath)] + #[clap(name = "FILE", value_hint = ValueHint::AnyPath)] files: Vec, } diff --git a/clap_derive/examples/basic.rs b/clap_derive/examples/basic.rs index 4a96e90f899..59869f037b9 100644 --- a/clap_derive/examples/basic.rs +++ b/clap_derive/examples/basic.rs @@ -24,7 +24,7 @@ struct Opt { speed: f64, /// Output file - #[clap(short, long, parse(from_os_str), value_hint = ValueHint::FilePath)] + #[clap(short, long, value_hint = ValueHint::FilePath)] output: PathBuf, // the long option will be translated by default to kebab case, @@ -38,7 +38,7 @@ struct Opt { level: Vec, /// Files to process - #[clap(name = "FILE", parse(from_os_str), value_hint = ValueHint::AnyPath)] + #[clap(name = "FILE", value_hint = ValueHint::AnyPath)] files: Vec, } diff --git a/clap_derive/examples/value_hints_derive.rs b/clap_derive/examples/value_hints_derive.rs index 859411a1e64..2b34338b3c5 100644 --- a/clap_derive/examples/value_hints_derive.rs +++ b/clap_derive/examples/value_hints_derive.rs @@ -53,7 +53,7 @@ struct Opt { dir: Option, #[clap(short, long, value_hint = ValueHint::ExecutablePath)] exe: Option, - #[clap(long, parse(from_os_str), value_hint = ValueHint::CommandName)] + #[clap(long, value_hint = ValueHint::CommandName)] cmd_name: Option, #[clap(short, long, value_hint = ValueHint::CommandString)] cmd: Option, diff --git a/clap_derive/src/attrs.rs b/clap_derive/src/attrs.rs index e570211da5f..1eb186e049b 100644 --- a/clap_derive/src/attrs.rs +++ b/clap_derive/src/attrs.rs @@ -37,7 +37,7 @@ pub const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake; #[allow(clippy::large_enum_variant)] #[derive(Clone)] pub enum Kind { - Arg(Sp), + Arg(Sp, TokenStream), Subcommand(Sp), Flatten, Skip(Option), @@ -53,11 +53,12 @@ pub struct Method { #[derive(Clone)] pub struct Parser { pub kind: Sp, - pub func: TokenStream, + pub parse_func: Option, } #[derive(Debug, PartialEq, Clone)] pub enum ParserKind { + Auto, FromStr, TryFromStr, FromOsStr, @@ -155,15 +156,16 @@ impl ToTokens for Method { impl Parser { fn default_spanned(span: Span) -> Sp { - let kind = Sp::new(ParserKind::TryFromStr, span); - let func = quote_spanned!(span=> ::std::str::FromStr::from_str); - Sp::new(Parser { kind, func }, span) + let kind = Sp::new(ParserKind::Auto, span); + let parse_func = None; + Sp::new(Parser { kind, parse_func }, span) } fn from_spec(parse_ident: Ident, spec: ParserSpec) -> Sp { use self::ParserKind::*; let kind = match &*spec.kind.to_string() { + "auto" => Auto, "from_str" => FromStr, "try_from_str" => TryFromStr, "from_os_str" => FromOsStr, @@ -173,28 +175,11 @@ impl Parser { s => abort!(spec.kind.span(), "unsupported parser `{}`", s), }; - let func = match spec.parse_func { - None => match kind { - FromStr | FromOsStr => { - quote_spanned!(spec.kind.span()=> ::std::convert::From::from) - } - TryFromStr => quote_spanned!(spec.kind.span()=> ::std::str::FromStr::from_str), - TryFromOsStr => abort!( - spec.kind.span(), - "you must set parser for `try_from_os_str` explicitly" - ), - FromOccurrences => quote_spanned!(spec.kind.span()=> { |v| v as _ }), - FromFlag => quote_spanned!(spec.kind.span()=> ::std::convert::From::from), - }, - - Some(func) => match func { - Expr::Path(_) => quote!(#func), - _ => abort!(func, "`parse` argument must be a function path"), - }, - }; - let kind = Sp::new(kind, spec.kind.span()); - let parser = Parser { kind, func }; + let parser = Parser { + kind, + parse_func: spec.parse_func, + }; Sp::new(parser, parse_ident.span()) } } @@ -283,7 +268,10 @@ impl Attrs { verbatim_doc_comment: None, is_enum: false, has_custom_parser: false, - kind: Sp::new(Kind::Arg(Sp::new(Ty::Other, default_span)), default_span), + kind: Sp::new( + Kind::Arg(Sp::new(Ty::Other, default_span), TokenStream::new()), + default_span, + ), } } @@ -453,7 +441,7 @@ impl Attrs { match &*res.kind { Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"), Kind::Skip(_) => abort!(res.kind.span(), "skip is only allowed on fields"), - Kind::Arg(_) | Kind::Flatten | Kind::ExternalSubcommand => res, + Kind::Arg(_, _) | Kind::Flatten | Kind::ExternalSubcommand => res, } } @@ -512,7 +500,7 @@ impl Attrs { ); } - let ty = Ty::from_syn_ty(&field.ty); + let (ty, _inner) = Ty::from_syn_ty(&field.ty); match *ty { Ty::OptionOption => { abort!( @@ -539,8 +527,9 @@ impl Attrs { ); } } - Kind::Arg(orig_ty) => { - let mut ty = Ty::from_syn_ty(&field.ty); + Kind::Arg(orig_ty, inner) => { + assert!(inner.is_empty()); + let (mut ty, inner) = Ty::from_syn_ty(&field.ty); if res.has_custom_parser { match *ty { Ty::Option | Ty::Vec | Ty::OptionVec => (), @@ -596,7 +585,13 @@ impl Attrs { _ => (), } - res.kind = Sp::new(Kind::Arg(ty), orig_ty.span()); + + // Serialize the `inner` type (eg. the `T` in `Vec`) into + // tokens so that we can include it in macro expansions. + let mut inner_tokens = TokenStream::new(); + inner.to_tokens(&mut inner_tokens); + + res.kind = Sp::new(Kind::Arg(ty, inner_tokens), orig_ty.span()); } } @@ -604,7 +599,7 @@ impl Attrs { } fn set_kind(&mut self, kind: Sp) { - if let Kind::Arg(_) = *self.kind { + if let Kind::Arg(_, _) = *self.kind { self.kind = kind; } else { abort!( diff --git a/clap_derive/src/derives/from_arg_matches.rs b/clap_derive/src/derives/from_arg_matches.rs index 7aea931e5d2..4a70af7eed2 100644 --- a/clap_derive/src/derives/from_arg_matches.rs +++ b/clap_derive/src/derives/from_arg_matches.rs @@ -11,14 +11,14 @@ // This work was derived from Structopt (https://github.com/TeXitoi/structopt) // commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the // MIT/Apache 2.0 license. -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; use quote::{quote, quote_spanned}; -use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, Field, Ident, Type}; +use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, Expr, Field, Ident}; use crate::{ attrs::{Attrs, Kind, ParserKind}, - utils::{sub_type, subty_if_name, Sp, Ty}, + utils::{sub_type, Sp, Ty}, }; pub fn gen_for_struct( @@ -86,17 +86,10 @@ pub fn gen_for_enum(name: &Ident) -> TokenStream { } } -fn gen_arg_enum_parse(ty: &Type, attrs: &Attrs) -> TokenStream { - let ci = attrs.case_insensitive(); - - quote_spanned! { ty.span()=> - |s| <#ty as clap::ArgEnum>::from_str(s, #ci) - } -} - fn gen_parsers( attrs: &Attrs, ty: &Sp, + inner: &TokenStream, field_name: &Ident, field: &Field, update: Option<&TokenStream>, @@ -104,19 +97,50 @@ fn gen_parsers( use self::ParserKind::*; let parser = attrs.parser(); - let func = &parser.func; + let parse_func = &parser.parse_func; let span = parser.kind.span(); // The operand type of the `parse` function. let parse_operand_type = match *parser.kind { FromStr | TryFromStr => quote_spanned!(ty.span()=> &str), - FromOsStr | TryFromOsStr => quote_spanned!(ty.span()=> &::std::ffi::OsStr), + Auto | FromOsStr | TryFromOsStr => quote_spanned!(ty.span()=> &::std::ffi::OsStr), FromOccurrences => quote_spanned!(ty.span()=> u64), FromFlag => quote_spanned!(ty.span()=> bool), }; // Wrap `parse` in a closure so that we can give the operand a concrete type. - let mut parse = quote_spanned!(span=> |s: #parse_operand_type| #func(s)); + let parse = if let Auto = *parser.kind { + if parse_func.is_some() { + abort!( + parser.kind.span(), + "`auto` may not be used with a custom parsing function" + ); + } + gen_auto_parser(&parse_operand_type, inner, attrs, span) + } else { + let func = match parse_func { + None => match *parser.kind { + Auto => panic!(), // Handled above. + FromStr | FromOsStr => { + quote_spanned!(parser.kind.span()=> ::std::convert::From::from) + } + TryFromStr => quote_spanned!(parser.kind.span()=> ::std::str::FromStr::from_str), + TryFromOsStr => abort!( + parser.kind.span(), + "you must set parser for `try_from_os_str` explicitly" + ), + FromOccurrences => quote_spanned!(parser.kind.span()=> { |v| v as _ }), + FromFlag => quote_spanned!(parser.kind.span()=> ::std::convert::From::from), + }, + + Some(func) => match func { + Expr::Path(_) => quote!(#func), + _ => abort!(func, "`parse` argument must be a function path"), + }, + }; + + quote_spanned!(span=> |s: #parse_operand_type| #func(s)) + }; let flag = *attrs.parser().kind == ParserKind::FromFlag; let occurrences = *attrs.parser().kind == ParserKind::FromOccurrences; @@ -126,26 +150,11 @@ fn gen_parsers( // allows us to refer to `arg_matches` within a `quote_spanned` block let arg_matches = quote! { arg_matches }; - if attrs.is_enum() { - match **ty { - Ty::Option => { - if let Some(subty) = subty_if_name(&field.ty, "Option") { - parse = gen_arg_enum_parse(subty, &attrs); - } - } - Ty::Vec => { - if let Some(subty) = subty_if_name(&field.ty, "Vec") { - parse = gen_arg_enum_parse(subty, &attrs); - } - } - Ty::Other => { - parse = gen_arg_enum_parse(&field.ty, &attrs); - } - _ => {} - } - } - let (value_of, values_of) = match *parser.kind { + Auto => ( + quote_spanned!(span=> #arg_matches.parse_optional_t_auto(#name, #parse)?), + quote_spanned!(span=> #arg_matches.parse_optional_vec_t_auto(#name, #parse)?), + ), FromStr => ( quote_spanned!(span=> #arg_matches.value_of(#name).map(#parse)), quote_spanned!(span=> @@ -230,6 +239,124 @@ fn gen_parsers( } } +/// Generate code to auto-detect which parsing trait a type supports and +/// perform parsing using it. +/// +/// Normally, doing this kind of thing would require specialization, which +/// isn't available on stable Rust, however it turns out to be possible to +/// do a limited form of specialization using autoref. This limited form +/// only works in macro-like contexts, but that's what we're in here! +/// +/// For more information on autoref specialization, see this blog post on +/// [generalized autoref-based specialization]. +/// +/// [generalized autoref-based specialization]: http://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html +fn gen_auto_parser( + operand_type: &TokenStream, + result_type: &TokenStream, + attrs: &Attrs, + span: Span, +) -> TokenStream { + let ci = attrs.case_insensitive(); + + quote_spanned!(span=> |s: #operand_type| { + use std::convert::{Infallible, TryFrom}; + use std::ffi::{OsStr, OsString}; + use std::str::FromStr; + use std::marker::PhantomData; + + struct Wrap(T); + trait Specialize7 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl<'a, T: clap::ArgEnum> Specialize7 for &&&&&&&Wrap<(&'a OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + match self.0.0.to_str() { + None => Err(Err(self.0.0.to_os_string())), + Some(s) => T::from_str(s, #ci).map_err(Ok), + } + } + } + trait Specialize6 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl<'a, T: TryFrom<&'a OsStr>> Specialize6 for &&&&&&Wrap<(&'a OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + T::try_from(self.0.0).map_err(Ok) + } + } + trait Specialize5 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl Specialize5 for &&&&&Wrap<(&OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + match self.0.0.to_str() { + None => Err(Err(self.0.0.to_os_string())), + Some(s) => T::from_str(s).map_err(Ok), + } + } + } + trait Specialize4 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl<'a, T: TryFrom<&'a str>> Specialize4 for &&&&Wrap<(&'a OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + match self.0.0.to_str() { + None => Err(Err(self.0.0.to_os_string())), + Some(s) => T::try_from(s).map_err(Ok), + } + } + } + trait Specialize3 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl<'a, T: From<&'a OsStr>> Specialize3 for &&&Wrap<(&'a OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + Ok(T::from(self.0.0)) + } + } + trait Specialize2 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl<'a, T: From<&'a str>> Specialize2 for &&Wrap<(&'a OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + match self.0.0.to_str() { + None => Err(Err(self.0.0.to_os_string())), + Some(s) => Ok(T::from(s)), + } + } + } + trait Specialize1 { + type Return; + fn specialized(&self) -> Self::Return; + } + impl<'a, T> Specialize1 for &Wrap<(&'a OsStr, PhantomData)> { + type Return = Result>; + fn specialized(&self) -> Self::Return { + Err(Ok(format!( + "Type `{}` does not implement any of the parsing traits: \ + `clap::ArgEnum`, `TryFrom<&OsStr>`, `FromStr`, `TryFrom<&str>`, \ + `From<&OsStr>`, or `From<&str>`", + stringify!(#result_type) + ))) + } + } + (&&&&&&&Wrap((s, PhantomData::<#result_type>))).specialized() + }) +} + pub fn gen_constructor(fields: &Punctuated, parent_attribute: &Attrs) -> TokenStream { let fields = fields.iter().map(|field| { let attrs = Attrs::from_field( @@ -277,7 +404,7 @@ pub fn gen_constructor(fields: &Punctuated, parent_attribute: &Att Some(val) => quote_spanned!(kind.span()=> #field_name: (#val).into()), }, - Kind::Arg(ty) => gen_parsers(&attrs, ty, field_name, field, None), + Kind::Arg(ty, inner) => gen_parsers(&attrs, ty, inner, field_name, field, None), } }); @@ -358,7 +485,7 @@ pub fn gen_updater( Kind::Skip(_) => quote!(), - Kind::Arg(ty) => gen_parsers(&attrs, ty, field_name, field, Some(&access)), + Kind::Arg(ty, inner) => gen_parsers(&attrs, ty, &inner, field_name, field, Some(&access)), } }); diff --git a/clap_derive/src/derives/into_app.rs b/clap_derive/src/derives/into_app.rs index db6bb1bfb20..55992d92aa0 100644 --- a/clap_derive/src/derives/into_app.rs +++ b/clap_derive/src/derives/into_app.rs @@ -229,7 +229,7 @@ pub fn gen_app_augmentation( let #app_var = <#ty as clap::IntoApp>::augment_clap(#app_var); }) } - Kind::Arg(ty) => { + Kind::Arg(ty, _inner) => { let parser = attrs.parser(); let occurrences = parser.kind == ParserKind::FromOccurrences; let flag = parser.kind == ParserKind::FromFlag; diff --git a/clap_derive/src/utils/ty.rs b/clap_derive/src/utils/ty.rs index e180758ea86..ab6baa319be 100644 --- a/clap_derive/src/utils/ty.rs +++ b/clap_derive/src/utils/ty.rs @@ -18,24 +18,26 @@ pub enum Ty { } impl Ty { - pub fn from_syn_ty(ty: &syn::Type) -> Sp { + /// Detect whether `ty` is one of our special-cased types above, and if so, + /// also return the inner type (eg. the `T` in `Vec`). + pub fn from_syn_ty(ty: &syn::Type) -> (Sp, &syn::Type) { use self::Ty::*; let t = |kind| Sp::new(kind, ty.span()); if is_simple_ty(ty, "bool") { - t(Bool) - } else if is_generic_ty(ty, "Vec") { - t(Vec) + (t(Bool), ty) + } else if let Some(elt) = subty_if_name(ty, "Vec") { + (t(Vec), elt) } else if let Some(subty) = subty_if_name(ty, "Option") { - if is_generic_ty(subty, "Option") { - t(OptionOption) - } else if is_generic_ty(subty, "Vec") { - t(OptionVec) + if let Some(subsubty) = subty_if_name(subty, "Option") { + (t(OptionOption), subsubty) + } else if let Some(elt) = subty_if_name(subty, "Vec") { + (t(OptionVec), elt) } else { - t(Option) + (t(Option), subty) } } else { - t(Other) + (t(Other), ty) } } } @@ -99,10 +101,6 @@ pub fn is_simple_ty(ty: &syn::Type, name: &str) -> bool { .unwrap_or(false) } -fn is_generic_ty(ty: &syn::Type, name: &str) -> bool { - subty_if_name(ty, name).is_some() -} - fn only_one(mut iter: I) -> Option where I: Iterator, diff --git a/clap_derive/tests/auto.rs b/clap_derive/tests/auto.rs new file mode 100644 index 00000000000..8814d65ed6e --- /dev/null +++ b/clap_derive/tests/auto.rs @@ -0,0 +1,389 @@ +//! Test `parse(auto)`. + +use clap::Clap; +use std::ffi::{OsStr, OsString}; + +// Define structs which implement various traits. +#[derive(Debug, Eq, PartialEq)] +struct HasTryFromOsStr(String); +#[derive(Debug, Eq, PartialEq)] +struct HasTraitFromStr(String); +#[derive(Debug, Eq, PartialEq)] +struct HasTryFromStr(String); +#[derive(Debug, Eq, PartialEq)] +struct HasFromOsStr(String); +#[derive(Debug, Eq, PartialEq)] +struct HasFromStr(String); +#[derive(Debug, Eq, PartialEq)] +struct HasNone(String); + +impl std::convert::TryFrom<&std::ffi::OsStr> for HasTryFromOsStr { + type Error = String; + + fn try_from(s: &std::ffi::OsStr) -> Result { + if format!("{:?}", s).contains("!") { + return Err(format!("failed HasTryFromOsStr on '{:?}'", s)); + } + Ok(HasTryFromOsStr(format!("HasTryFromOsStr({:?})", s))) + } +} + +impl std::str::FromStr for HasTraitFromStr { + type Err = String; + + fn from_str(s: &str) -> Result { + if format!("{:?}", s).contains("!") { + return Err(format!("failed HasTraitFromStr on '{:?}'", s)); + } + Ok(HasTraitFromStr(format!("HasTraitFromStr({:?})", s))) + } +} + +impl std::convert::TryFrom<&str> for HasTryFromStr { + type Error = String; + + fn try_from(s: &str) -> Result { + if format!("{:?}", s).contains("!") { + return Err(format!("failed HasTryFromStr on '{:?}'", s)); + } + Ok(HasTryFromStr(format!("HasTryFromStr({:?})", s))) + } +} + +impl From<&std::ffi::OsStr> for HasFromOsStr { + fn from(s: &std::ffi::OsStr) -> HasFromOsStr { + HasFromOsStr(format!("HasFromOsStr({:?})", s)) + } +} + +impl From<&str> for HasFromStr { + fn from(s: &str) -> HasFromStr { + HasFromStr(format!("HasFromStr({:?})", s)) + } +} + +/// Define a `Clap` type which contains one of each of the above structs. +#[derive(Clap, PartialEq, Debug)] +struct Opt { + #[clap(long, parse(auto))] + has_try_from_os_str: HasTryFromOsStr, + + #[clap(long, parse(auto))] + has_trait_from_str: HasTraitFromStr, + + #[clap(long, parse(auto))] + has_try_from_str: HasTryFromStr, + + #[clap(long, parse(auto))] + has_from_os_str: HasFromOsStr, + + #[clap(long, parse(auto))] + has_from_str: HasFromStr, +} + +/// Test successful parsing with each trait. +#[test] +fn auto_basic() { + assert_eq!( + Opt { + has_try_from_os_str: HasTryFromOsStr("HasTryFromOsStr(\"red\")".to_string()), + has_trait_from_str: HasTraitFromStr("HasTraitFromStr(\"orange\")".to_string()), + has_try_from_str: HasTryFromStr("HasTryFromStr(\"yellow\")".to_string()), + has_from_os_str: HasFromOsStr("HasFromOsStr(\"green\")".to_string()), + has_from_str: HasFromStr("HasFromStr(\"blue\")".to_string()), + }, + Opt::try_parse_from(&[ + "test", + "--has-try-from-os-str", + "red", + "--has-trait-from-str", + "orange", + "--has-try-from-str", + "yellow", + "--has-from-os-str", + "green", + "--has-from-str", + "blue", + ]) + .unwrap() + ); +} + +// Test invalid UTF-8 with each trait which needs an `&OsStr`. + +#[test] +fn auto_invalid_encodings_red() { + assert_eq!( + Opt { + #[cfg(not(windows))] + has_try_from_os_str: HasTryFromOsStr( + "HasTryFromOsStr(\"\\xED\\xA0\\x80\")".to_string() + ), + #[cfg(windows)] + has_try_from_os_str: HasTryFromOsStr("HasTryFromOsStr(\"\\u{d800}\")".to_string()), + has_trait_from_str: HasTraitFromStr("HasTraitFromStr(\"orange\")".to_string()), + has_try_from_str: HasTryFromStr("HasTryFromStr(\"yellow\")".to_string()), + has_from_os_str: HasFromOsStr("HasFromOsStr(\"green\")".to_string()), + has_from_str: HasFromStr("HasFromStr(\"blue\")".to_string()), + }, + Opt::try_parse_from(&[ + to_os_string(b"test"), + to_os_string(b"--has-try-from-os-str"), + to_os_string(b"\xed\xa0\x80"), + to_os_string(b"--has-trait-from-str"), + to_os_string(b"orange"), + to_os_string(b"--has-try-from-str"), + to_os_string(b"yellow"), + to_os_string(b"--has-from-os-str"), + to_os_string(b"green"), + to_os_string(b"--has-from-str"), + to_os_string(b"blue"), + ]) + .unwrap() + ); +} + +#[test] +fn auto_invalid_encodings_green() { + assert_eq!( + Opt { + has_try_from_os_str: HasTryFromOsStr("HasTryFromOsStr(\"red\")".to_string()), + has_trait_from_str: HasTraitFromStr("HasTraitFromStr(\"orange\")".to_string()), + has_try_from_str: HasTryFromStr("HasTryFromStr(\"yellow\")".to_string()), + #[cfg(not(windows))] + has_from_os_str: HasFromOsStr("HasFromOsStr(\"\\xED\\xA0\\x80\")".to_string()), + #[cfg(windows)] + has_from_os_str: HasFromOsStr("HasFromOsStr(\"\\u{d800}\")".to_string()), + has_from_str: HasFromStr("HasFromStr(\"blue\")".to_string()), + }, + Opt::try_parse_from(&[ + to_os_string(b"test"), + to_os_string(b"--has-try-from-os-str"), + to_os_string(b"red"), + to_os_string(b"--has-trait-from-str"), + to_os_string(b"orange"), + to_os_string(b"--has-try-from-str"), + to_os_string(b"yellow"), + to_os_string(b"--has-from-os-str"), + to_os_string(b"\xed\xa0\x80"), + to_os_string(b"--has-from-str"), + to_os_string(b"blue"), + ]) + .unwrap() + ); +} + +// Test invalid UTF-8 with each trait which needs a `&str`. + +#[test] +fn auto_invalid_encodings_orange() { + assert_eq!( + format!( + "error: Invalid value for \'has-trait-from-str\': \ + The argument \'{}\' isn\'t a valid encoding for \'has-trait-from-str\'\n\n\ + For more information try --help\n", + to_os_string(b"\xed\xa0\x80").to_string_lossy() + ), + Opt::try_parse_from(&[ + to_os_string(b"test"), + to_os_string(b"--has-try-from-os-str"), + to_os_string(b"red"), + to_os_string(b"--has-trait-from-str"), + to_os_string(b"\xed\xa0\x80"), + to_os_string(b"--has-try-from-str"), + to_os_string(b"yellow"), + to_os_string(b"--has-from-os-str"), + to_os_string(b"green"), + to_os_string(b"--has-from-str"), + to_os_string(b"blue"), + ]) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn auto_invalid_encodings_yellow() { + assert_eq!( + format!( + "error: Invalid value for \'has-try-from-str\': \ + The argument \'{}\' isn\'t a valid encoding for \'has-try-from-str\'\n\n\ + For more information try --help\n", + to_os_string(b"\xed\xa0\x80").to_string_lossy() + ), + Opt::try_parse_from(&[ + to_os_string(b"test"), + to_os_string(b"--has-try-from-os-str"), + to_os_string(b"red"), + to_os_string(b"--has-trait-from-str"), + to_os_string(b"orange"), + to_os_string(b"--has-try-from-str"), + to_os_string(b"\xed\xa0\x80"), + to_os_string(b"--has-from-os-str"), + to_os_string(b"green"), + to_os_string(b"--has-from-str"), + to_os_string(b"blue"), + ]) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn auto_invalid_encodings_blue() { + assert_eq!( + format!( + "error: Invalid value for \'has-from-str\': \ + The argument \'{}\' isn\'t a valid encoding for \'has-from-str\'\n\n\ + For more information try --help\n", + to_os_string(b"\xed\xa0\x80").to_string_lossy() + ), + Opt::try_parse_from(&[ + to_os_string(b"test"), + to_os_string(b"--has-try-from-os-str"), + to_os_string(b"red"), + to_os_string(b"--has-trait-from-str"), + to_os_string(b"orange"), + to_os_string(b"--has-try-from-str"), + to_os_string(b"yellow"), + to_os_string(b"--has-from-os-str"), + to_os_string(b"green"), + to_os_string(b"--has-from-str"), + to_os_string(b"\xed\xa0\x80"), + ]) + .unwrap_err() + .to_string() + ); +} + +// Test parse errors with traits that can have errors. + +#[test] +fn auto_parse_errors_red() { + assert_eq!( + "error: Invalid value for \'has-try-from-os-str\': \ + The argument \'red!\' isn\'t a valid value for \'has-try-from-os-str\': failed HasTryFromOsStr on \'\"red!\"\'\n\n\ + For more information try --help\n", + Opt::try_parse_from(&[ + "test", + "--has-try-from-os-str", "red!", + "--has-trait-from-str", "orange", + "--has-try-from-str", "yellow", + "--has-from-os-str", "green", + "--has-from-str", "blue", + ]).unwrap_err().to_string() + ); +} + +#[test] +fn auto_parse_errors_orange() { + assert_eq!( + "error: Invalid value for \'has-trait-from-str\': \ + The argument \'orange!\' isn\'t a valid value for \'has-trait-from-str\': failed HasTraitFromStr on \'\"orange!\"\'\n\n\ + For more information try --help\n", + Opt::try_parse_from(&[ + "test", + "--has-try-from-os-str", "red", + "--has-trait-from-str", "orange!", + "--has-try-from-str", "yellow", + "--has-from-os-str", "green", + "--has-from-str", "blue", + ]).unwrap_err().to_string() + ); +} + +#[test] +fn auto_parse_errors_yellow() { + assert_eq!( + "error: Invalid value for \'has-try-from-str\': \ + The argument \'yellow!\' isn\'t a valid value for \'has-try-from-str\': failed HasTryFromStr on \'\"yellow!\"\'\n\n\ + For more information try --help\n", + Opt::try_parse_from(&[ + "test", + "--has-try-from-os-str", "red", + "--has-trait-from-str", "orange", + "--has-try-from-str", "yellow!", + "--has-from-os-str", "green", + "--has-from-str", "blue", + ]).unwrap_err().to_string() + ); +} + +// Test a type which doesn't implement any parsing traits, to ensure that we +// emit a friendly error message. + +#[derive(Clap, PartialEq, Debug)] +struct NoneOpt { + #[clap(long, parse(auto))] + has_none: HasNone, +} + +#[test] +fn auto_none() { + assert_eq!( + "error: Invalid value for \'has-none\': \ + The argument \'colorless\' isn\'t a valid value for \'has-none\': \ + Type `HasNone` does not implement any of the parsing traits: `clap::ArgEnum`, `TryFrom<&OsStr>`, `FromStr`, `TryFrom<&str>`, `From<&OsStr>`, or `From<&str>`\n\n\ + For more information try --help\n".to_string(), + NoneOpt::try_parse_from(&[ + "test", + "--has-none", "colorless", + ]).unwrap_err().to_string() + ); +} + +// Test that Vec-of-T parsing works with `auto`. +#[derive(Clap, PartialEq, Debug)] +struct VecOpt { + #[clap(long, parse(auto))] + has_try_from_os_str: Vec, + + #[clap(long, parse(auto))] + has_trait_from_str: Vec, + + #[clap(long, parse(auto))] + has_try_from_str: Vec, + + #[clap(long, parse(auto))] + has_from_os_str: Vec, + + #[clap(long, parse(auto))] + has_from_str: Vec, +} + +#[test] +fn auto_vec() { + assert_eq!( + VecOpt { + has_try_from_os_str: vec![HasTryFromOsStr("HasTryFromOsStr(\"red\")".to_string())], + has_trait_from_str: vec![HasTraitFromStr("HasTraitFromStr(\"orange\")".to_string())], + has_try_from_str: vec![HasTryFromStr("HasTryFromStr(\"yellow\")".to_string())], + has_from_os_str: vec![HasFromOsStr("HasFromOsStr(\"green\")".to_string())], + has_from_str: vec![HasFromStr("HasFromStr(\"blue\")".to_string())], + }, + VecOpt::try_parse_from(&[ + "test", + "--has-try-from-os-str", + "red", + "--has-trait-from-str", + "orange", + "--has-try-from-str", + "yellow", + "--has-from-os-str", + "green", + "--has-from-str", + "blue", + ]) + .unwrap() + ); +} + +/// Convert from bytes to `&OsStr`. +#[inline(never)] +fn to_os_string(bytes: &[u8]) -> OsString { + use os_str_bytes::{EncodingError, OsStrBytes}; + use std::borrow::Cow; + + let t: Result, EncodingError> = OsStrBytes::from_bytes(bytes); + t.unwrap().into_owned() +} diff --git a/src/parse/matches/arg_matches.rs b/src/parse/matches/arg_matches.rs index 692633d61a9..73f84e3caa1 100644 --- a/src/parse/matches/arg_matches.rs +++ b/src/parse/matches/arg_matches.rs @@ -572,6 +572,67 @@ impl ArgMatches { }) } + /// Gets optional the value of a specific argument (i.e. an argument that takes an additional + /// value at runtime) and then converts it into the result type using `parse`, which takes + /// the OS version of the string value of the argument. + /// + /// In the case where the argument wasn't present, `Ok(None)` is returned. In the + /// case where it was present and parsing succeeded, `Ok(Some(value))` is returned. + /// If parsing (of any value) has failed, returns Err. + /// + /// *NOTE:* If getting a value for an option or positional argument that allows multiples, + /// prefer [`ArgMatches::parse_optional_vec_t_auto`] as `Arg::parse_optional_t_auto` will + /// only return the *first* value. + /// + /// `parse` returns a result which is either `Ok` to indicate a successful parse, + /// `Err(Ok(err))` to indicate a parse error, or `Err(Err(err))` to indicate that + /// invalid UTF-8 encodings were encountered and not supported. + /// + /// This routine is used to support `parse(auto)`. + pub fn parse_optional_t_auto( + &self, + name: &str, + parse: impl Fn(&OsStr) -> Result>, + ) -> Result, Error> + where + E: Display, + { + Ok(match self.value_of_os(name) { + Some(v) => Some(parse(v).map_err(|e| match e { + Ok(e) => { + let message = format!( + "The argument '{}' isn't a valid value for '{}': {}", + v.to_string_lossy(), + name, + e + ); + + Error::value_validation( + name.to_string(), + v.to_string_lossy().to_string(), + message.into(), + ColorChoice::Auto, + ) + } + Err(_e) => { + let message = format!( + "The argument '{}' isn't a valid encoding for '{}'", + v.to_string_lossy(), + name, + ); + + Error::value_validation( + name.to_string(), + v.to_string_lossy().to_string(), + message.into(), + ColorChoice::Auto, + ) + } + })?), + None => None, + }) + } + /// Gets the value of a specific argument (i.e. an argument that takes an additional /// value at runtime) and then converts it into the result type using [`std::str::FromStr`]. /// @@ -820,6 +881,67 @@ impl ArgMatches { }) } + /// Gets the typed values of a specific argument (i.e. an argument that takes multiple + /// values at runtime) and then converts them into the result type using `parse`, which takes + /// the OS version of the string value of the argument. + /// + /// In the case where the argument wasn't present, `Ok(None)` is returned. In the + /// case where it was present and parsing succeeded, `Ok(Some(vec))` is returned. + /// If parsing (of any value) has failed, returns Err. + /// + /// `parse` returns a result which is either `Ok` to indicate a successful parse, + /// `Err(Ok(err))` to indicate a parse error, or `Err(Err(err))` to indicate that + /// invalid UTF-8 encodings were encountered and not supported. + /// + /// This routine is used to support `parse(auto)`. + pub fn parse_optional_vec_t_auto( + &self, + name: &str, + parse: impl Fn(&OsStr) -> Result>, + ) -> Result>, Error> + where + E: Display, + { + Ok(match self.values_of_os(name) { + Some(vals) => Some( + vals.map(|v| { + parse(v).map_err(|e| match e { + Ok(e) => { + let message = format!( + "The argument '{}' isn't a valid value: {}", + v.to_string_lossy(), + e + ); + + Error::value_validation( + name.to_string(), + v.to_string_lossy().to_string(), + message.into(), + ColorChoice::Auto, + ) + } + Err(e) => { + let message = format!( + "The argument '{}' isn't a valid encoding: {}", + v.to_string_lossy(), + e.to_string_lossy(), + ); + + Error::value_validation( + name.to_string(), + v.to_string_lossy().to_string(), + message.into(), + ColorChoice::Auto, + ) + } + }) + }) + .collect::>()?, + ), + None => None, + }) + } + /// Gets the typed values of a specific argument (i.e. an argument that takes multiple /// values at runtime) and then converts them into the result type using [`std::str::FromStr`]. ///