Skip to content

Commit

Permalink
feat: Auto-detect FromStr, TryFrom<&OsStr>, etc., in clap_derive
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sunfishcode committed Jan 18, 2021
1 parent b8b112b commit 291a85b
Show file tree
Hide file tree
Showing 11 changed files with 671 additions and 88 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//
Expand Down
1 change: 1 addition & 0 deletions clap_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions clap_derive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -56,7 +56,7 @@ struct Opt {
level: Vec<String>,

/// Files to process
#[clap(name = "FILE", parse(from_os_str), value_hint = ValueHint::AnyPath)]
#[clap(name = "FILE", value_hint = ValueHint::AnyPath)]
files: Vec<PathBuf>,
}

Expand Down
4 changes: 2 additions & 2 deletions clap_derive/examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,7 +38,7 @@ struct Opt {
level: Vec<String>,

/// Files to process
#[clap(name = "FILE", parse(from_os_str), value_hint = ValueHint::AnyPath)]
#[clap(name = "FILE", value_hint = ValueHint::AnyPath)]
files: Vec<PathBuf>,
}

Expand Down
2 changes: 1 addition & 1 deletion clap_derive/examples/value_hints_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct Opt {
dir: Option<PathBuf>,
#[clap(short, long, value_hint = ValueHint::ExecutablePath)]
exe: Option<PathBuf>,
#[clap(long, parse(from_os_str), value_hint = ValueHint::CommandName)]
#[clap(long, value_hint = ValueHint::CommandName)]
cmd_name: Option<OsString>,
#[clap(short, long, value_hint = ValueHint::CommandString)]
cmd: Option<String>,
Expand Down
61 changes: 28 additions & 33 deletions clap_derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake;
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum Kind {
Arg(Sp<Ty>),
Arg(Sp<Ty>, TokenStream),
Subcommand(Sp<Ty>),
Flatten,
Skip(Option<Expr>),
Expand All @@ -53,11 +53,12 @@ pub struct Method {
#[derive(Clone)]
pub struct Parser {
pub kind: Sp<ParserKind>,
pub func: TokenStream,
pub parse_func: Option<Expr>,
}

#[derive(Debug, PartialEq, Clone)]
pub enum ParserKind {
Auto,
FromStr,
TryFromStr,
FromOsStr,
Expand Down Expand Up @@ -155,15 +156,16 @@ impl ToTokens for Method {

impl Parser {
fn default_spanned(span: Span) -> Sp<Self> {
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<Self> {
use self::ParserKind::*;

let kind = match &*spec.kind.to_string() {
"auto" => Auto,
"from_str" => FromStr,
"try_from_str" => TryFromStr,
"from_os_str" => FromOsStr,
Expand All @@ -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())
}
}
Expand Down Expand Up @@ -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,
),
}
}

Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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!(
Expand All @@ -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 => (),
Expand Down Expand Up @@ -596,15 +585,21 @@ impl Attrs {

_ => (),
}
res.kind = Sp::new(Kind::Arg(ty), orig_ty.span());

// Serialize the `inner` type (eg. the `T` in `Vec<T>`) 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());
}
}

res
}

fn set_kind(&mut self, kind: Sp<Kind>) {
if let Kind::Arg(_) = *self.kind {
if let Kind::Arg(_, _) = *self.kind {
self.kind = kind;
} else {
abort!(
Expand Down

0 comments on commit 291a85b

Please sign in to comment.