Merge pull request #82 from tertsdiepraam/new-positional

New positional argument system
This commit is contained in:
Terts Diepraam
2024-02-05 14:50:54 +01:00
committed by GitHub
10 changed files with 534 additions and 54 deletions
+5
View File
@@ -13,6 +13,7 @@ pub struct ArgumentsAttr {
pub file: Option<String>,
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(())
+15 -1
View File
@@ -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::<Vec<OsString>>();
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 },
}
}
+5 -3
View File
@@ -28,7 +28,7 @@ pub enum ErrorKind {
UnexpectedOption(String, Vec<String>),
/// 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<lexopt::Error> 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 }
}
+5
View File
@@ -11,6 +11,7 @@
mod error;
pub mod internal;
pub mod positional;
mod value;
#[cfg(doc)]
@@ -64,6 +65,7 @@ pub enum Argument<T: Arguments> {
Help,
Version,
Positional(OsString),
MultiPositional(Vec<OsString>),
Custom(T),
}
@@ -142,6 +144,9 @@ impl<T: Arguments> ArgumentIter<T> {
Argument::Positional(arg) => {
self.positional_arguments.push(arg);
}
Argument::MultiPositional(args) => {
self.positional_arguments.extend(args);
}
Argument::Custom(arg) => return Ok(Some(arg)),
}
}
+361
View File
@@ -0,0 +1,361 @@
//! Parsing of positional arguments.
//!
//! 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<T>` into the corresponding
//! [`Unpack::Output<T>`] type.
//!
//! For example:
//! ```
//! use std::ffi::OsString;
//! use uutils_args::positional::{Opt, Unpack};
//!
//! 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!["one", "two"]).unwrap();
//! assert_eq!(a, "one");
//! assert_eq!(b, Some("two"));
//!
//! // It works for any `Vec<T>`:
//! 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:
//!
//! ```ignore
//! () // no arguments
//! "FOO" // one required argument with output `OsString`
//! Opt("FOO") // one optional argument with output `Option<OsString>`
//! Many1("FOO") // one or more arguments with output `Vec<OsString>`
//! Many0("FOO") // zero or more arguments with output `Vec<OsString>`
//! ("FOO", "FOO") // two required arguments with output (`OsString`, `OsString`)
//! ```
//!
//! This allows for the construction of complex signatures. The signature
//!
//! ```ignore
//! ("FOO", Many0("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
//! (Many0("FOO"), Many0("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::fmt::Debug;
/// A required argument
type Req = &'static str;
/// Makes it's argument optional
pub struct Opt<T>(pub T);
/// 1 or more arguments
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<T>;
fn unpack<T: Debug>(&self, operands: Vec<T>) -> Result<Self::Output<T>, Error>;
}
impl Unpack for () {
type Output<T> = ();
fn unpack<T: Debug>(&self, operands: Vec<T>) -> Result<Self::Output<T>, Error> {
assert_empty(operands)
}
}
impl<U: Unpack> Unpack for (U,) {
type Output<T> = U::Output<T>;
fn unpack<T: Debug>(&self, operands: Vec<T>) -> Result<Self::Output<T>, Error> {
self.0.unpack(operands)
}
}
impl Unpack for Req {
type Output<T> = T;
fn unpack<T: Debug>(&self, mut operands: Vec<T>) -> Result<Self::Output<T>, Error> {
let arg = pop_front(self, &mut operands)?;
assert_empty(operands)?;
Ok(arg)
}
}
impl<U: Unpack> Unpack for Opt<U> {
type Output<T> = Option<U::Output<T>>;
fn unpack<T: Debug>(&self, operands: Vec<T>) -> Result<Self::Output<T>, Error> {
Ok(if operands.is_empty() {
None
} else {
Some(self.0.unpack(operands)?)
})
}
}
impl Unpack for Many0 {
type Output<T> = Vec<T>;
fn unpack<T: Debug>(&self, operands: Vec<T>) -> Result<Self::Output<T>, Error> {
Ok(operands)
}
}
impl Unpack for Many1 {
type Output<T> = Vec<T>;
fn unpack<T: Debug>(&self, operands: Vec<T>) -> Result<Self::Output<T>, Error> {
if operands.is_empty() {
return Err(Error {
exit_code: 1,
kind: ErrorKind::MissingPositionalArguments(vec![self.0.into()]),
});
}
Ok(operands)
}
}
impl<U: Unpack> Unpack for (Req, U) {
type Output<T> = (T, U::Output<T>);
fn unpack<T: Debug>(&self, mut operands: Vec<T>) -> Result<Self::Output<T>, Error> {
let arg = pop_front(self.0, &mut operands)?;
let rest = self.1.unpack(operands)?;
Ok((arg, rest))
}
}
impl<U: Unpack> Unpack for (Req, Req, U) {
type Output<T> = (T, T, U::Output<T>);
fn unpack<T: Debug>(&self, mut operands: Vec<T>) -> Result<Self::Output<T>, Error> {
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<U: Unpack> Unpack for (Opt<U>, Req) {
type Output<T> = (Option<<U as Unpack>::Output<T>>, T);
fn unpack<T: Debug>(&self, mut operands: Vec<T>) -> Result<Self::Output<T>, 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<T> = (Vec<T>, T);
fn unpack<T: Debug>(&self, mut operands: Vec<T>) -> Result<Self::Output<T>, Error> {
let arg = pop_back(self.1, &mut operands)?;
let rest = self.0.unpack(operands)?;
Ok((rest, arg))
}
}
impl Unpack for (Many1, Req) {
type Output<T> = (Vec<T>, T);
fn unpack<T: Debug>(&self, mut operands: Vec<T>) -> Result<Self::Output<T>, Error> {
let arg = pop_back(self.1, &mut operands)?;
let rest = self.0.unpack(operands)?;
Ok((rest, arg))
}
}
fn pop_front<T: Debug>(name: &str, operands: &mut Vec<T>) -> Result<T, Error> {
if operands.is_empty() {
return Err(Error {
exit_code: 1,
kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]),
});
}
Ok(operands.remove(0))
}
fn pop_back<T: Debug>(name: &str, operands: &mut Vec<T>) -> Result<T, Error> {
operands.pop().ok_or_else(|| Error {
exit_code: 1,
kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]),
})
}
fn assert_empty<T: Debug>(mut operands: Vec<T>) -> Result<(), Error> {
if let Some(arg) = operands.pop() {
return Err(Error {
exit_code: 1,
kind: ErrorKind::UnexpectedArgument(format!("{:?}", arg)),
});
}
Ok(())
}
#[cfg(test)]
mod test {
use super::{Many0, Many1, Opt, Unpack};
macro_rules! a {
($e:expr, $t:ty) => {
let _: Result<$t, _> = $e.unpack(Vec::<&str>::new());
};
}
#[track_caller]
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,
{
assert_eq!(signature.unpack(Vec::from(operands)).unwrap(), expected);
}
#[track_caller]
fn assert_err<const N: usize>(signature: &impl Unpack, operands: [&str; N]) {
let operands = Vec::from(operands);
assert!(signature.unpack(operands).is_err());
}
#[test]
fn compile_tests() {
// The five basic ones
a!((), ());
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"), (&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<&str>, &str));
a!((Many0("FOO"), "BAR"), (Vec<&str>, &str));
a!((Many1("FOO"), "BAR"), (Vec<&str>, &str));
// Longer tuples!
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<(&str, Option<&str>)>, &str)
);
// mknod NAME TYPE [MAJOR MINOR]
a!(
("NAME", "TYPE", Opt(("MAJOR", "MINOR"))),
(&str, &str, Option<(&str, &str)>)
);
// chroot
a!(
("NEWROOT", Opt(("COMMAND", Many0("ARG")))),
(&str, Option<(&str, Vec<&str>)>)
);
}
#[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", ["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"), ["foo"]);
assert_err(&s, ["foo", "bar"]);
assert_err(&s, ["foo", "bar", "baz"]);
}
#[test]
fn many1() {
let s = Many1("FOO");
assert_err(&s, []);
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 many0() {
let s = Many0("FOO");
assert_ok(&s, vec![], []);
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 req_req() {
let s = ("FOO", "BAR");
assert_err(&s, []);
assert_err(&s, ["foo"]);
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"]);
}
}
+27 -23
View File
@@ -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<Arg> for Settings {
}
}
fn parse<I>(args: I) -> Result<(Settings, Option<OsString>), uutils_args::Error>
where
I: IntoIterator,
I::Item: Into<OsString>,
{
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"))
);
}
+20 -12
View File
@@ -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<Arg> 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<Settings, uutils_args::Error> {
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"]);
+1 -1
View File
@@ -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,
}
}
+33 -14
View File
@@ -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<Arg> for Settings {
}
}
fn parse<I>(args: I) -> Result<(Settings, Option<OsString>), uutils_args::Error>
where
I: IntoIterator,
I::Item: Into<OsString>,
{
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());
}
+62
View File
@@ -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<Arg> 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")]);
}