diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 7cf3c3b..46c3289 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -6,7 +6,7 @@ use syn::{Attribute, Fields, FieldsUnnamed, Ident, Meta, Variant}; use crate::{ attributes::{parse_argument_attribute, ArgAttr, ArgumentsAttr}, - flags::{Flag, Flags, Value}, + flags::{Flags, Value}, }; pub(crate) struct Argument { @@ -27,6 +27,9 @@ pub(crate) enum ArgType { num_args: RangeInclusive, last: bool, }, + Free { + filters: Vec, + }, } pub(crate) fn parse_arguments_attr(attrs: &[Attribute]) -> ArgumentsAttr { @@ -93,6 +96,9 @@ pub(crate) fn parse_argument(v: Variant) -> Vec { last: pos.last, } } + ArgAttr::Free(free) => ArgType::Free { + filters: free.filters, + }, }; Argument { ident: ident.clone(), @@ -129,7 +135,11 @@ fn collect_help(attrs: &[Attribute]) -> String { fn get_arg_attributes(attrs: &[Attribute]) -> syn::Result> { attrs .iter() - .filter(|a| a.path().is_ident("option") || a.path().is_ident("positional")) + .filter(|a| { + a.path().is_ident("option") + || a.path().is_ident("positional") + || a.path().is_ident("free") + }) .map(parse_argument_attribute) .collect() } @@ -147,6 +157,7 @@ pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { hidden: _, } => (flags, takes_value, default), ArgType::Positional { .. } => continue, + ArgType::Free { .. } => continue, }; if flags.short.is_empty() { @@ -196,6 +207,7 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea hidden: _, } => (flags, takes_value, default), ArgType::Positional { .. } => continue, + ArgType::Free { .. } => continue, }; if flags.long.is_empty() { @@ -269,27 +281,65 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea ) } -pub(crate) fn number_handling(args: &[Argument]) -> TokenStream { - let mut number_args = Vec::new(); +pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { + let mut if_expressions = Vec::new(); - for arg in args { - let flags = match &arg.arg_type { + // Free arguments + for arg @ Argument { arg_type, .. } in args { + let filters = match arg_type { + ArgType::Free { filters } => filters, + ArgType::Positional { .. } => continue, + ArgType::Option { .. } => continue, + }; + + for filter in filters { + let ident = &arg.ident; + + if_expressions.push(quote!( + if let Some(inner) = #filter(arg) { + let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(inner))?; + let _ = raw.next(); + return Ok(Some(Argument::Custom(Self::#ident(value)))); + } + )); + } + } + + // dd-style arguments + let mut dd_branches = Vec::new(); + for arg @ Argument { arg_type, .. } in args { + let flags = match arg_type { ArgType::Option { flags, .. } => flags, + ArgType::Free { .. } => continue, ArgType::Positional { .. } => continue, }; - let ident = &arg.ident; + for (prefix, _) in &flags.dd_style { + let ident = &arg.ident; - for Flag { flag: prefix, .. } in &flags.number_prefix { - number_args.push(quote!( - if let Some(v) = ::uutils_args::parse_prefix(parser, #prefix) { - return Ok(Some(::uutils_args::Argument::Custom(Self::#ident(v)))); + dd_branches.push(quote!( + if prefix == #prefix { + let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(value))?; + let _ = raw.next(); + return Ok(Some(Argument::Custom(Self::#ident(value)))); } )); } } - quote!(#(#number_args)*) + if !dd_branches.is_empty() { + if_expressions.push(quote!( + if let Some((prefix, value)) = arg.split_once('=') { + #(#dd_branches)* + } + )); + } + + quote!(if let Some(mut raw) = parser.try_raw_args() { + if let Some(arg) = raw.peek().and_then(|s| s.to_str()) { + #(#if_expressions)* + } + }) } pub(crate) fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) { @@ -306,6 +356,7 @@ pub(crate) fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStrea let (num_args, last) = match arg_type { ArgType::Positional { num_args, last } => (num_args, last), ArgType::Option { .. } => continue, + ArgType::Free { .. } => continue, }; if *num_args.start() > 0 { diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index 076f4d3..2a19604 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -10,6 +10,7 @@ use crate::flags::Flags; pub(crate) enum ArgAttr { Option(OptionAttr), Positional(PositionalAttr), + Free(FreeAttr), } pub(crate) fn parse_argument_attribute(attr: &Attribute) -> syn::Result { @@ -17,6 +18,8 @@ pub(crate) fn parse_argument_attribute(attr: &Attribute) -> syn::Result Ok(ArgAttr::Option(OptionAttr::parse(attr)?)) } else if attr.path().is_ident("positional") { Ok(ArgAttr::Positional(PositionalAttr::parse(attr)?)) + } else if attr.path().is_ident("free") { + Ok(ArgAttr::Free(FreeAttr::parse(attr)?)) } else { panic!("Internal error: invalid argument attribute"); } @@ -185,6 +188,25 @@ impl OptionAttr { } } +#[derive(Default)] +pub(crate) struct FreeAttr { + pub(crate) filters: Vec, +} + +impl FreeAttr { + pub(crate) fn parse(attr: &Attribute) -> syn::Result { + let mut free_attr = FreeAttr::default(); + + parse_args(attr, |s: ParseStream| { + let ident = s.parse::()?; + free_attr.filters.push(ident); + Ok(()) + })?; + + Ok(free_attr) + } +} + #[derive(Default)] pub(crate) struct ValueAttr { pub(crate) keys: Vec, diff --git a/derive/src/flags.rs b/derive/src/flags.rs index 86abc50..b999f0a 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -5,7 +5,7 @@ use quote::quote; pub(crate) struct Flags { pub short: Vec>, pub long: Vec>, - pub number_prefix: Vec>, + pub dd_style: Vec<(String, String)>, } #[derive(Clone)] @@ -31,19 +31,7 @@ impl Flags { } pub(crate) fn add(&mut self, flag: &str) { - if let Some((prefix, rest)) = flag.split_once('{') { - // It's an argument with a prefix, e.g. "-{N}" or "+{N}" - let (value_name, after) = rest - .split_once('}') - .expect("Missing '}' in flag definition"); - - assert!(after.is_empty(), "There can be no suffix after '}}'"); - - self.number_prefix.push(Flag { - flag: prefix.into(), - value: Value::Required(value_name.into()), - }); - } else if let Some(s) = flag.strip_prefix("--") { + if let Some(s) = flag.strip_prefix("--") { // There are three possible patterns: // --flag // --flag=value @@ -110,11 +98,17 @@ impl Flags { panic!("Invalid short flag '{flag}'") }; self.short.push(Flag { flag: f, value }); + } else if let Some((s, v)) = flag.split_once('=') { + // It's a dd-style argument: arg=value + assert!(!s.is_empty()); + assert!(!v.is_empty()); + + self.dd_style.push((s.into(), v.into())); } } pub(crate) fn is_empty(&self) -> bool { - self.short.is_empty() && self.long.is_empty() && self.number_prefix.is_empty() + self.short.is_empty() && self.long.is_empty() && self.dd_style.is_empty() } pub(crate) fn pat(&self) -> TokenStream { diff --git a/derive/src/help.rs b/derive/src/help.rs index 50cb0aa..a853b77 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -49,6 +49,8 @@ pub(crate) fn help_string( // Hidden arguments should not show up in --help ArgType::Option { hidden: true, .. } => {} ArgType::Positional { .. } => {} + // TODO: Free arguments should show up in help + ArgType::Free { .. } => {} } } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 1f3c128..e78c358 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -6,7 +6,7 @@ mod help_parser; mod initial; use argument::{ - long_handling, number_handling, parse_argument, parse_arguments_attr, positional_handling, + free_handling, long_handling, parse_argument, parse_arguments_attr, positional_handling, short_handling, }; use attributes::ValueAttr; @@ -21,7 +21,7 @@ pub fn initial(input: TokenStream) -> TokenStream { initial::initial(input) } -#[proc_macro_derive(Arguments, attributes(flag, option, positional, arguments))] +#[proc_macro_derive(Arguments, attributes(flag, option, positional, free, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -38,7 +38,8 @@ pub fn arguments(input: TokenStream) -> TokenStream { let exit_code = arguments_attr.exit_code; let (short, short_flags) = short_handling(&arguments); let long = long_handling(&arguments, &arguments_attr.help_flags); - let number_argument = number_handling(&arguments); + // let number_argument = number_handling(&arguments); + let free = free_handling(&arguments); let (positional, missing_argument_checks) = positional_handling(&arguments); let help_string = help_string( &arguments, @@ -76,7 +77,8 @@ pub fn arguments(input: TokenStream) -> TokenStream { ) -> Result>, uutils_args::Error> { use uutils_args::{Value, lexopt, Error, Argument}; - #number_argument + // #number_argment + #free let arg = match { #next_arg } { Some(arg) => arg, diff --git a/examples/deprecated.rs b/examples/deprecated.rs new file mode 100644 index 0000000..0e5cdde --- /dev/null +++ b/examples/deprecated.rs @@ -0,0 +1,50 @@ +use uutils_args::{Arguments, Initial, Options}; + +fn parse_minus(s: &str) -> Option<&str> { + let num = s.strip_prefix('-')?; + if num.chars().all(|c| c.is_ascii_digit()) { + Some(num) + } else { + None + } +} +fn parse_plus(s: &str) -> Option<&str> { + let num = s.strip_prefix('+')?; + let num2 = num.strip_prefix('-').unwrap_or(num); + if num2.chars().all(|c| c.is_ascii_digit()) { + Some(num) + } else { + None + } +} + +#[derive(Arguments)] +enum Arg { + #[free(parse_minus)] + Min(usize), + + #[free(parse_plus)] + Plus(isize), +} + +#[derive(Initial)] +struct Settings { + n1: usize, + n2: isize, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Min(n) => self.n1 = n, + Arg::Plus(n) => self.n2 = n, + } + } +} + +fn main() { + assert_eq!(Settings::parse(["test", "-10"]).n1, 10usize); + assert!(Settings::try_parse(["test", "--10"]).is_err()); + assert_eq!(Settings::parse(["test", "+10"]).n2, 10isize); + assert_eq!(Settings::parse(["test", "+-10"]).n2, -10isize); +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs index a225951..d37425c 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -8,7 +8,7 @@ enum Arg { /// Just to show off, I can do multiple paragraphs and wrap text! /// /// # Also headings! - #[option("-n NAME", "--name=NAME")] + #[option("-n NAME", "--name=NAME", "name=NAME")] Name(String), /// The **number of times** to `greet` diff --git a/tests/options.rs b/tests/options.rs index 6b094b7..451c43c 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -425,12 +425,30 @@ fn infer_value() { #[test] fn deprecated() { + fn parse_minus(s: &str) -> Option<&str> { + let num = s.strip_prefix('-')?; + if num.chars().all(|c| c.is_ascii_digit()) { + Some(num) + } else { + None + } + } + fn parse_plus(s: &str) -> Option<&str> { + let num = s.strip_prefix('+')?; + let num2 = num.strip_prefix('-').unwrap_or(num); + if num2.chars().all(|c| c.is_ascii_digit()) { + Some(num) + } else { + None + } + } + #[derive(Arguments)] enum Arg { - #[option("-{N}")] + #[free(parse_minus)] Min(usize), - #[option("+{N}")] + #[free(parse_plus)] Plus(isize), }