From c6d692c8b496a597c8ee7d52955d0273cd68155d Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 13:34:50 +0100 Subject: [PATCH 1/5] new positional argument system --- src/lib.rs | 1 + src/positional.rs | 400 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 src/positional.rs diff --git a/src/lib.rs b/src/lib.rs index 694bcdf..6fdfd42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod error; pub mod internal; +pub mod positional; mod value; #[cfg(doc)] diff --git a/src/positional.rs b/src/positional.rs new file mode 100644 index 0000000..b3af7e2 --- /dev/null +++ b/src/positional.rs @@ -0,0 +1,400 @@ +//! Parsing of positional arguments. +//! +//! The signature for parsing positional arguments is one of [`&'static str`], +//! [`Opt`], [`Many`] or a tuple of those. The [`Unpack::unpack`] method of +//! these types parses a `Vec` into the corresponding +//! [`Unpack::Output`] type. +//! +//! For example: +//! ``` +//! use std::ffi::OsString; +//! use uutils_args::positional::{Opt, Unpack}; +//! +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one")]).unwrap(); +//! assert_eq!(a, OsString::from("one")); +//! assert_eq!(b, None); +//! +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one"), OsString::from("two")]).unwrap(); +//! assert_eq!(a, OsString::from("one")); +//! assert_eq!(b, Some(OsString::from("two"))); +//! ``` +//! +//! Here are a few examples: +//! +//! ```ignore +//! () // no arguments +//! "FOO" // one required argument with output `OsString` +//! Opt("FOO") // one optional argument with output `Option` +//! Many("FOO") // one or more arguments with output `Vec` +//! Opt(Many("FOO")) // zero or more arguments with output `Vec` +//! ("FOO", "FOO") // two required arguments with output (`OsString`, `OsString`) +//! ``` +//! +//! This allows for the construction of complex signatures. The signature +//! +//! ```ignore +//! ("FOO", Opt(Many("BAR"))) +//! ``` +//! +//! specifies that there is first a required argument "FOO" and any number of +//! values for "BAR". +//! +//! However, not all combinations are supported by design. For example, the +//! signature +//! +//! ```ignore +//! (Many("FOO"), Many("BAR")) +//! ``` +//! +//! does not make sense, because it's unclear where the positional arguments +//! should go. The supported tuples implement [`Unpack`]. + +use crate::error::{Error, ErrorKind}; +use std::ffi::OsString; + +/// A required argument +type Req = &'static str; + +/// Makes it's argument optional +pub struct Opt(pub T); + +/// 1 or more arguments +pub struct Many(pub Req); + +/// Unpack a `Vec` into the output type +/// +/// See the [module documentation](crate::positional) for more information. +pub trait Unpack { + type Output: ToOptional; + fn unpack(&self, operands: Vec) -> Result; +} + +impl Unpack for () { + type Output = (); + + fn unpack(&self, operands: Vec) -> Result { + assert_empty(operands) + } +} + +impl Unpack for (T,) { + type Output = T::Output; + + fn unpack(&self, operands: Vec) -> Result { + self.0.unpack(operands) + } +} + +impl Unpack for Req { + type Output = OsString; + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_front(self, &mut operands)?; + assert_empty(operands)?; + Ok(arg) + } +} + +impl Unpack for Opt +where + T::Output: ToOptional, +{ + type Output = ::Out; + + fn unpack(&self, operands: Vec) -> Result { + Ok(if operands.is_empty() { + ::none() + } else { + self.0.unpack(operands)?.some() + }) + } +} + +impl Unpack for Many { + type Output = Vec; + + fn unpack(&self, operands: Vec) -> Result { + if operands.is_empty() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![self.0.into()]), + }); + } + Ok(operands) + } +} + +impl Unpack for (Req, T) { + type Output = (OsString, T::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_front(self.0, &mut operands)?; + let rest = self.1.unpack(operands)?; + Ok((arg, rest)) + } +} + +impl Unpack for (Req, Req, T) { + type Output = (OsString, OsString, T::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg1 = pop_front(self.0, &mut operands)?; + let arg2 = pop_front(self.1, &mut operands)?; + let rest = self.2.unpack(operands)?; + Ok((arg1, arg2, rest)) + } +} + +impl Unpack for (Opt, Req) { + type Output = ( as Unpack>::Output, ::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +impl Unpack for (Many, Req) { + type Output = (::Output, ::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +fn pop_front(name: &str, operands: &mut Vec) -> Result { + if operands.is_empty() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + }); + } + Ok(operands.remove(0)) +} + +fn pop_back(name: &str, operands: &mut Vec) -> Result { + operands.pop().ok_or_else(|| Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + }) +} + +fn assert_empty(mut operands: Vec) -> Result<(), Error> { + if let Some(arg) = operands.pop() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::UnexpectedArgument(arg), + }); + } + Ok(()) +} + +pub trait ToOptional { + type Out: ToOptional; + fn some(self) -> Self::Out; + fn none() -> Self::Out; +} + +impl ToOptional for OsString { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for () { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for Vec { + type Out = Self; + fn some(self) -> Self::Out { + self + } + fn none() -> Self::Out { + Vec::new() + } +} + +impl ToOptional for (T1, T2) { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for (T1, T2, T3) { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for Option { + type Out = Self; + fn some(self) -> Self::Out { + self + } + fn none() -> Self::Out { + None + } +} + +#[cfg(test)] +mod test { + use super::{Many, Opt, Unpack}; + use std::ffi::OsString; + + macro_rules! a { + ($e:expr, $t:ty) => { + let _: Result<$t, _> = $e.unpack(Vec::new()); + }; + } + + #[track_caller] + fn assert_ok(signature: &U, expected: U::Output, operands: [&str; N]) + where + U::Output: Eq + std::fmt::Debug, + { + let operands = operands.into_iter().map(Into::into).collect(); + assert_eq!(signature.unpack(operands).unwrap(), expected); + } + + #[track_caller] + fn assert_err(signature: &impl Unpack, operands: [&str; N]) { + let operands = operands.into_iter().map(Into::into).collect(); + assert!(signature.unpack(operands).is_err()); + } + + #[test] + fn compile_tests() { + // The five basic ones + a!((), ()); + a!("FOO", OsString); + a!(Opt("FOO"), Option); + a!(Many("FOO"), Vec); + a!(Opt(Many("FOO")), Vec); + + // Start building some tuples + a!(("FOO", "BAR"), (OsString, OsString)); + a!(("FOO", Opt("BAR")), (OsString, Option)); + a!(("FOO", Many("BAR")), (OsString, Vec)); + a!(("FOO", Opt(Many("BAR"))), (OsString, Vec)); + + // The other way around! + a!((Opt("FOO"), "BAR"), (Option, OsString)); + a!((Many("FOO"), "BAR"), (Vec, OsString)); + a!((Opt(Many("FOO")), "BAR"), (Vec, OsString)); + + // Longer tuples! + a!(("FOO", "BAR", "BAZ"), (OsString, OsString, OsString)); + a!( + ("FOO", "BAR", Opt("BAZ")), + (OsString, OsString, Option) + ); + a!( + ("FOO", "BAR", Many("BAZ")), + (OsString, OsString, Vec) + ); + a!( + ("FOO", "BAR", Opt(Many("BAZ"))), + (OsString, OsString, Vec) + ); + + // seq [FIRST [INCREMENT]] LAST + a!( + (Opt(("FIRST", Opt("INCREMENT"))), "LAST"), + (Option<(OsString, Option)>, OsString) + ); + + // mknod NAME TYPE [MAJOR MINOR] + a!( + ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))), + (OsString, OsString, Option<(OsString, OsString)>) + ); + + // chroot + a!( + ("NEWROOT", Opt(("COMMAND", Opt(Many("ARG"))))), + (OsString, Option<(OsString, Vec)>) + ); + } + + #[test] + fn unit() { + assert_ok(&(), (), []); + assert_err(&(), ["foo"]); + assert_err(&(), ["foo", "bar"]); + } + + #[test] + fn required() { + let s = "FOO"; + assert_err(&s, []); + assert_ok(&s, "foo".into(), ["foo"]); + assert_err(&s, ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } + + #[test] + fn optional() { + let s = Opt("FOO"); + assert_ok(&s, None, []); + assert_ok(&s, Some("foo".into()), ["foo"]); + assert_err(&s, ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } + + #[test] + fn many() { + let s = Many("FOO"); + assert_err(&s, []); + assert_ok(&s, vec!["foo".into()], ["foo"]); + assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); + assert_ok( + &s, + vec!["foo".into(), "bar".into(), "baz".into()], + ["foo", "bar", "baz"], + ); + } + + #[test] + fn opt_many() { + let s = Opt(Many("FOO")); + assert_ok(&s, vec![], []); + assert_ok(&s, vec!["foo".into()], ["foo"]); + assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); + assert_ok( + &s, + vec!["foo".into(), "bar".into(), "baz".into()], + ["foo", "bar", "baz"], + ); + } + + #[test] + fn req_req() { + let s = ("FOO", "BAR"); + assert_err(&s, []); + assert_err(&s, ["foo"]); + assert_ok(&s, ("foo".into(), "bar".into()), ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } +} From 291beb3b3b3c5d91fdcaf5122e16ed351c99a9bb Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 14:16:58 +0100 Subject: [PATCH 2/5] make the positional system generic over Vec --- src/error.rs | 8 +- src/positional.rs | 287 ++++++++++++++++++---------------------------- 2 files changed, 119 insertions(+), 176 deletions(-) diff --git a/src/error.rs b/src/error.rs index ff7a6f4..0da36e5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,7 +28,7 @@ pub enum ErrorKind { UnexpectedOption(String, Vec), /// No more positional arguments were expected, but one was given anyway. - UnexpectedArgument(OsString), + UnexpectedArgument(String), /// A value was passed to an option that didn't expect a value. UnexpectedValue { @@ -99,7 +99,7 @@ impl Display for ErrorKind { Ok(()) } ErrorKind::UnexpectedArgument(arg) => { - write!(f, "Found an invalid argument '{}'.", arg.to_string_lossy()) + write!(f, "Found an invalid argument '{}'.", arg) } ErrorKind::UnexpectedValue { option, value } => { write!( @@ -144,7 +144,9 @@ impl From for ErrorKind { match other { lexopt::Error::MissingValue { option } => Self::MissingValue { option }, lexopt::Error::UnexpectedOption(s) => Self::UnexpectedOption(s, Vec::new()), - lexopt::Error::UnexpectedArgument(s) => Self::UnexpectedArgument(s), + lexopt::Error::UnexpectedArgument(s) => { + Self::UnexpectedArgument(s.to_string_lossy().to_string()) + } lexopt::Error::UnexpectedValue { option, value } => { Self::UnexpectedValue { option, value } } diff --git a/src/positional.rs b/src/positional.rs index b3af7e2..c2dbaee 100644 --- a/src/positional.rs +++ b/src/positional.rs @@ -1,22 +1,27 @@ //! Parsing of positional arguments. //! -//! The signature for parsing positional arguments is one of [`&'static str`], -//! [`Opt`], [`Many`] or a tuple of those. The [`Unpack::unpack`] method of -//! these types parses a `Vec` into the corresponding -//! [`Unpack::Output`] type. +//! The signature for parsing positional arguments is one of `&'static str`, +//! [`Opt`], [`Many0`], [`Many1`] or a tuple of those. The [`Unpack::unpack`] +//! method of these types parses a `Vec` into the corresponding +//! [`Unpack::Output`] type. //! //! For example: //! ``` //! use std::ffi::OsString; //! use uutils_args::positional::{Opt, Unpack}; //! -//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one")]).unwrap(); -//! assert_eq!(a, OsString::from("one")); +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec!["one"]).unwrap(); +//! assert_eq!(a, "one"); //! assert_eq!(b, None); //! -//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one"), OsString::from("two")]).unwrap(); -//! assert_eq!(a, OsString::from("one")); -//! assert_eq!(b, Some(OsString::from("two"))); +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec!["one", "two"]).unwrap(); +//! assert_eq!(a, "one"); +//! assert_eq!(b, Some("two")); +//! +//! // It works for any `Vec`: +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![1, 2]).unwrap(); +//! assert_eq!(a, 1); +//! assert_eq!(b, Some(2)); //! ``` //! //! Here are a few examples: @@ -50,7 +55,7 @@ //! should go. The supported tuples implement [`Unpack`]. use crate::error::{Error, ErrorKind}; -use std::ffi::OsString; +use std::fmt::Debug; /// A required argument type Req = &'static str; @@ -59,61 +64,69 @@ type Req = &'static str; pub struct Opt(pub T); /// 1 or more arguments -pub struct Many(pub Req); +pub struct Many1(pub Req); + +/// 0 or more arguments +pub struct Many0(pub Req); /// Unpack a `Vec` into the output type /// /// See the [module documentation](crate::positional) for more information. pub trait Unpack { - type Output: ToOptional; - fn unpack(&self, operands: Vec) -> Result; + type Output; + fn unpack(&self, operands: Vec) -> Result, Error>; } impl Unpack for () { - type Output = (); + type Output = (); - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { assert_empty(operands) } } -impl Unpack for (T,) { - type Output = T::Output; +impl Unpack for (U,) { + type Output = U::Output; - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { self.0.unpack(operands) } } impl Unpack for Req { - type Output = OsString; + type Output = T; - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_front(self, &mut operands)?; assert_empty(operands)?; Ok(arg) } } -impl Unpack for Opt -where - T::Output: ToOptional, -{ - type Output = ::Out; +impl Unpack for Opt { + type Output = Option>; - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { Ok(if operands.is_empty() { - ::none() + None } else { - self.0.unpack(operands)?.some() + Some(self.0.unpack(operands)?) }) } } -impl Unpack for Many { - type Output = Vec; +impl Unpack for Many0 { + type Output = Vec; + + fn unpack(&self, operands: Vec) -> Result, Error> { + Ok(operands) + } +} + +impl Unpack for Many1 { + type Output = Vec; - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { if operands.is_empty() { return Err(Error { exit_code: 1, @@ -124,20 +137,20 @@ impl Unpack for Many { } } -impl Unpack for (Req, T) { - type Output = (OsString, T::Output); +impl Unpack for (Req, U) { + type Output = (T, U::Output); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_front(self.0, &mut operands)?; let rest = self.1.unpack(operands)?; Ok((arg, rest)) } } -impl Unpack for (Req, Req, T) { - type Output = (OsString, OsString, T::Output); +impl Unpack for (Req, Req, U) { + type Output = (T, T, U::Output); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg1 = pop_front(self.0, &mut operands)?; let arg2 = pop_front(self.1, &mut operands)?; let rest = self.2.unpack(operands)?; @@ -145,142 +158,87 @@ impl Unpack for (Req, Req, T) { } } -impl Unpack for (Opt, Req) { - type Output = ( as Unpack>::Output, ::Output); +impl Unpack for (Opt, Req) { + type Output = (Option<::Output>, T); + + fn unpack(&self, mut operands: Vec) -> Result, Error> { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +impl Unpack for (Many0, Req) { + type Output = (Vec, T); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_back(self.1, &mut operands)?; let rest = self.0.unpack(operands)?; Ok((rest, arg)) } } -impl Unpack for (Many, Req) { - type Output = (::Output, ::Output); +impl Unpack for (Many1, Req) { + type Output = (Vec, T); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_back(self.1, &mut operands)?; let rest = self.0.unpack(operands)?; Ok((rest, arg)) } } -fn pop_front(name: &str, operands: &mut Vec) -> Result { +fn pop_front(name: &str, operands: &mut Vec) -> Result { if operands.is_empty() { return Err(Error { exit_code: 1, - kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]), }); } Ok(operands.remove(0)) } -fn pop_back(name: &str, operands: &mut Vec) -> Result { +fn pop_back(name: &str, operands: &mut Vec) -> Result { operands.pop().ok_or_else(|| Error { exit_code: 1, - kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]), }) } -fn assert_empty(mut operands: Vec) -> Result<(), Error> { +fn assert_empty(mut operands: Vec) -> Result<(), Error> { if let Some(arg) = operands.pop() { return Err(Error { exit_code: 1, - kind: ErrorKind::UnexpectedArgument(arg), + kind: ErrorKind::UnexpectedArgument(format!("{:?}", arg)), }); } Ok(()) } -pub trait ToOptional { - type Out: ToOptional; - fn some(self) -> Self::Out; - fn none() -> Self::Out; -} - -impl ToOptional for OsString { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for () { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for Vec { - type Out = Self; - fn some(self) -> Self::Out { - self - } - fn none() -> Self::Out { - Vec::new() - } -} - -impl ToOptional for (T1, T2) { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for (T1, T2, T3) { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for Option { - type Out = Self; - fn some(self) -> Self::Out { - self - } - fn none() -> Self::Out { - None - } -} - #[cfg(test)] mod test { - use super::{Many, Opt, Unpack}; - use std::ffi::OsString; + use super::{Many0, Many1, Opt, Unpack}; macro_rules! a { ($e:expr, $t:ty) => { - let _: Result<$t, _> = $e.unpack(Vec::new()); + let _: Result<$t, _> = $e.unpack(Vec::<&str>::new()); }; } #[track_caller] - fn assert_ok(signature: &U, expected: U::Output, operands: [&str; N]) - where - U::Output: Eq + std::fmt::Debug, + fn assert_ok<'a, U: Unpack, const N: usize>( + signature: &U, + expected: U::Output<&'a str>, + operands: [&'a str; N], + ) where + U::Output<&'a str>: Eq + std::fmt::Debug, { - let operands = operands.into_iter().map(Into::into).collect(); - assert_eq!(signature.unpack(operands).unwrap(), expected); + assert_eq!(signature.unpack(Vec::from(operands)).unwrap(), expected); } #[track_caller] fn assert_err(signature: &impl Unpack, operands: [&str; N]) { - let operands = operands.into_iter().map(Into::into).collect(); + let operands = Vec::from(operands); assert!(signature.unpack(operands).is_err()); } @@ -288,53 +246,44 @@ mod test { fn compile_tests() { // The five basic ones a!((), ()); - a!("FOO", OsString); - a!(Opt("FOO"), Option); - a!(Many("FOO"), Vec); - a!(Opt(Many("FOO")), Vec); + a!("FOO", &str); + a!(Opt("FOO"), Option<&str>); + a!(Many0("FOO"), Vec<&str>); + a!(Many1("FOO"), Vec<&str>); // Start building some tuples - a!(("FOO", "BAR"), (OsString, OsString)); - a!(("FOO", Opt("BAR")), (OsString, Option)); - a!(("FOO", Many("BAR")), (OsString, Vec)); - a!(("FOO", Opt(Many("BAR"))), (OsString, Vec)); + a!(("FOO", "BAR"), (&str, &str)); + a!(("FOO", Opt("BAR")), (&str, Option<&str>)); + a!(("FOO", Many0("BAR")), (&str, Vec<&str>)); + a!(("FOO", Many1("BAR")), (&str, Vec<&str>)); // The other way around! - a!((Opt("FOO"), "BAR"), (Option, OsString)); - a!((Many("FOO"), "BAR"), (Vec, OsString)); - a!((Opt(Many("FOO")), "BAR"), (Vec, OsString)); + a!((Opt("FOO"), "BAR"), (Option<&str>, &str)); + a!((Many0("FOO"), "BAR"), (Vec<&str>, &str)); + a!((Many1("FOO"), "BAR"), (Vec<&str>, &str)); // Longer tuples! - a!(("FOO", "BAR", "BAZ"), (OsString, OsString, OsString)); - a!( - ("FOO", "BAR", Opt("BAZ")), - (OsString, OsString, Option) - ); - a!( - ("FOO", "BAR", Many("BAZ")), - (OsString, OsString, Vec) - ); - a!( - ("FOO", "BAR", Opt(Many("BAZ"))), - (OsString, OsString, Vec) - ); + a!(("FOO", "BAR", "BAZ"), (&str, &str, &str)); + a!(("FOO", "BAR", Opt("BAZ")), (&str, &str, Option<&str>)); + a!(("FOO", "BAR", Many0("BAZ")), (&str, &str, Vec<&str>)); + a!(("FOO", "BAR", Many1("BAZ")), (&str, &str, Vec<&str>)); // seq [FIRST [INCREMENT]] LAST a!( (Opt(("FIRST", Opt("INCREMENT"))), "LAST"), - (Option<(OsString, Option)>, OsString) + (Option<(&str, Option<&str>)>, &str) ); // mknod NAME TYPE [MAJOR MINOR] a!( ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))), - (OsString, OsString, Option<(OsString, OsString)>) + (&str, &str, Option<(&str, &str)>) ); // chroot a!( - ("NEWROOT", Opt(("COMMAND", Opt(Many("ARG"))))), - (OsString, Option<(OsString, Vec)>) + ("NEWROOT", Opt(("COMMAND", Many0("ARG")))), + (&str, Option<(&str, Vec<&str>)>) ); } @@ -349,7 +298,7 @@ mod test { fn required() { let s = "FOO"; assert_err(&s, []); - assert_ok(&s, "foo".into(), ["foo"]); + assert_ok(&s, "foo", ["foo"]); assert_err(&s, ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } @@ -358,35 +307,27 @@ mod test { fn optional() { let s = Opt("FOO"); assert_ok(&s, None, []); - assert_ok(&s, Some("foo".into()), ["foo"]); + assert_ok(&s, Some("foo"), ["foo"]); assert_err(&s, ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } #[test] - fn many() { - let s = Many("FOO"); + fn many1() { + let s = Many1("FOO"); assert_err(&s, []); - assert_ok(&s, vec!["foo".into()], ["foo"]); - assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); - assert_ok( - &s, - vec!["foo".into(), "bar".into(), "baz".into()], - ["foo", "bar", "baz"], - ); + assert_ok(&s, vec!["foo"], ["foo"]); + assert_ok(&s, vec!["foo", "bar"], ["foo", "bar"]); + assert_ok(&s, vec!["foo", "bar", "baz"], ["foo", "bar", "baz"]); } #[test] - fn opt_many() { - let s = Opt(Many("FOO")); + fn many0() { + let s = Many0("FOO"); assert_ok(&s, vec![], []); - assert_ok(&s, vec!["foo".into()], ["foo"]); - assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); - assert_ok( - &s, - vec!["foo".into(), "bar".into(), "baz".into()], - ["foo", "bar", "baz"], - ); + assert_ok(&s, vec!["foo"], ["foo"]); + assert_ok(&s, vec!["foo", "bar"], ["foo", "bar"]); + assert_ok(&s, vec!["foo", "bar", "baz"], ["foo", "bar", "baz"]); } #[test] @@ -394,7 +335,7 @@ mod test { let s = ("FOO", "BAR"); assert_err(&s, []); assert_err(&s, ["foo"]); - assert_ok(&s, ("foo".into(), "bar".into()), ["foo", "bar"]); + assert_ok(&s, ("foo", "bar"), ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } } From a31f9bef3978604fb43bc1f612023936eef42ef2 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 15:00:10 +0100 Subject: [PATCH 3/5] test the new positional argument system in coreutils tests --- tests/coreutils/base32.rs | 50 ++++++++++++++++++++----------------- tests/coreutils/basename.rs | 32 +++++++++++++++--------- tests/coreutils/ls.rs | 2 +- tests/coreutils/mktemp.rs | 47 +++++++++++++++++++++++----------- 4 files changed, 81 insertions(+), 50 deletions(-) diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index de32e74..24ce251 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -1,4 +1,9 @@ -use uutils_args::{Arguments, Options}; +use std::ffi::OsString; + +use uutils_args::{ + positional::{Opt, Unpack}, + Arguments, Options, +}; #[derive(Clone, Arguments)] enum Arg { @@ -39,30 +44,29 @@ impl Options for Settings { } } +fn parse(args: I) -> Result<(Settings, Option), uutils_args::Error> +where + I: IntoIterator, + I::Item: Into, +{ + let (s, ops) = Settings::default().parse(args)?; + let file = Opt("FILE").unpack(ops)?; + Ok((s, file)) +} + #[test] fn wrap() { + assert_eq!(parse(["base32"]).unwrap().0.wrap, Some(76)); + assert_eq!(parse(["base32", "-w0"]).unwrap().0.wrap, None); + assert_eq!(parse(["base32", "-w100"]).unwrap().0.wrap, Some(100)); + assert_eq!(parse(["base32", "--wrap=100"]).unwrap().0.wrap, Some(100)); +} + +#[test] +fn file() { + assert_eq!(parse(["base32"]).unwrap().1, None); assert_eq!( - Settings::default().parse(["base32"]).unwrap().0.wrap, - Some(76) - ); - assert_eq!( - Settings::default().parse(["base32", "-w0"]).unwrap().0.wrap, - None - ); - assert_eq!( - Settings::default() - .parse(["base32", "-w100"]) - .unwrap() - .0 - .wrap, - Some(100) - ); - assert_eq!( - Settings::default() - .parse(["base32", "--wrap=100"]) - .unwrap() - .0 - .wrap, - Some(100) + parse(["base32", "file"]).unwrap().1, + Some(OsString::from("file")) ); } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 7f0716e..9324fe3 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -1,6 +1,9 @@ use std::ffi::OsString; -use uutils_args::{Arguments, Options}; +use uutils_args::{ + positional::{Many1, Unpack}, + Arguments, Options, +}; #[derive(Clone, Arguments)] enum Arg { @@ -35,19 +38,24 @@ impl Options for Settings { } } -fn parse(args: &[&str]) -> Settings { - let (mut settings, operands) = Settings::default().parse(args).unwrap(); - settings.names = operands; - if !settings.multiple { - assert_eq!(settings.names.len(), 2); - settings.suffix = settings.names.pop().unwrap(); +fn parse(args: &[&str]) -> Result { + let (mut settings, operands) = Settings::default().parse(args)?; + + if settings.multiple { + let names = Many1("FILE").unpack(operands)?; + settings.names = names; + } else { + let (names, suffix) = ("FILE", "SUFFIX").unpack(operands)?; + settings.names = vec![names]; + settings.suffix = suffix; } - settings + + Ok(settings) } #[test] fn name_and_suffix() { - let settings = parse(&["basename", "foobar", "bar"]); + let settings = parse(&["basename", "foobar", "bar"]).unwrap(); assert!(!settings.zero); assert_eq!(settings.names, vec!["foobar"]); assert_eq!(settings.suffix, "bar"); @@ -55,7 +63,7 @@ fn name_and_suffix() { #[test] fn zero_name_and_suffix() { - let settings = parse(&["basename", "-z", "foobar", "bar"]); + let settings = parse(&["basename", "-z", "foobar", "bar"]).unwrap(); assert!(settings.zero); assert_eq!(settings.names, vec!["foobar"]); assert_eq!(settings.suffix, "bar"); @@ -63,7 +71,7 @@ fn zero_name_and_suffix() { #[test] fn all_and_names() { - let settings = parse(&["basename", "-a", "foobar", "bar"]); + let settings = parse(&["basename", "-a", "foobar", "bar"]).unwrap(); assert!(settings.multiple); assert!(!settings.zero); assert_eq!(settings.names, vec!["foobar", "bar"]); @@ -72,7 +80,7 @@ fn all_and_names() { #[test] fn option_like_names() { - let settings = parse(&["basename", "-a", "--", "-a", "-z", "--suffix=SUFFIX"]); + let settings = parse(&["basename", "-a", "--", "-a", "-z", "--suffix=SUFFIX"]).unwrap(); assert!(settings.multiple); assert!(!settings.zero); assert_eq!(settings.names, vec!["-a", "-z", "--suffix=SUFFIX"]); diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 681e83c..9673926 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -38,7 +38,7 @@ impl When { Self::Always => true, Self::Never => false, // Should be atty::is(atty::Stream::Stdout), but I don't want to - // pull that depenency in just for this test. + // pull that dependency in just for this test. Self::Auto => true, } } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index 1319709..2410f7d 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -1,6 +1,12 @@ -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, +}; -use uutils_args::{Arguments, Options}; +use uutils_args::{ + positional::{Opt, Unpack}, + Arguments, Options, +}; #[derive(Clone, Arguments)] enum Arg { @@ -46,39 +52,52 @@ impl Options for Settings { } } +fn parse(args: I) -> Result<(Settings, Option), uutils_args::Error> +where + I: IntoIterator, + I::Item: Into, +{ + let (s, ops) = Settings::default().parse(args)?; + let file = Opt("FILE").unpack(ops)?; + Ok((s, file)) +} + #[test] fn suffix() { - let (s, _operands) = Settings::default() - .parse(["mktemp", "--suffix=hello"]) - .unwrap(); + let (s, _template) = parse(["mktemp", "--suffix=hello"]).unwrap(); assert_eq!(s.suffix.unwrap(), "hello"); - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]).unwrap(); + let (s, _template) = parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]).unwrap(); + let (s, _template) = parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let (s, _operands) = Settings::default().parse(["mktemp"]).unwrap(); + let (s, _template) = parse(["mktemp"]).unwrap(); assert_eq!(s.suffix, None); } #[test] fn tmpdir() { - let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir"]).unwrap(); + let (s, _template) = parse(["mktemp", "--tmpdir"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new(".")); - let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir="]).unwrap(); + let (s, _template) = parse(["mktemp", "--tmpdir="]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - let (s, _operands) = Settings::default().parse(["mktemp", "-p", "foo"]).unwrap(); + let (s, _template) = parse(["mktemp", "-p", "foo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let (s, _operands) = Settings::default().parse(["mktemp", "-pfoo"]).unwrap(); + let (s, _template) = parse(["mktemp", "-pfoo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let (s, _operands) = Settings::default().parse(["mktemp", "-p", ""]).unwrap(); + let (s, _template) = parse(["mktemp", "-p", ""]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - assert!(Settings::default().parse(["mktemp", "-p"]).is_err()); + assert!(parse(["mktemp", "-p"]).is_err()); +} + +#[test] +fn too_many_arguments() { + assert!(parse(["mktemp", "foo", "bar"]).is_err()); } From 50bd56675b0d952b98e4aa2672a493a99d60b4a4 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 15:24:24 +0100 Subject: [PATCH 4/5] add test for seq and mknod positional args --- src/positional.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/positional.rs b/src/positional.rs index c2dbaee..b8b0640 100644 --- a/src/positional.rs +++ b/src/positional.rs @@ -30,15 +30,15 @@ //! () // no arguments //! "FOO" // one required argument with output `OsString` //! Opt("FOO") // one optional argument with output `Option` -//! Many("FOO") // one or more arguments with output `Vec` -//! Opt(Many("FOO")) // zero or more arguments with output `Vec` +//! Many1("FOO") // one or more arguments with output `Vec` +//! Many0("FOO") // zero or more arguments with output `Vec` //! ("FOO", "FOO") // two required arguments with output (`OsString`, `OsString`) //! ``` //! //! This allows for the construction of complex signatures. The signature //! //! ```ignore -//! ("FOO", Opt(Many("BAR"))) +//! ("FOO", Many0("BAR")) //! ``` //! //! specifies that there is first a required argument "FOO" and any number of @@ -48,7 +48,7 @@ //! signature //! //! ```ignore -//! (Many("FOO"), Many("BAR")) +//! (Many0("FOO"), Many0("BAR")) //! ``` //! //! does not make sense, because it's unclear where the positional arguments @@ -338,4 +338,24 @@ mod test { assert_ok(&s, ("foo", "bar"), ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } + + #[test] + fn seq() { + let s = (Opt(("FIRST", Opt("INCREMENT"))), "LAST"); + assert_err(&s, []); + assert_ok(&s, (None, "1"), ["1"]); + assert_ok(&s, (Some(("1", None)), "2"), ["1", "2"]); + assert_ok(&s, (Some(("1", Some("2"))), "3"), ["1", "2", "3"]); + assert_err(&s, ["1", "2", "3", "4"]); + } + + #[test] + fn mknod() { + let s = ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))); + assert_err(&s, []); + assert_err(&s, ["1"]); + assert_ok(&s, ("1", "2", None), ["1", "2"]); + assert_err(&s, ["1", "2", "3"]); + assert_ok(&s, ("1", "2", Some(("3", "4"))), ["1", "2", "3", "4"]); + } } From 5c7bf4e3a7e8e58ad06db2ecce99e249ec93121e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 10 Jan 2024 13:07:41 +0100 Subject: [PATCH 5/5] add options_first argument for the arguments attribute --- derive/src/attributes.rs | 5 ++++ derive/src/lib.rs | 16 ++++++++++- src/lib.rs | 4 +++ tests/options_first.rs | 62 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/options_first.rs 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")]); +}