gumdrop_derive-0.8.1/.cargo_vcs_info.json0000644000000001540000000000100140550ustar { "git": { "sha1": "2e12c79a793066028ac2261274041f4a12ba6c61" }, "path_in_vcs": "gumdrop_derive" }gumdrop_derive-0.8.1/Cargo.toml0000644000000020710000000000100120530ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "gumdrop_derive" version = "0.8.1" authors = ["Murarth "] description = "custom derive support for gumdrop" homepage = "https://github.com/murarth/gumdrop" documentation = "https://docs.rs/gumdrop_derive/" keywords = ["args", "command-line", "flag", "getopts", "option"] license = "MIT/Apache-2.0" repository = "https://github.com/murarth/gumdrop" [lib] name = "gumdrop_derive" proc-macro = true [dependencies.proc-macro2] version = "1" [dependencies.quote] version = "1" [dependencies.syn] version = "1" [features] default = [] default_expr = ["syn/full"] gumdrop_derive-0.8.1/Cargo.toml.orig000064400000000000000000000010620072674642500155630ustar 00000000000000[package] name = "gumdrop_derive" version = "0.8.1" authors = ["Murarth "] edition = "2018" description = "custom derive support for gumdrop" documentation = "https://docs.rs/gumdrop_derive/" homepage = "https://github.com/murarth/gumdrop" repository = "https://github.com/murarth/gumdrop" keywords = ["args", "command-line", "flag", "getopts", "option"] license = "MIT/Apache-2.0" [lib] name = "gumdrop_derive" proc-macro = true [features] default = [] default_expr = ["syn/full"] [dependencies] proc-macro2 = "1" quote = "1" syn = "1" gumdrop_derive-0.8.1/src/lib.rs000064400000000000000000001671540072674642500146160ustar 00000000000000//! Provides `derive(Options)` for `gumdrop` crate //! //! # `derive(Options)` //! //! `derive(Options)` generates an implementation of the trait `Options`, //! creating an option for each field of the decorated `struct`. //! //! See the `gumdrop` [documentation](https://docs.rs/gumdrop/) for an example //! of its usage. //! //! ## `options` attribute //! //! Behavior of `derive(Options)` can be controlled by adding `#[options(...)]` //! attributes to one or more fields within a decorated struct. //! //! Supported items for `struct` fields are: //! //! * `command` indicates that a field represents a subcommand. The field must //! be of type `Option` where `T` is a type implementing `Options`. //! Typically, this type is an `enum` containing subcommand option types. //! * `help_flag` marks an option as a help flag. The field must be `bool` type. //! Options named `help` will automatically receive this option. //! * `no_help_flag` prevents an option from being considered a help flag. //! * `count` marks a field as a counter value. The field will be incremented //! each time the option appears in the arguments, i.e. `field += 1;` //! * `free` marks a field as a positional argument field. Non-option arguments //! will be used to fill all `free` fields, in declared sequence. //! If the final `free` field is of type `Vec`, it will contain all //! remaining free arguments. //! * `short = "?"` sets the short option name to the given character //! * `no_short` prevents a short option from being assigned to the field //! * `long = "..."` sets the long option name to the given string //! * `no_long` prevents a long option from being assigned to the field //! * `default` provides a default value for the option field. //! The value of this field is parsed in the same way as argument values. //! * `default_expr` provides a default value for the option field. //! The value of this field is parsed at compile time as a Rust expression //! and is evaluated before any argument values are processed. //! The `default_expr` feature must be enabled to use this attribute. //! * `required` will cause an error if the option is not present, //! unless at least one `help_flag` option is also present. //! * `multi = "..."` will allow parsing an option multiple times, //! adding each parsed value to the field using the named method. //! This behavior is automatically applied to `Vec` fields, unless the //! `no_multi` option is present. //! * `no_multi` will inhibit automatically marking `Vec` fields as `multi` //! * `not_required` will cancel a type-level `required` flag (see below). //! * `help = "..."` sets help text returned from the `Options::usage` method; //! field doc comment may also be provided to set the help text. //! If both are present, the `help` attribute value is used. //! * `meta = "..."` sets the meta variable displayed in usage for options //! which accept an argument //! * `parse(...)` uses a named function to parse a value from a string. //! Valid parsing function types are: //! * `parse(from_str = "...")` for `fn(&str) -> T` //! * `parse(try_from_str = "...")` for //! `fn(&str) -> Result where E: Display` //! * `parse(from_str)` uses `std::convert::From::from` //! * `parse(try_from_str)` uses `std::str::FromStr::from_str` //! //! Additionally, the following flags may be set at the type level to establish //! default values for all contained fields: `no_help_flag`, `no_long`, //! `no_short`, and `required`. //! //! Supported items for `enum` variants are: //! //! * `name = "..."` sets the user-facing command name. //! If this option is not present, one is automatically generated from the variant name. //! * `help = "..."` sets the help string for the command; //! variant doc comment may also be provided to set the help text. //! If both are present, the `help` attribute value is used. //! //! The `help` attribute (or a type-level doc comment) can be used to provide //! some introductory text which will precede option help text in the usage //! string. #![recursion_limit = "1024"] extern crate proc_macro; use std::iter::repeat; use quote::quote; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use syn::{ parse::Error, spanned::Spanned, Attribute, AttrStyle, Data, DataEnum, DataStruct, DeriveInput, Fields, GenericArgument, Ident, Lit, Meta, NestedMeta, Path, PathArguments, Type, parse_str, }; #[cfg(feature = "default_expr")] use syn::Expr; /// Derives the `gumdrop::Options` trait for `struct` and `enum` items. /// /// `#[options(...)]` attributes can be used to control behavior of generated trait /// implementation. See [crate-level documentation](index.html) for the full list of /// supported options. #[proc_macro_derive(Options, attributes(options))] pub fn derive_options(input: TokenStream) -> TokenStream { let ast: DeriveInput = match syn::parse(input) { Ok(ast) => ast, Err(e) => { return e.to_compile_error().into(); } }; let span = ast.ident.span(); let result = match &ast.data { Data::Enum(data) => derive_options_enum(&ast, data), Data::Struct(DataStruct{fields: Fields::Unit, ..}) => Err(Error::new(span, "cannot derive Options for unit struct types")), Data::Struct(DataStruct{fields: Fields::Unnamed(..), ..}) => Err(Error::new(span, "cannot derive Options for tuple struct types")), Data::Struct(DataStruct{fields, ..}) => derive_options_struct(&ast, fields), Data::Union(_) => Err(Error::new(span, "cannot derive Options for union types")), }; match result { Ok(tokens) => tokens.into(), Err(e) => e.to_compile_error().into() } } fn derive_options_enum(ast: &DeriveInput, data: &DataEnum) -> Result { let name = &ast.ident; let mut commands = Vec::new(); let mut var_ty = Vec::new(); for var in &data.variants { let span = var.ident.span(); let ty = match &var.fields { Fields::Unit | Fields::Named(_) => return Err(Error::new(span, "command variants must be unary tuple variants")), Fields::Unnamed(fields) if fields.unnamed.len() != 1 => return Err(Error::new(span, "command variants must be unary tuple variants")), Fields::Unnamed(fields) => &fields.unnamed.first().unwrap().ty, }; let opts = CmdOpts::parse(&var.attrs)?; let var_name = &var.ident; var_ty.push(ty); commands.push(Cmd{ name: opts.name.unwrap_or_else( || make_command_name(&var_name.to_string())), help: opts.help.or(opts.doc), variant_name: var_name, ty: ty, }); } let mut command = Vec::new(); let mut handle_cmd = Vec::new(); let mut help_req_impl = Vec::new(); let mut variant = Vec::new(); let usage = make_cmd_usage(&commands); for cmd in commands { command.push(cmd.name); let var_name = cmd.variant_name; let ty = &cmd.ty; variant.push(var_name); handle_cmd.push(quote!{ #name::#var_name(<#ty as ::gumdrop::Options>::parse(_parser)?) }); help_req_impl.push(quote!{ #name::#var_name(cmd) => { ::gumdrop::Options::help_requested(cmd) } }); } // Borrow re-used items let command = &command; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); let command_impl = { let name = repeat(name); quote!{ match self { #( #name::#variant(cmd) => ::gumdrop::Options::command(cmd), )* } } }; let command_name_impl = { let name = repeat(name); quote!{ match self { #( #name::#variant(_) => ::std::option::Option::Some(#command), )* } } }; let self_usage_impl = { let name = repeat(name); quote!{ match self { #( #name::#variant(sub) => ::gumdrop::Options::self_usage(sub), )* } } }; let self_command_list_impl = { let name = repeat(name); quote!{ match self { #( #name::#variant(sub) => ::gumdrop::Options::self_command_list(sub), )* } } }; Ok(quote!{ impl #impl_generics ::gumdrop::Options for #name #ty_generics #where_clause { fn parse<__S: ::std::convert::AsRef>( _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { let _arg = _parser.next_arg() .ok_or_else(::gumdrop::Error::missing_command)?; Self::parse_command(_arg, _parser) } fn command(&self) -> ::std::option::Option<&dyn ::gumdrop::Options> { #command_impl } fn command_name(&self) -> ::std::option::Option<&'static str> { #command_name_impl } fn help_requested(&self) -> bool { match self { #( #help_req_impl )* } } fn parse_command<__S: ::std::convert::AsRef>(name: &str, _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { let cmd = match name { #( #command => { #handle_cmd } )* _ => return ::std::result::Result::Err( ::gumdrop::Error::unrecognized_command(name)) }; ::std::result::Result::Ok(cmd) } fn usage() -> &'static str { #usage } fn self_usage(&self) -> &'static str { #self_usage_impl } fn command_list() -> ::std::option::Option<&'static str> { ::std::option::Option::Some(::usage()) } fn self_command_list(&self) -> ::std::option::Option<&'static str> { #self_command_list_impl } fn command_usage(name: &str) -> ::std::option::Option<&'static str> { match name { #( #command => ::std::option::Option::Some( <#var_ty as ::gumdrop::Options>::usage()), )* _ => ::std::option::Option::None } } } }) } fn derive_options_struct(ast: &DeriveInput, fields: &Fields) -> Result { let mut pattern = Vec::new(); let mut handle_opt = Vec::new(); let mut short_names = Vec::new(); let mut long_names = Vec::new(); let mut free: Vec = Vec::new(); let mut required = Vec::new(); let mut required_err = Vec::new(); let mut command = None; let mut command_ty = None; let mut command_required = false; let mut help_flag = Vec::new(); let mut options = Vec::new(); let mut field_name = Vec::new(); let mut default = Vec::new(); let default_expr = quote!{ ::std::default::Default::default() }; let default_opts = DefaultOpts::parse(&ast.attrs)?; for field in fields { let span = field.ident.as_ref().unwrap().span(); let mut opts = AttrOpts::parse(span, &field.attrs)?; opts.set_defaults(&default_opts); let ident = field.ident.as_ref().unwrap(); field_name.push(ident); if let Some(expr) = &opts.default { default.push(opts.parse.as_ref() .unwrap_or(&ParseFn::Default) .make_parse_default_action(ident, &expr)); } else { #[cfg(not(feature = "default_expr"))] default.push(default_expr.clone()); #[cfg(feature = "default_expr")] { if let Some(expr) = &opts.default_expr { default.push(quote!{ #expr }); } else { default.push(default_expr.clone()); } } } if opts.command { if command.is_some() { return Err(Error::new(span, "duplicate declaration of `command` field")); } if !free.is_empty() { return Err(Error::new(span, "`command` and `free` options are mutually exclusive")); } command = Some(ident); command_ty = Some(first_ty_param(&field.ty).unwrap_or(&field.ty)); command_required = opts.required; if opts.required { required.push(ident); required_err.push(quote!{ ::gumdrop::Error::missing_required_command() }); } continue; } if opts.free { if command.is_some() { return Err(Error::new(span, "`command` and `free` options are mutually exclusive")); } if let Some(last) = free.last() { if last.action.is_push() { return Err(Error::new(span, "only the final `free` option may be of type `Vec`")); } } if opts.required { required.push(ident); required_err.push(quote!{ ::gumdrop::Error::missing_required_free() }); } free.push(FreeOpt{ field: ident, action: FreeAction::infer(&field.ty, &opts), parse: opts.parse.unwrap_or_default(), required: opts.required, help: opts.help.or(opts.doc), }); continue; } if opts.long.is_none() && !opts.no_long { opts.long = Some(make_long_name(&ident.to_string())); } if let Some(long) = &opts.long { validate_long_name(span, long, &long_names)?; long_names.push(long.clone()); } if let Some(short) = opts.short { validate_short_name(span, short, &short_names)?; short_names.push(short); } if opts.help_flag || (!opts.no_help_flag && opts.long.as_ref().map(|s| &s[..]) == Some("help")) { help_flag.push(ident); } let action = if opts.count { Action::Count } else { Action::infer(&field.ty, &opts) }; if action.takes_arg() { if opts.meta.is_none() { opts.meta = Some(make_meta(&ident.to_string(), &action)); } } else if opts.meta.is_some() { return Err(Error::new(span, "`meta` value is invalid for this field")); } options.push(Opt{ field: ident, action: action, long: opts.long, short: opts.short, no_short: opts.no_short, required: opts.required, meta: opts.meta, help: opts.help.or(opts.doc), default: opts.default, }); } // Assign short names after checking all options. // Thus, manual short names will take priority over automatic ones. for opt in &mut options { if opt.short.is_none() && !opt.no_short { let short = make_short_name(&opt.field.to_string(), &short_names); if let Some(short) = short { short_names.push(short); } opt.short = short; } } for opt in &options { if opt.required { required.push(opt.field); let display = opt.display_form(); required_err.push(quote!{ ::gumdrop::Error::missing_required(#display) }); } let pat = match (&opt.long, opt.short) { (Some(long), Some(short)) => quote!{ ::gumdrop::Opt::Long(#long) | ::gumdrop::Opt::Short(#short) }, (Some(long), None) => quote!{ ::gumdrop::Opt::Long(#long) }, (None, Some(short)) => quote!{ ::gumdrop::Opt::Short(#short) }, (None, None) => { return Err(Error::new(opt.field.span(), "option has no long or short flags")); } }; pattern.push(pat); handle_opt.push(opt.make_action()); if let Some(long) = &opt.long { let (pat, handle) = if let Some(n) = opt.action.tuple_len() { (quote!{ ::gumdrop::Opt::LongWithArg(#long, _) }, quote!{ return ::std::result::Result::Err( ::gumdrop::Error::unexpected_single_argument(_opt, #n)) }) } else if opt.action.takes_arg() { (quote!{ ::gumdrop::Opt::LongWithArg(#long, _arg) }, opt.make_action_arg()) } else { (quote!{ ::gumdrop::Opt::LongWithArg(#long, _) }, quote!{ return ::std::result::Result::Err( ::gumdrop::Error::unexpected_argument(_opt)) }) }; pattern.push(pat); handle_opt.push(handle); } } let name = &ast.ident; let opts_help = default_opts.help.or(default_opts.doc); let usage = make_usage(&opts_help, &free, &options); let handle_free = if !free.is_empty() { let catch_all = if free.last().unwrap().action.is_push() { let last = free.pop().unwrap(); let free = last.field; let name = free.to_string(); let meth = match &last.action { FreeAction::Push(meth) => meth, _ => unreachable!() }; let parse = last.parse.make_parse_action(Some(&name[..])); let mark_used = last.mark_used(); quote!{ #mark_used let _arg = _free; _result.#free.#meth(#parse); } } else { quote!{ return ::std::result::Result::Err( ::gumdrop::Error::unexpected_free(_free)) } }; let num = 0..free.len(); let action = free.iter().map(|free| { let field = free.field; let name = field.to_string(); let mark_used = free.mark_used(); let parse = free.parse.make_parse_action(Some(&name[..])); let assign = match &free.action { FreeAction::Push(meth) => quote!{ let _arg = _free; _result.#field.#meth(#parse); }, FreeAction::SetField => quote!{ let _arg = _free; _result.#field = #parse; }, FreeAction::SetOption => quote!{ let _arg = _free; _result.#field = ::std::option::Option::Some(#parse); }, }; quote!{ #mark_used #assign } }).collect::>(); quote!{ match _free_counter { #( #num => { _free_counter += 1; #action } )* _ => { #catch_all } } } } else if let Some(ident) = command { let mark_used = if command_required { quote!{ _used.#ident = true; } } else { quote!{ } }; quote!{ #mark_used _result.#ident = ::std::option::Option::Some( ::gumdrop::Options::parse_command(_free, _parser)?); break; } } else { quote!{ return ::std::result::Result::Err( ::gumdrop::Error::unexpected_free(_free)); } }; let command_impl = match &command { None => quote!{ ::std::option::Option::None }, Some(field) => quote!{ ::std::option::Option::map( ::std::option::Option::as_ref(&self.#field), |sub| sub as _) } }; let command_name_impl = match &command { None => quote!{ ::std::option::Option::None }, Some(field) => quote!{ ::std::option::Option::and_then( ::std::option::Option::as_ref(&self.#field), ::gumdrop::Options::command_name) } }; let command_list = match command_ty { Some(ty) => quote!{ ::std::option::Option::Some( <#ty as ::gumdrop::Options>::usage()) }, None => quote!{ ::std::option::Option::None } }; let command_usage = match command_ty { Some(ty) => quote!{ <#ty as ::gumdrop::Options>::command_usage(_name) }, None => quote!{ ::std::option::Option::None } }; let help_requested_impl = match (&help_flag, &command) { (flags, None) => quote!{ fn help_requested(&self) -> bool { false #( || self.#flags )* } }, (flags, Some(cmd)) => quote!{ fn help_requested(&self) -> bool { #( self.#flags || )* ::std::option::Option::map_or( ::std::option::Option::as_ref(&self.#cmd), false, ::gumdrop::Options::help_requested) } } }; let self_usage_impl = match &command { None => quote!{ ::usage() }, Some(field) => quote!{ ::std::option::Option::map_or_else( ::std::option::Option::as_ref(&self.#field), ::usage, ::gumdrop::Options::self_usage) } }; let self_command_list_impl = match &command { None => quote!{ ::command_list() }, Some(field) => quote!{ ::std::option::Option::map_or_else( ::std::option::Option::as_ref(&self.#field), ::command_list, ::gumdrop::Options::self_command_list) } }; let required = &required; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); Ok(quote!{ impl #impl_generics ::gumdrop::Options for #name #ty_generics #where_clause { fn parse<__S: ::std::convert::AsRef>( _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { #[derive(Default)] struct _Used { #( #required: bool , )* } let mut _result = #name{ #( #field_name: #default ),* }; let mut _free_counter = 0usize; let mut _used = _Used::default(); while let ::std::option::Option::Some(_opt) = _parser.next_opt() { match _opt { #( #pattern => { #handle_opt } )* ::gumdrop::Opt::Free(_free) => { #handle_free } _ => { return ::std::result::Result::Err( ::gumdrop::Error::unrecognized_option(_opt)); } } } if true #( && !_result.#help_flag )* { #( if !_used.#required { return ::std::result::Result::Err(#required_err); } )* } ::std::result::Result::Ok(_result) } fn command(&self) -> ::std::option::Option<&dyn ::gumdrop::Options> { #command_impl } fn command_name(&self) -> ::std::option::Option<&'static str> { #command_name_impl } #help_requested_impl fn parse_command<__S: ::std::convert::AsRef>(name: &str, _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { ::std::result::Result::Err( ::gumdrop::Error::unrecognized_command(name)) } fn usage() -> &'static str { #usage } fn self_usage(&self) -> &'static str { #self_usage_impl } fn command_list() -> ::std::option::Option<&'static str> { #command_list } fn command_usage(_name: &str) -> ::std::option::Option<&'static str> { #command_usage } fn self_command_list(&self) -> ::std::option::Option<&'static str> { #self_command_list_impl } } }) } enum Action { /// Increase count Count, /// Push an argument to a `multi` field using the given method Push(Ident, ParseMethod), /// Set field SetField(ParseMethod), /// Set `Option` field SetOption(ParseMethod), /// Set field to `true` Switch, } #[derive(Default)] struct AttrOpts { long: Option, short: Option, multi: Option, free: bool, count: bool, help_flag: bool, no_help_flag: bool, no_short: bool, no_long: bool, no_multi: bool, required: bool, not_required: bool, doc: Option, help: Option, meta: Option, parse: Option, default: Option, #[cfg(feature = "default_expr")] default_expr: Option, command: bool, } struct Cmd<'a> { name: String, help: Option, variant_name: &'a Ident, ty: &'a Type, } #[derive(Default)] struct CmdOpts { name: Option, doc: Option, help: Option, } #[derive(Default)] struct DefaultOpts { no_help_flag: bool, no_long: bool, no_multi: bool, no_short: bool, required: bool, doc: Option, help: Option, } enum FreeAction { Push(Ident), SetField, SetOption, } struct FreeOpt<'a> { field: &'a Ident, action: FreeAction, parse: ParseFn, required: bool, help: Option, } struct Opt<'a> { field: &'a Ident, action: Action, long: Option, short: Option, no_short: bool, required: bool, help: Option, meta: Option, default: Option, // NOTE: `default_expr` is not contained here // because it is not displayed to the user in usage text } #[derive(Clone)] enum ParseFn { Default, FromStr(Option), TryFromStr(Path), } struct ParseMethod { parse_fn: ParseFn, tuple_len: Option, } impl Action { fn infer(ty: &Type, opts: &AttrOpts) -> Action { match ty { Type::Path(path) => { let path = path.path.segments.last().unwrap(); let param = first_ty_param(ty); match &path.ident.to_string()[..] { "bool" if opts.parse.is_none() => Action::Switch, "Vec" if !opts.no_multi && param.is_some() => { let tuple_len = tuple_len(param.unwrap()); Action::Push( Ident::new("push", Span::call_site()), ParseMethod{ parse_fn: opts.parse.clone().unwrap_or_default(), tuple_len, }) } "Option" if param.is_some() => { let tuple_len = tuple_len(param.unwrap()); Action::SetOption(ParseMethod{ parse_fn: opts.parse.clone().unwrap_or_default(), tuple_len, }) } _ => { if let Some(meth) = &opts.multi { let tuple_len = param.and_then(tuple_len); Action::Push( meth.clone(), ParseMethod{ parse_fn: opts.parse.clone().unwrap_or_default(), tuple_len, }) } else { Action::SetField(ParseMethod{ parse_fn: opts.parse.clone().unwrap_or_default(), tuple_len: tuple_len(ty), }) } } } } _ => { let tuple_len = tuple_len(ty); Action::SetField(ParseMethod{ parse_fn: opts.parse.clone().unwrap_or_default(), tuple_len, }) } } } fn takes_arg(&self) -> bool { use self::Action::*; match self { Push(_, parse) | SetField(parse) | SetOption(parse) => parse.takes_arg(), _ => false } } fn tuple_len(&self) -> Option { use self::Action::*; match self { Push(_, parse) | SetField(parse) | SetOption(parse) => parse.tuple_len, _ => None } } } impl AttrOpts { fn check(&self, span: Span) -> Result<(), Error> { macro_rules! err { ( $($tt:tt)* ) => { { return Err(Error::new(span, $($tt)*)); } } } if self.command { if self.free { err!("`command` and `free` are mutually exclusive"); } if self.default.is_some() { err!("`command` and `default` are mutually exclusive"); } if self.multi.is_some() { err!("`command` and `multi` are mutually exclusive"); } if self.long.is_some() { err!("`command` and `long` are mutually exclusive"); } if self.short.is_some() { err!("`command` and `short` are mutually exclusive"); } if self.count { err!("`command` and `count` are mutually exclusive"); } if self.help_flag { err!("`command` and `help_flag` are mutually exclusive"); } if self.no_help_flag { err!("`command` and `no_help_flag` are mutually exclusive"); } if self.no_short { err!("`command` and `no_short` are mutually exclusive"); } if self.no_long { err!("`command` and `no_long` are mutually exclusive"); } if self.no_multi { err!("`command` and `no_multi` are mutually exclusive"); } if self.help.is_some() { err!("`command` and `help` are mutually exclusive"); } if self.meta.is_some() { err!("`command` and `meta` are mutually exclusive"); } } if self.free { if self.default.is_some() { err!("`free` and `default` are mutually exclusive"); } if self.long.is_some() { err!("`free` and `long` are mutually exclusive"); } if self.short.is_some() { err!("`free` and `short` are mutually exclusive"); } if self.count { err!("`free` and `count` are mutually exclusive"); } if self.help_flag { err!("`free` and `help_flag` are mutually exclusive"); } if self.no_help_flag { err!("`free` and `no_help_flag` are mutually exclusive"); } if self.no_short { err!("`free` and `no_short` are mutually exclusive"); } if self.no_long { err!("`free` and `no_long` are mutually exclusive"); } if self.meta.is_some() { err!("`free` and `meta` are mutually exclusive"); } } if self.multi.is_some() && self.no_multi { err!("`multi` and `no_multi` are mutually exclusive"); } if self.help_flag && self.no_help_flag { err!("`help_flag` and `no_help_flag` are mutually exclusive"); } if self.no_short && self.short.is_some() { err!("`no_short` and `short` are mutually exclusive"); } if self.no_long && self.long.is_some() { err!("`no_long` and `long` are mutually exclusive"); } if self.required && self.not_required { err!("`required` and `not_required` are mutually exclusive"); } if self.parse.is_some() { if self.count { err!("`count` and `parse` are mutually exclusive"); } } #[cfg(feature = "default_expr")] { if self.default.is_some() && self.default_expr.is_some() { err!("`default` and `default_expr` are mutually exclusive"); } } Ok(()) } fn parse(span: Span, attrs: &[Attribute]) -> Result { let mut opts = AttrOpts::default(); for attr in attrs { if is_outer(attr.style) { if path_eq(&attr.path, "doc") { let meta = attr.parse_meta()?; if let Meta::NameValue(nv) = meta { let doc = lit_str(&nv.lit)?; if opts.doc.is_none() { opts.doc = Some(doc.trim_start().to_owned()); } } } else if path_eq(&attr.path, "options") { let meta = attr.parse_meta()?; match meta { Meta::Path(path) => return Err(Error::new(path.span(), "`#[options]` is not a valid attribute")), Meta::NameValue(nv) => return Err(Error::new(nv.path.span(), "`#[options = ...]` is not a valid attribute")), Meta::List(items) => { for item in &items.nested { opts.parse_item(item)?; } } } } } } opts.check(span)?; Ok(opts) } fn parse_item(&mut self, item: &NestedMeta) -> Result<(), Error> { match item { NestedMeta::Lit(lit) => return Err(unexpected_meta_item(lit.span())), NestedMeta::Meta(item) => { match item { Meta::Path(path) => match path.get_ident() { Some(ident) => match ident.to_string().as_str() { "free" => self.free = true, "command" => self.command = true, "count" => self.count = true, "help_flag" => self.help_flag = true, "no_help_flag" => self.no_help_flag = true, "no_short" => self.no_short = true, "no_long" => self.no_long = true, "no_multi" => self.no_multi = true, "required" => self.required = true, "not_required" => self.not_required = true, _ => return Err(unexpected_meta_item(path.span())) } None => return Err(unexpected_meta_item(path.span())) }, Meta::List(list) => { match list.path.get_ident() { Some(ident) if ident.to_string() == "parse" => { if list.nested.len() != 1 { return Err(unexpected_meta_item(list.path.span())); } self.parse = Some(ParseFn::parse(&list.nested[0])?); } _ => return Err(unexpected_meta_item(list.path.span())) } } Meta::NameValue(nv) => { match nv.path.get_ident() { Some(ident) => match ident.to_string().as_str() { "default" => self.default = Some(lit_str(&nv.lit)?), #[cfg(feature = "default_expr")] "default_expr" => { let expr = parse_str(&lit_str(&nv.lit)?)?; self.default_expr = Some(expr); } #[cfg(not(feature = "default_expr"))] "default_expr" => { return Err(Error::new(nv.path.span(), "compile gumdrop with the `default_expr` \ feature to enable this attribute")); } "long" => self.long = Some(lit_str(&nv.lit)?), "short" => self.short = Some(lit_char(&nv.lit)?), "help" => self.help = Some(lit_str(&nv.lit)?), "meta" => self.meta = Some(lit_str(&nv.lit)?), "multi" => { let name = parse_str(&lit_str(&nv.lit)?)?; self.multi = Some(name); } _ => return Err(unexpected_meta_item(nv.path.span())) } None => return Err(unexpected_meta_item(nv.path.span())) } } } } } Ok(()) } fn set_defaults(&mut self, defaults: &DefaultOpts) { if !self.help_flag && defaults.no_help_flag { self.no_help_flag = true; } if self.short.is_none() && defaults.no_short { self.no_short = true; } if self.long.is_none() && defaults.no_long { self.no_long = true; } if self.multi.is_none() && defaults.no_multi { self.no_multi = true; } if self.not_required { self.required = false; } else if defaults.required { self.required = true; } } } impl CmdOpts { fn parse(attrs: &[Attribute]) -> Result { let mut opts = CmdOpts::default(); for attr in attrs { if is_outer(attr.style) { if path_eq(&attr.path, "doc") { let meta = attr.parse_meta()?; if let Meta::NameValue(nv) = meta { let doc = lit_str(&nv.lit)?; if opts.doc.is_none() { opts.doc = Some(doc.trim_start().to_owned()); } } } else if path_eq(&attr.path, "options") { let meta = attr.parse_meta()?; match meta { Meta::Path(path) => return Err(Error::new(path.span(), "`#[options]` is not a valid attribute")), Meta::NameValue(nv) => return Err(Error::new(nv.path.span(), "`#[options = ...]` is not a valid attribute")), Meta::List(items) => { for item in &items.nested { opts.parse_item(item)?; } } } } } } Ok(opts) } fn parse_item(&mut self, item: &NestedMeta) -> Result<(), Error> { match item { NestedMeta::Lit(lit) => return Err(unexpected_meta_item(lit.span())), NestedMeta::Meta(item) => { match item { Meta::Path(path) => return Err(unexpected_meta_item(path.span())), Meta::List(list) => return Err(unexpected_meta_item(list.path.span())), Meta::NameValue(nv) => { match nv.path.get_ident() { Some(ident) => match ident.to_string().as_str() { "name" => self.name = Some(lit_str(&nv.lit)?), "help" => self.help = Some(lit_str(&nv.lit)?), _ => return Err(unexpected_meta_item(nv.path.span())) } None => return Err(unexpected_meta_item(nv.path.span())) } } } } } Ok(()) } } impl DefaultOpts { fn parse(attrs: &[Attribute]) -> Result { let mut opts = DefaultOpts::default(); for attr in attrs { if is_outer(attr.style) { if path_eq(&attr.path, "doc") { let meta = attr.parse_meta()?; if let Meta::NameValue(nv) = meta { let doc = lit_str(&nv.lit)?; if let Some(text) = opts.doc.as_mut() { text.push('\n'); text.push_str(doc.trim_start()); } else { opts.doc = Some(doc.trim_start().to_owned()); } } } else if path_eq(&attr.path, "options") { let meta = attr.parse_meta()?; match meta { Meta::Path(path) => return Err(Error::new(path.span(), "`#[options]` is not a valid attribute")), Meta::NameValue(nv) => return Err(Error::new(nv.path.span(), "`#[options = ...]` is not a valid attribute")), Meta::List(items) => { for item in &items.nested { opts.parse_item(item)?; } } } } } } Ok(opts) } fn parse_item(&mut self, item: &NestedMeta) -> Result<(), Error> { match item { NestedMeta::Lit(lit) => return Err(unexpected_meta_item(lit.span())), NestedMeta::Meta(item) => { match item { Meta::Path(path) => match path.get_ident() { Some(ident) => match ident.to_string().as_str() { "no_help_flag" => self.no_help_flag = true, "no_short" => self.no_short = true, "no_long" => self.no_long = true, "no_multi" => self.no_multi = true, "required" => self.required = true, _ => return Err(unexpected_meta_item(ident.span())) } None => return Err(unexpected_meta_item(path.span())) }, Meta::NameValue(nv) => { match nv.path.get_ident() { Some(ident) if ident.to_string() == "help" => self.help = Some(lit_str(&nv.lit)?), _ => return Err(unexpected_meta_item(nv.path.span())) } } Meta::List(list) => return Err(unexpected_meta_item(list.path.span())) } } } Ok(()) } } impl FreeAction { fn infer(ty: &Type, opts: &AttrOpts) -> FreeAction { match ty { Type::Path(path) => { let path = path.path.segments.last().unwrap(); match &path.ident.to_string()[..] { "Option" => FreeAction::SetOption, "Vec" if !opts.no_multi => FreeAction::Push(Ident::new("push", Span::call_site())), _ => { if let Some(meth) = &opts.multi { FreeAction::Push(meth.clone()) } else { FreeAction::SetField } } } } _ => FreeAction::SetField, } } fn is_push(&self) -> bool { match self { FreeAction::Push(_) => true, _ => false } } } impl<'a> FreeOpt<'a> { fn mark_used(&self) -> TokenStream2 { if self.required { let field = self.field; quote!{ _used.#field = true; } } else { quote!{ } } } fn width(&self) -> usize { 2 + self.field.to_string().len() + 2 // name + spaces before and after } } impl<'a> Opt<'a> { fn display_form(&self) -> String { if let Some(long) = &self.long { format!("--{}", long) } else { format!("-{}", self.short.unwrap()) } } fn mark_used(&self) -> TokenStream2 { if self.required { let field = self.field; quote!{ _used.#field = true; } } else { quote!{ } } } fn width(&self) -> usize { let short = self.short.map_or(0, |_| 1 + 1); // '-' + char let long = self.long.as_ref().map_or(0, |s| s.len() + 2); // "--" + str let sep = if short == 0 || long == 0 { 0 } else { 2 }; // ", " let meta = self.meta.as_ref().map_or(0, |s| s.len() + 1); // ' ' + meta 2 + short + long + sep + meta + 2 // total + spaces before and after } fn make_action(&self) -> TokenStream2 { use self::Action::*; let field = self.field; let mark_used = self.mark_used(); let action = match &self.action { Count => quote!{ _result.#field += 1; }, Push(meth, parse) => { let act = parse.make_action_type(); quote!{ _result.#field.#meth(#act); } } SetField(parse) => { let act = parse.make_action_type(); quote!{ _result.#field = #act; } } SetOption(parse) => { let act = parse.make_action_type(); quote!{ _result.#field = ::std::option::Option::Some(#act); } } Switch => quote!{ _result.#field = true; } }; quote!{ #mark_used #action } } fn make_action_arg(&self) -> TokenStream2 { use self::Action::*; let field = self.field; let mark_used = self.mark_used(); let action = match &self.action { Push(meth, parse) => { let act = parse.make_action_type_arg(); quote!{ _result.#field.#meth(#act); } } SetField(parse) => { let act = parse.make_action_type_arg(); quote!{ _result.#field = #act; } } SetOption(parse) => { let act = parse.make_action_type_arg(); quote!{ _result.#field = ::std::option::Option::Some(#act); } } _ => unreachable!() }; quote!{ #mark_used #action } } fn usage(&self, col_width: usize) -> String { let mut res = String::from(" "); if let Some(short) = self.short { res.push('-'); res.push(short); } if self.short.is_some() && self.long.is_some() { res.push_str(", "); } if let Some(long) = &self.long { res.push_str("--"); res.push_str(long); } if let Some(meta) = &self.meta { res.push(' '); res.push_str(meta); } if self.help.is_some() || self.default.is_some() { if res.len() < col_width { let n = col_width - res.len(); res.extend(repeat(' ').take(n)); } else { res.push('\n'); res.extend(repeat(' ').take(col_width)); } } if let Some(help) = &self.help { res.push_str(help); } if let Some(default) = &self.default { res.push_str(" (default: "); res.push_str(default); res.push_str(")"); } res } } impl ParseFn { fn parse(item: &NestedMeta) -> Result { let result = match item { NestedMeta::Meta(Meta::Path(path)) => { match path.get_ident() { Some(ident) => match ident.to_string().as_str() { "from_str" => ParseFn::FromStr(None), "try_from_str" => ParseFn::Default, _ => return Err(unexpected_meta_item(ident.span())) } None => return Err(unexpected_meta_item(path.span())) } } NestedMeta::Meta(Meta::NameValue(nv)) => { match nv.path.get_ident() { Some(ident) => match ident.to_string().as_str() { "from_str" => { let path = parse_str(&lit_str(&nv.lit)?)?; ParseFn::FromStr(Some(path)) } "try_from_str" => { let path = parse_str(&lit_str(&nv.lit)?)?; ParseFn::TryFromStr(path) } _ => return Err(unexpected_meta_item(nv.path.span())) } None => return Err(unexpected_meta_item(nv.path.span())) } } NestedMeta::Lit(_) | NestedMeta::Meta(Meta::List(_)) => return Err(unexpected_meta_item(item.span())), }; Ok(result) } fn make_parse_action(&self, name: Option<&str>) -> TokenStream2 { let name = if let Some(name) = name { quote!{ ::std::string::ToString::to_string(#name) } } else { quote!{ ::gumdrop::Opt::to_string(&_opt) } }; let res = match self { ParseFn::Default => quote!{ ::std::str::FromStr::from_str(_arg) .map_err(|e| ::gumdrop::Error::failed_parse_with_name( #name, ::std::string::ToString::to_string(&e)))? }, ParseFn::FromStr(None) => quote!{ ::std::convert::From::from(_arg) }, ParseFn::FromStr(Some(fun)) => quote!{ #fun(_arg) }, ParseFn::TryFromStr(fun) => quote!{ #fun(_arg) .map_err(|e| ::gumdrop::Error::failed_parse_with_name( #name, ::std::string::ToString::to_string(&e)))? } }; res } fn make_parse_default_action(&self, ident: &Ident, expr: &str) -> TokenStream2 { let res = match self { ParseFn::Default => quote!{ ::std::str::FromStr::from_str(#expr) .map_err(|e| ::gumdrop::Error::failed_parse_default( stringify!(#ident), #expr, ::std::string::ToString::to_string(&e)))? }, ParseFn::FromStr(None) => quote!{ ::std::convert::From::from(#expr) }, ParseFn::FromStr(Some(fun)) => quote!{ #fun(#expr) }, ParseFn::TryFromStr(fun) => quote!{ #fun(#expr) .map_err(|e| ::gumdrop::Error::failed_parse_default( stringify!(#ident), #expr, ::std::string::ToString::to_string(&e)))? } }; res } } impl Default for ParseFn { fn default() -> ParseFn { ParseFn::Default } } impl ParseMethod { fn make_action_type(&self) -> TokenStream2 { let parse = self.parse_fn.make_parse_action(None); match self.tuple_len { None => quote!{ { let _arg = _parser.next_arg() .ok_or_else(|| ::gumdrop::Error::missing_argument(_opt))?; #parse } }, Some(n) => { let num = 0..n; let n = repeat(n); let parse = repeat(parse); quote!{ ( #( { let _found = #num; let _arg = _parser.next_arg() .ok_or_else(|| ::gumdrop::Error::insufficient_arguments( _opt, #n, _found))?; #parse } , )* ) } } } } fn make_action_type_arg(&self) -> TokenStream2 { match self.tuple_len { None => self.parse_fn.make_parse_action(None), Some(_) => unreachable!() } } fn takes_arg(&self) -> bool { match self.tuple_len { Some(0) => false, _ => true } } } fn first_ty_param(ty: &Type) -> Option<&Type> { match ty { Type::Path(path) => { let path = path.path.segments.last().unwrap(); match &path.arguments { PathArguments::AngleBracketed(data) => data.args.iter().filter_map(|arg| match arg { GenericArgument::Type(ty) => Some(ty), _ => None }).next(), _ => None } } _ => None } } fn is_outer(style: AttrStyle) -> bool { match style { AttrStyle::Outer => true, _ => false } } fn lit_str(lit: &Lit) -> Result { match lit { Lit::Str(s) => Ok(s.value()), _ => Err(Error::new(lit.span(), "expected string literal")) } } fn lit_char(lit: &Lit) -> Result { match lit { Lit::Char(ch) => Ok(ch.value()), // Character literals in attributes are not necessarily allowed Lit::Str(s) => { let s = s.value(); let mut chars = s.chars(); let first = chars.next(); let second = chars.next(); match (first, second) { (Some(ch), None) => Ok(ch), _ => Err(Error::new(lit.span(), "expected one-character string literal")) } } _ => Err(Error::new(lit.span(), "expected character literal")) } } fn path_eq(path: &Path, s: &str) -> bool { path.segments.len() == 1 && { let seg = path.segments.first().unwrap(); match seg.arguments { PathArguments::None => seg.ident == s, _ => false } } } fn tuple_len(ty: &Type) -> Option { match ty { Type::Tuple(tup) => Some(tup.elems.len()), _ => None } } fn make_command_name(name: &str) -> String { let mut res = String::with_capacity(name.len()); for ch in name.chars() { if ch.is_lowercase() { res.push(ch); } else { if !res.is_empty() { res.push('-'); } res.extend(ch.to_lowercase()); } } res } fn make_long_name(name: &str) -> String { name.replace('_', "-") } fn make_short_name(name: &str, short: &[char]) -> Option { let first = name.chars().next().expect("empty field name"); if !short.contains(&first) { return Some(first); } let mut to_upper = first.to_uppercase(); let upper = to_upper.next().expect("empty to_uppercase"); if to_upper.next().is_some() { return None; } if !short.contains(&upper) { Some(upper) } else { None } } fn validate_long_name(span: Span, name: &str, names: &[String]) -> Result<(), Error> { if name.is_empty() || name.starts_with('-') || name.contains(char::is_whitespace) { Err(Error::new(span, "not a valid long option")) } else if names.iter().any(|n| n == name) { Err(Error::new(span, "duplicate option name")) } else { Ok(()) } } fn validate_short_name(span: Span, ch: char, names: &[char]) -> Result<(), Error> { if ch == '-' || ch.is_whitespace() { Err(Error::new(span, "not a valid short option")) } else if names.contains(&ch) { Err(Error::new(span, "duplicate option name")) } else { Ok(()) } } fn make_meta(name: &str, action: &Action) -> String { use std::fmt::Write; let mut name = name.replace('_', "-").to_uppercase(); match action.tuple_len() { Some(0) => unreachable!(), Some(1) | None => (), Some(2) => { name.push_str(" VALUE"); } Some(n) => { for i in 1..n { let _ = write!(name, " VALUE{}", i - 1); } } } name } fn make_usage(help: &Option, free: &[FreeOpt], opts: &[Opt]) -> String { let mut res = String::new(); if let Some(help) = help { res.push_str(help); res.push('\n'); } let width = max_width(free, |opt| opt.width()) .max(max_width(opts, |opt| opt.width())); if !free.is_empty() { if !res.is_empty() { res.push('\n'); } res.push_str("Positional arguments:\n"); for opt in free { let mut line = String::from(" "); line.push_str(&opt.field.to_string()); if let Some(help) = &opt.help { if line.len() < width { let n = width - line.len(); line.extend(repeat(' ').take(n)); } else { line.push('\n'); line.extend(repeat(' ').take(width)); } line.push_str(help); } res.push_str(&line); res.push('\n'); } } if !opts.is_empty() { if !res.is_empty() { res.push('\n'); } res.push_str("Optional arguments:\n"); for opt in opts { res.push_str(&opt.usage(width)); res.push('\n'); } } // Pop the last newline so the user may println!() the result. res.pop(); res } fn max_width(items: &[T], f: F) -> usize where F: Fn(&T) -> usize { const MIN_WIDTH: usize = 8; const MAX_WIDTH: usize = 30; let width = items.iter().filter_map(|item| { let w = f(item); if w > MAX_WIDTH { None } else { Some(w) } }).max().unwrap_or(0); width.max(MIN_WIDTH).min(MAX_WIDTH) } fn make_cmd_usage(cmds: &[Cmd]) -> String { let mut res = String::new(); let width = max_width(cmds, // Two spaces each, before and after |cmd| cmd.name.len() + 4); for cmd in cmds { let mut line = String::from(" "); line.push_str(&cmd.name); if let Some(help) = &cmd.help { if line.len() < width { let n = width - line.len(); line.extend(repeat(' ').take(n)); } else { line.push('\n'); line.extend(repeat(' ').take(width)); } line.push_str(help); } res.push_str(&line); res.push('\n'); } // Pop the last newline res.pop(); res } fn unexpected_meta_item(span: Span) -> Error { Error::new(span, "unexpected meta item") }