diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index fa068b4..a517f14 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -13,6 +13,7 @@ pub struct ArgumentsAttr { pub file: Option, pub exit_code: i32, pub parse_echo_style: bool, + pub options_first: bool, } impl Default for ArgumentsAttr { @@ -23,6 +24,7 @@ impl Default for ArgumentsAttr { file: None, exit_code: 1, parse_echo_style: false, + options_first: false, } } } @@ -55,6 +57,9 @@ impl ArgumentsAttr { "parse_echo_style" => { args.parse_echo_style = true; } + "options_first" => { + args.options_first = true; + } _ => return Err(meta.error("unrecognized argument for arguments attribute")), }; Ok(()) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 3b2fbdd..5e8d7f4 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -67,6 +67,20 @@ pub fn arguments(input: TokenStream) -> TokenStream { quote!(parser.next()?) }; + // If options_first is set and we find the first positional argument, we + // immediately return all of them. + let positional = if arguments_attr.options_first { + quote!( + // Unwrap is fine because this is called when we have just parsed a + // value and therefore are not partially within an option. + let mut values = parser.raw_args().unwrap().collect::>(); + values.insert(0, value); + Ok(Some(::uutils_args::Argument::MultiPositional(values))) + ) + } else { + quote!(Ok(Some(::uutils_args::Argument::Positional(value)))) + }; + let expanded = quote!( impl #impl_generics Arguments for #name #ty_generics #where_clause { const EXIT_CODE: i32 = #exit_code; @@ -91,7 +105,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { match arg { lexopt::Arg::Short(short) => { #short }, lexopt::Arg::Long(long) => { #long }, - lexopt::Arg::Value(value) => { Ok(Some(::uutils_args::Argument::Positional(value))) }, + lexopt::Arg::Value(value) => { #positional }, } } diff --git a/src/lib.rs b/src/lib.rs index 6fdfd42..5164734 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,7 @@ pub enum Argument { Help, Version, Positional(OsString), + MultiPositional(Vec), Custom(T), } @@ -143,6 +144,9 @@ impl ArgumentIter { Argument::Positional(arg) => { self.positional_arguments.push(arg); } + Argument::MultiPositional(args) => { + self.positional_arguments.extend(args); + } Argument::Custom(arg) => return Ok(Some(arg)), } } diff --git a/tests/options_first.rs b/tests/options_first.rs new file mode 100644 index 0000000..ee9bc73 --- /dev/null +++ b/tests/options_first.rs @@ -0,0 +1,62 @@ +use std::ffi::OsString; + +use uutils_args::{Arguments, Options}; + +#[test] +fn timeout_like() { + // The timeout coreutil has -v and a command argument + #[derive(Arguments)] + #[arguments(options_first)] + enum Arg { + #[arg("-v", "--verbose")] + Verbose, + } + + #[derive(Default)] + struct Settings { + verbose: bool, + } + + impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Verbose => self.verbose = true, + } + } + } + + let (settings, command) = Settings::default() + .parse(["timeout", "-v", "10", "foo", "-v"]) + .unwrap(); + + assert!(settings.verbose); + assert_eq!( + command, + vec![ + OsString::from("10"), + OsString::from("foo"), + OsString::from("-v") + ] + ); + + let (settings, command) = Settings::default() + .parse(["timeout", "10", "foo", "-v"]) + .unwrap(); + + assert!(!settings.verbose); + assert_eq!( + command, + vec![ + OsString::from("10"), + OsString::from("foo"), + OsString::from("-v") + ] + ); + + let (settings, command) = Settings::default() + .parse(["timeout", "--", "10", "-v"]) + .unwrap(); + + assert!(!settings.verbose); + assert_eq!(command, vec![OsString::from("10"), OsString::from("-v")]); +}