take positional arguments out of arguments enum

This commit is contained in:
Terts Diepraam
2023-12-14 12:28:24 +01:00
parent 932a4712ad
commit 462898ca91
25 changed files with 285 additions and 690 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features --workspace -- -D warnings
args: --all-targets --features complete --workspace -- -D warnings
docs:
name: Docs
+13 -25
View File
@@ -7,13 +7,13 @@ behaviour that aligns with GNU coreutils.
## Features
- A derive macro for declarative argument definition.
- Automatic help generation.
- Positional and optional arguments.
- Automatically parsing values into Rust types.
- Define a custom exit code on errors.
- Automatically accept unambiguous abbreviations of long options.
- Handles invalid UTF-8 gracefully.
- A derive macro for declarative argument definition.
- Automatic help generation.
- Positional and optional arguments.
- Automatically parsing values into Rust types.
- Define a custom exit code on errors.
- Automatically accept unambiguous abbreviations of long options.
- Handles invalid UTF-8 gracefully.
## When you should not use this library
@@ -50,16 +50,11 @@ enum Arg {
/// Transform input text to uppercase
#[arg("-c", "--caps")]
Caps,
// This option takes a value:
// This option takes a value:
/// Add exclamation marks to output
#[arg("-e N", "--exclaim=N")]
ExclamationMarks(u8),
// This is a positional argument, the range specifies that
// at least one positional argument must be passed.
#[arg("TEXT", 1..)]
Text(String),
}
#[derive(Default)]
@@ -76,24 +71,17 @@ impl Options<Arg> for Settings {
match arg {
Arg::Caps => self.caps = true,
Arg::ExclamationMarks(n) => self.exclamation_marks += n,
Arg::Text(s) => {
if self.text.is_empty() {
self.text.push_str(&s);
} else {
self.text.push(' ');
self.text.push_str(&s);
}
}
}
}
}
fn run(args: &[&str]) -> String {
let s = Settings::default().parse(args);
let (s, operands) = Settings::default().parse(args);
let text = operands.iter().map(|s| s.to_string_lossy()).collect::<Vec<_>>().join(" ");
let mut output = if s.caps {
s.text.to_uppercase()
text.to_uppercase()
} else {
s.text
text
};
for i in 0..s.exclamation_marks {
output.push('!');
-104
View File
@@ -1,8 +1,6 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ops::RangeInclusive;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Fields, FieldsUnnamed, Ident, Meta, Variant};
@@ -27,10 +25,6 @@ pub enum ArgType {
takes_value: bool,
default: TokenStream,
},
Positional {
num_args: RangeInclusive<usize>,
last: bool,
},
Free {
filters: Vec<syn::Ident>,
},
@@ -93,13 +87,6 @@ pub fn parse_argument(v: Variant) -> Vec<Argument> {
hidden: opt.hidden,
}
}
ArgAttr::Positional(pos) => {
assert!(field.is_some(), "Positional arguments must have a field");
ArgType::Positional {
num_args: pos.num_args,
last: pos.last,
}
}
ArgAttr::Free(free) => ArgType::Free {
filters: free.filters,
},
@@ -157,7 +144,6 @@ pub fn short_handling(args: &[Argument]) -> (TokenStream, Vec<char>) {
ref default,
hidden: _,
} => (flags, takes_value, default),
ArgType::Positional { .. } => continue,
ArgType::Free { .. } => continue,
};
@@ -207,7 +193,6 @@ pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream {
ref default,
hidden: _,
} => (flags, takes_value, default),
ArgType::Positional { .. } => continue,
ArgType::Free { .. } => continue,
};
@@ -275,7 +260,6 @@ pub fn free_handling(args: &[Argument]) -> TokenStream {
for arg @ Argument { arg_type, .. } in args {
let filters = match arg_type {
ArgType::Free { filters } => filters,
ArgType::Positional { .. } => continue,
ArgType::Option { .. } => continue,
};
@@ -299,7 +283,6 @@ pub fn free_handling(args: &[Argument]) -> TokenStream {
let flags = match arg_type {
ArgType::Option { flags, .. } => flags,
ArgType::Free { .. } => continue,
ArgType::Positional { .. } => continue,
};
for (prefix, _) in &flags.dd_style {
@@ -338,74 +321,6 @@ pub fn free_handling(args: &[Argument]) -> TokenStream {
)
}
pub fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) {
let mut match_arms = Vec::new();
// The largest index of the previous argument, so the the argument after this should
// belong to the next argument.
let mut last_index = 0;
// The minimum number of arguments needed to not return a missing argument error.
let mut minimum_needed = 0;
let mut missing_argument_checks = vec![];
for arg @ Argument { name, arg_type, .. } in args {
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 {
minimum_needed = last_index + num_args.start();
missing_argument_checks.push(quote!(if positional_idx < #minimum_needed {
missing.push(#name);
}));
}
last_index += num_args.end();
let expr = if *last {
last_positional_expression(&arg.ident)
} else {
positional_expression(&arg.ident)
};
match_arms.push(quote!(0..=#last_index => { #expr }));
}
let value_handling = quote!(
*positional_idx += 1;
Ok(Some(Argument::Custom(
match positional_idx {
#(#match_arms)*
_ => return Err(lexopt::Arg::Value(value).unexpected().into()),
}
)))
);
let missing_argument_checks = quote!(
// We have the minimum number of required arguments overall.
// So we don't need to check the others.
if positional_idx >= #minimum_needed {
return Ok(());
}
let mut missing: Vec<&str> = vec![];
#(#missing_argument_checks)*
if !missing.is_empty() {
Err(uutils_args::Error {
exit_code: Self::EXIT_CODE,
kind: uutils_args::ErrorKind::MissingPositionalArguments(
missing.iter().map(ToString::to_string).collect::<Vec<String>>()
)
})
} else {
Ok(())
}
);
(value_handling, missing_argument_checks)
}
fn no_value_expression(ident: &Ident) -> TokenStream {
quote!(Self::#ident)
}
@@ -424,22 +339,3 @@ fn optional_value_expression(ident: &Ident, default_expr: &TokenStream) -> Token
fn required_value_expression(ident: &Ident) -> TokenStream {
quote!(Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &parser.value()?)?))
}
fn positional_expression(ident: &Ident) -> TokenStream {
// TODO: Add option name in this from_value call
quote!(
Self::#ident(::uutils_args::internal::parse_value_for_option("", &value)?)
)
}
fn last_positional_expression(ident: &Ident) -> TokenStream {
// TODO: Add option name in this from_value call
quote!({
let raw_args = parser.raw_args()?;
let collection = std::iter::once(value)
.chain(raw_args)
.map(|v| ::uutils_args::internal::parse_value_for_option("", &v))
.collect::<Result<_,_>>()?;
Self::#ident(collection)
})
}
+2 -82
View File
@@ -1,11 +1,8 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ops::RangeInclusive;
use syn::{
meta::ParseNestedMeta, parse::ParseStream, Attribute, Expr, ExprLit, ExprRange, Ident, Lit,
LitInt, LitStr, RangeLimits, Token,
meta::ParseNestedMeta, parse::ParseStream, Attribute, Expr, Ident, LitInt, LitStr, Token,
};
use crate::flags::Flags;
@@ -69,7 +66,6 @@ impl ArgumentsAttr {
pub enum ArgAttr {
Option(OptionAttr),
Positional(PositionalAttr),
Free(FreeAttr),
}
@@ -84,7 +80,7 @@ impl ArgAttr {
if v.starts_with('-') || v.contains('=') {
OptionAttr::from_args(v, s).map(Self::Option)
} else {
PositionalAttr::from_args(v, s).map(Self::Positional)
panic!("Could not determine type of argument");
}
} else if let Ok(v) = s.parse::<syn::Ident>() {
FreeAttr::from_args(v, s).map(Self::Free)
@@ -170,82 +166,6 @@ impl FreeAttr {
}
}
pub struct PositionalAttr {
pub num_args: RangeInclusive<usize>,
pub last: bool,
}
impl Default for PositionalAttr {
fn default() -> Self {
Self {
num_args: 1..=1,
last: false,
}
}
}
impl PositionalAttr {
pub fn from_args(_first_value: String, s: ParseStream) -> syn::Result<Self> {
let mut positional_attr = Self::default();
parse_args(s, |s| {
if (s.peek(LitInt) && s.peek2(Token![..])) || s.peek(Token![..]) {
let range = s.parse::<ExprRange>()?;
// We're dealing with a range
let from = match range.start.as_deref() {
Some(Expr::Lit(ExprLit {
lit: Lit::Int(i), ..
})) => i.base10_parse::<usize>().unwrap(),
None => 0,
_ => panic!("Range must consist of usize"),
};
let inclusive = matches!(range.limits, RangeLimits::Closed(_));
let to = match range.end.as_deref() {
Some(Expr::Lit(ExprLit {
lit: Lit::Int(i), ..
})) => {
let n = i.base10_parse::<usize>().unwrap();
if inclusive {
Some(n)
} else {
Some(n - 1)
}
}
None => None,
_ => panic!("Range must consist of usize"),
};
positional_attr.num_args = match to {
Some(to) => from..=to,
None => from..=usize::MAX,
};
return Ok(());
}
if let Ok(int) = s.parse::<LitInt>() {
let suffix = int.suffix();
// FIXME: should be a proper error instead of assert!
assert!(
suffix.is_empty() || suffix == "usize",
"The position index must be usize"
);
let n = int.base10_parse::<usize>().unwrap();
positional_attr.num_args = n..=n;
return Ok(());
}
let ident = s.parse::<Ident>()?;
match ident.to_string().as_str() {
"last" => positional_attr.last = true,
_ => return Err(s.error("unrecognized keyword in value attribute")),
}
Ok(())
})?;
Ok(positional_attr)
}
}
#[derive(Default)]
pub struct ValueAttr {
pub keys: Vec<String>,
-1
View File
@@ -51,7 +51,6 @@ pub 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 { .. } => {}
}
+4 -11
View File
@@ -9,8 +9,7 @@ mod help;
mod help_parser;
use argument::{
free_handling, long_handling, parse_argument, parse_arguments_attr, positional_handling,
short_handling,
free_handling, long_handling, parse_argument, parse_arguments_attr, short_handling,
};
use attributes::ValueAttr;
use help::{help_handling, help_string, version_handling};
@@ -36,9 +35,7 @@ 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 free = free_handling(&arguments);
let (positional, missing_argument_checks) = positional_handling(&arguments);
let help_string = help_string(
&arguments,
&arguments_attr.help_flags,
@@ -72,9 +69,9 @@ pub fn arguments(input: TokenStream) -> TokenStream {
#[allow(unreachable_code)]
fn next_arg(
parser: &mut uutils_args::lexopt::Parser, positional_idx: &mut usize
parser: &mut ::uutils_args::lexopt::Parser
) -> Result<Option<::uutils_args::Argument<Self>>, ::uutils_args::ErrorKind> {
use uutils_args::{Value, lexopt, Error, Argument};
use ::uutils_args::{Value, lexopt, Error, Argument};
#free
@@ -90,14 +87,10 @@ pub fn arguments(input: TokenStream) -> TokenStream {
match arg {
lexopt::Arg::Short(short) => { #short },
lexopt::Arg::Long(long) => { #long },
lexopt::Arg::Value(value) => { #positional },
lexopt::Arg::Value(value) => { Ok(Some(::uutils_args::Argument::Positional(value))) },
}
}
fn check_missing(positional_idx: usize) -> Result<(), uutils_args::Error> {
#missing_argument_checks
}
fn help(bin_name: &str) -> ::std::io::Result<()> {
#help_string
}
+3 -3
View File
@@ -43,8 +43,8 @@ impl Options<Arg> for Settings {
}
fn main() {
assert_eq!(Settings::default().parse(["test", "-10"]).n1, 10usize);
assert_eq!(Settings::default().parse(["test", "-10"]).0.n1, 10usize);
assert!(Settings::default().try_parse(["test", "--10"]).is_err());
assert_eq!(Settings::default().parse(["test", "+10"]).n2, 10isize);
assert_eq!(Settings::default().parse(["test", "+-10"]).n2, -10isize);
assert_eq!(Settings::default().parse(["test", "+10"]).0.n2, 10isize);
assert_eq!(Settings::default().parse(["test", "+-10"]).0.n2, -10isize);
}
+1 -1
View File
@@ -32,7 +32,7 @@ impl Options<Arg> for Settings {
}
fn main() -> Result<(), uutils_args::Error> {
let settings = Settings {
let (settings, _operands) = Settings {
name: String::new(),
count: 1,
}
+2 -2
View File
@@ -33,6 +33,6 @@ impl Options<Arg> for Settings {
}
fn main() {
let color = Settings::default().parse(std::env::args_os()).color;
println!("{:?}", color);
let (settings, _operands) = Settings::default().parse(std::env::args_os());
println!("{:?}", settings.color);
}
+21 -30
View File
@@ -21,6 +21,7 @@ use std::{ffi::OsString, marker::PhantomData};
pub enum Argument<T: Arguments> {
Help,
Version,
Positional(OsString),
Custom(T),
}
@@ -59,18 +60,7 @@ pub trait Arguments: Sized {
/// Parse the next argument from the lexopt parser.
///
/// This method is called by [`ArgumentIter::next_arg`].
fn next_arg(
parser: &mut lexopt::Parser,
positional_idx: &mut usize,
) -> Result<Option<Argument<Self>>, ErrorKind>;
/// Check for any required arguments that have not been found.
///
/// If any missing arguments are found, the appropriate error is returned.
/// The `positional_idx` parameter specifies how many positional arguments
/// have been passed so far. This method is called at the end of
/// [`Options::parse`] and [`Options::try_parse`].
fn check_missing(positional_idx: usize) -> Result<(), Error>;
fn next_arg(parser: &mut lexopt::Parser) -> Result<Option<Argument<Self>>, ErrorKind>;
/// Print the help string for this command.
///
@@ -117,7 +107,7 @@ pub trait Arguments: Sized {
/// [`Options::try_parse`].
pub struct ArgumentIter<T: Arguments> {
parser: lexopt::Parser,
pub positional_idx: usize,
positional_arguments: Vec<OsString>,
t: PhantomData<T>,
}
@@ -129,18 +119,16 @@ impl<T: Arguments> ArgumentIter<T> {
{
Self {
parser: lexopt::Parser::from_iter(args),
positional_idx: 0,
positional_arguments: Vec::new(),
t: PhantomData,
}
}
pub fn next_arg(&mut self) -> Result<Option<T>, Error> {
if let Some(arg) =
T::next_arg(&mut self.parser, &mut self.positional_idx).map_err(|kind| Error {
exit_code: T::EXIT_CODE,
kind,
})?
{
while let Some(arg) = T::next_arg(&mut self.parser).map_err(|kind| Error {
exit_code: T::EXIT_CODE,
kind,
})? {
match arg {
Argument::Help => {
self.help().unwrap();
@@ -150,11 +138,17 @@ impl<T: Arguments> ArgumentIter<T> {
print!("{}", self.version());
std::process::exit(0);
}
Argument::Custom(arg) => Ok(Some(arg)),
Argument::Positional(arg) => {
self.positional_arguments.push(arg);
}
Argument::Custom(arg) => return Ok(Some(arg)),
}
} else {
Ok(None)
}
Ok(None)
}
fn get_positional_arguments(self) -> Vec<OsString> {
self.positional_arguments
}
fn help(&self) -> std::io::Result<()> {
@@ -176,15 +170,13 @@ impl<T: Arguments> ArgumentIter<T> {
///
/// By default, the [`Options::parse`] method will
/// 1. repeatedly call [`ArgumentIter::next_arg`] and call [`Options::apply`]
/// on the result until the arguments are exhausted,
/// 2. and finally call [`Arguments::check_missing`] to check whether all
/// required arguments were given.
/// on the result until the arguments are exhausted.
pub trait Options<Arg: Arguments>: Sized {
/// Apply a single argument to the options.
fn apply(&mut self, arg: Arg);
/// Parse an iterator of arguments into the options
fn parse<I>(self, args: I) -> Self
fn parse<I>(self, args: I) -> (Self, Vec<OsString>)
where
I: IntoIterator,
I::Item: Into<OsString>,
@@ -193,7 +185,7 @@ pub trait Options<Arg: Arguments>: Sized {
}
#[allow(unused_mut)]
fn try_parse<I>(mut self, args: I) -> Result<Self, Error>
fn try_parse<I>(mut self, args: I) -> Result<(Self, Vec<OsString>), Error>
where
I: IntoIterator,
I::Item: Into<OsString>,
@@ -216,8 +208,7 @@ pub trait Options<Arg: Arguments>: Sized {
while let Some(arg) = iter.next_arg()? {
self.apply(arg);
}
Arg::check_missing(iter.positional_idx)?;
Ok(self)
Ok((self, iter.get_positional_arguments()))
}
}
-1
View File
@@ -12,5 +12,4 @@ fn no_args() {
fn one_arg_fails() {
assert!(Arg::try_check(["arch", "-f"]).is_err());
assert!(Arg::try_check(["arch", "--foo"]).is_err());
assert!(Arg::try_check(["arch", "foo"]).is_err());
}
+25 -17
View File
@@ -1,4 +1,4 @@
use std::path::{Path, PathBuf};
use std::ffi::OsString;
use uutils_args::{Arguments, Options};
#[derive(Clone, Arguments)]
@@ -26,9 +26,6 @@ enum Arg {
#[arg("-w", "--warn")]
Warn,
#[arg("FILE", ..)]
File(PathBuf),
}
#[derive(Default, Debug, PartialEq, Eq)]
@@ -46,7 +43,6 @@ struct Settings {
tag: bool,
check_output: CheckOutput,
strict: bool,
files: Vec<PathBuf>,
}
impl Options<Arg> for Settings {
@@ -60,58 +56,68 @@ impl Options<Arg> for Settings {
Arg::Status => self.check_output = CheckOutput::Status,
Arg::Strict => self.strict = true,
Arg::Warn => self.check_output = CheckOutput::Warn,
Arg::File(f) => self.files.push(f),
}
}
}
#[test]
fn binary() {
assert!(!Settings::default().parse(["b2sum"]).binary);
assert!(!Settings::default().parse(["b2sum", "--text"]).binary);
assert!(!Settings::default().parse(["b2sum", "-t"]).binary);
assert!(!Settings::default().parse(["b2sum"]).0.binary);
assert!(!Settings::default().parse(["b2sum", "--text"]).0.binary);
assert!(!Settings::default().parse(["b2sum", "-t"]).0.binary);
assert!(
!Settings::default()
.parse(["b2sum", "--binary", "--text"])
.0
.binary
);
assert!(!Settings::default().parse(["b2sum", "-b", "-t"]).binary);
assert!(!Settings::default().parse(["b2sum", "-b", "-t"]).0.binary);
assert!(Settings::default().parse(["b2sum", "--binary"]).binary);
assert!(Settings::default().parse(["b2sum", "-b"]).binary);
assert!(Settings::default().parse(["b2sum", "--binary"]).0.binary);
assert!(Settings::default().parse(["b2sum", "-b"]).0.binary);
assert!(
Settings::default()
.parse(["b2sum", "--text", "--binary"])
.0
.binary
);
assert!(Settings::default().parse(["b2sum", "-t", "-b"]).binary);
assert!(Settings::default().parse(["b2sum", "-t", "-b"]).0.binary);
}
#[test]
fn check_output() {
assert_eq!(
Settings::default().parse(["b2sum", "--warn"]).check_output,
Settings::default()
.parse(["b2sum", "--warn"])
.0
.check_output,
CheckOutput::Warn
);
assert_eq!(
Settings::default().parse(["b2sum", "--quiet"]).check_output,
Settings::default()
.parse(["b2sum", "--quiet"])
.0
.check_output,
CheckOutput::Quiet
);
assert_eq!(
Settings::default()
.parse(["b2sum", "--status"])
.0
.check_output,
CheckOutput::Status
);
assert_eq!(
Settings::default()
.parse(["b2sum", "--status", "--warn"])
.0
.check_output,
CheckOutput::Warn
);
assert_eq!(
Settings::default()
.parse(["b2sum", "--status", "--warn"])
.0
.check_output,
CheckOutput::Warn
);
@@ -119,6 +125,7 @@ fn check_output() {
assert_eq!(
Settings::default()
.parse(["b2sum", "--warn", "--quiet"])
.0
.check_output,
CheckOutput::Quiet
);
@@ -126,6 +133,7 @@ fn check_output() {
assert_eq!(
Settings::default()
.parse(["b2sum", "--quiet", "--status"])
.0
.check_output,
CheckOutput::Status
);
@@ -134,7 +142,7 @@ fn check_output() {
#[test]
fn files() {
assert_eq!(
Settings::default().parse(["b2sum", "foo", "bar"]).files,
vec![Path::new("foo"), Path::new("bar")]
Settings::default().parse(["b2sum", "foo", "bar"]).1,
vec![OsString::from("foo"), OsString::from("bar")]
);
}
+4 -12
View File
@@ -1,5 +1,3 @@
use std::path::PathBuf;
use uutils_args::{Arguments, Options};
#[derive(Clone, Arguments)]
@@ -12,16 +10,12 @@ enum Arg {
#[arg("-w COLS", "--wrap=COLS")]
Wrap(usize),
#[arg("FILE", ..=1)]
File(PathBuf),
}
struct Settings {
decode: bool,
ignore_garbage: bool,
wrap: Option<usize>,
file: Option<PathBuf>,
}
impl Default for Settings {
@@ -30,7 +24,6 @@ impl Default for Settings {
wrap: Some(76),
decode: Default::default(),
ignore_garbage: Default::default(),
file: Default::default(),
}
}
}
@@ -42,21 +35,20 @@ impl Options<Arg> for Settings {
Arg::IgnoreGarbage => self.ignore_garbage = true,
Arg::Wrap(0) => self.wrap = None,
Arg::Wrap(x) => self.wrap = Some(x),
Arg::File(f) => self.file = Some(f),
}
}
}
#[test]
fn wrap() {
assert_eq!(Settings::default().parse(["base32"]).wrap, Some(76));
assert_eq!(Settings::default().parse(["base32", "-w0"]).wrap, None);
assert_eq!(Settings::default().parse(["base32"]).0.wrap, Some(76));
assert_eq!(Settings::default().parse(["base32", "-w0"]).0.wrap, None);
assert_eq!(
Settings::default().parse(["base32", "-w100"]).wrap,
Settings::default().parse(["base32", "-w100"]).0.wrap,
Some(100)
);
assert_eq!(
Settings::default().parse(["base32", "--wrap=100"]).wrap,
Settings::default().parse(["base32", "--wrap=100"]).0.wrap,
Some(100)
);
}
+7 -8
View File
@@ -1,3 +1,5 @@
use std::ffi::OsString;
use uutils_args::{Arguments, Options};
#[derive(Clone, Arguments)]
@@ -6,21 +8,18 @@ enum Arg {
Multiple,
#[arg("-s SUFFIX", "--suffix=SUFFIX")]
Suffix(String),
Suffix(OsString),
#[arg("-z", "--zero")]
Zero,
#[arg("NAMES", last, ..)]
Names(Vec<String>),
}
#[derive(Default)]
struct Settings {
multiple: bool,
suffix: String,
suffix: OsString,
zero: bool,
names: Vec<String>,
names: Vec<OsString>,
}
impl Options<Arg> for Settings {
@@ -32,13 +31,13 @@ impl Options<Arg> for Settings {
self.suffix = s
}
Arg::Zero => self.zero = true,
Arg::Names(names) => self.names = names,
}
}
}
fn parse(args: &[&str]) -> Settings {
let mut settings = Settings::default().parse(args);
let (mut settings, operands) = Settings::default().parse(args);
settings.names = operands;
if !settings.multiple {
assert_eq!(settings.names.len(), 2);
settings.suffix = settings.names.pop().unwrap();
+8 -15
View File
@@ -1,5 +1,3 @@
use std::path::PathBuf;
use uutils_args::{Arguments, Options};
#[derive(Default)]
@@ -38,9 +36,6 @@ enum Arg {
#[arg("-v", "--show-nonprinting")]
ShowNonPrinting,
#[arg("FILES", ..)]
File(PathBuf),
}
#[derive(Default)]
@@ -50,7 +45,6 @@ struct Settings {
show_nonprinting: bool,
number: NumberingMode,
squeeze_blank: bool,
files: Vec<PathBuf>,
}
impl Options<Arg> for Settings {
@@ -75,34 +69,33 @@ impl Options<Arg> for Settings {
Arg::Number => self.number = NumberingMode::All,
Arg::NumberNonblank => self.number = NumberingMode::NonEmpty,
Arg::SqueezeBlank => self.squeeze_blank = true,
Arg::File(f) => self.files.push(f),
}
}
}
#[test]
fn show() {
let s = Settings::default().parse(["cat", "-v"]);
let (s, _) = Settings::default().parse(["cat", "-v"]);
assert!(!s.show_ends && !s.show_tabs && s.show_nonprinting);
let s = Settings::default().parse(["cat", "-E"]);
let (s, _) = Settings::default().parse(["cat", "-E"]);
assert!(s.show_ends && !s.show_tabs && !s.show_nonprinting);
let s = Settings::default().parse(["cat", "-T"]);
let (s, _) = Settings::default().parse(["cat", "-T"]);
assert!(!s.show_ends && s.show_tabs && !s.show_nonprinting);
let s = Settings::default().parse(["cat", "-e"]);
let (s, _) = Settings::default().parse(["cat", "-e"]);
assert!(s.show_ends && !s.show_tabs && s.show_nonprinting);
let s = Settings::default().parse(["cat", "-t"]);
let (s, _) = Settings::default().parse(["cat", "-t"]);
assert!(!s.show_ends && s.show_tabs && s.show_nonprinting);
let s = Settings::default().parse(["cat", "-A"]);
let (s, _) = Settings::default().parse(["cat", "-A"]);
assert!(s.show_ends && s.show_tabs && s.show_nonprinting);
let s = Settings::default().parse(["cat", "-te"]);
let (s, _) = Settings::default().parse(["cat", "-te"]);
assert!(s.show_ends && s.show_tabs && s.show_nonprinting);
let s = Settings::default().parse(["cat", "-vET"]);
let (s, _) = Settings::default().parse(["cat", "-vET"]);
assert!(s.show_ends && s.show_tabs && s.show_nonprinting);
}
+7 -14
View File
@@ -116,16 +116,13 @@ impl Options<Arg> for Settings {
#[test]
fn empty() {
assert_eq!(
Settings::default().try_parse(["dd"]).unwrap(),
Settings::default()
)
assert_eq!(Settings::default().parse(["dd"]).0, Settings::default())
}
#[test]
fn infile() {
assert_eq!(
Settings::default().try_parse(["dd", "if=hello"]).unwrap(),
Settings::default().parse(["dd", "if=hello"]).0,
Settings {
infile: Some(PathBuf::from("hello")),
..Settings::default()
@@ -136,7 +133,7 @@ fn infile() {
#[test]
fn outfile() {
assert_eq!(
Settings::default().try_parse(["dd", "of=hello"]).unwrap(),
Settings::default().parse(["dd", "of=hello"]).0,
Settings {
outfile: Some(PathBuf::from("hello")),
..Settings::default()
@@ -147,7 +144,7 @@ fn outfile() {
#[test]
fn bs() {
assert_eq!(
Settings::default().try_parse(["dd", "ibs=1"]).unwrap(),
Settings::default().parse(["dd", "ibs=1"]).0,
Settings {
ibs: 1,
obs: 512,
@@ -155,7 +152,7 @@ fn bs() {
}
);
assert_eq!(
Settings::default().try_parse(["dd", "obs=1"]).unwrap(),
Settings::default().parse(["dd", "obs=1"]).0,
Settings {
ibs: 512,
obs: 1,
@@ -163,9 +160,7 @@ fn bs() {
}
);
assert_eq!(
Settings::default()
.try_parse(["dd", "ibs=10", "obs=1"])
.unwrap(),
Settings::default().parse(["dd", "ibs=10", "obs=1"]).0,
Settings {
ibs: 10,
obs: 1,
@@ -173,9 +168,7 @@ fn bs() {
}
);
assert_eq!(
Settings::default()
.try_parse(["dd", "ibs=10", "bs=1"])
.unwrap(),
Settings::default().parse(["dd", "ibs=10", "bs=1"]).0,
Settings {
ibs: 1,
obs: 1,
+7 -11
View File
@@ -15,16 +15,12 @@ enum Arg {
/// Disable interpretation of backslash escapes
#[arg("-E")]
DisableEscape,
#[arg("STRING", last)]
String(Vec<OsString>),
}
#[derive(Default)]
struct Settings {
trailing_newline: bool,
escape: bool,
strings: Vec<OsString>,
}
impl Options<Arg> for Settings {
@@ -33,7 +29,6 @@ impl Options<Arg> for Settings {
Arg::NoNewline => self.trailing_newline = false,
Arg::EnableEscape => self.escape = true,
Arg::DisableEscape => self.escape = false,
Arg::String(s) => self.strings = s,
}
}
}
@@ -42,17 +37,18 @@ impl Options<Arg> for Settings {
// support explicitly.
#[test]
#[ignore = "needs to be fixed after positional argument refactor"]
fn double_hyphen() {
let s = Settings::default().parse(["echo", "--"]);
assert_eq!(s.strings, vec![OsString::from("--")]);
let (_, operands) = Settings::default().parse(["echo", "--"]);
assert_eq!(operands, vec![OsString::from("--")]);
let s = Settings::default().parse(["echo", "--", "-n"]);
assert_eq!(s.strings, vec![OsString::from("--"), OsString::from("-n")]);
let (_, operands) = Settings::default().parse(["echo", "--", "-n"]);
assert_eq!(operands, vec![OsString::from("--"), OsString::from("-n")]);
}
#[test]
#[ignore]
fn nonexistent_options_are_values() {
let s = Settings::default().parse(["echo", "-f"]);
assert_eq!(s.strings, vec![OsString::from("-f")]);
let (_, operands) = Settings::default().parse(["echo", "-f"]);
assert_eq!(operands, vec![OsString::from("-f")]);
}
+25 -28
View File
@@ -8,7 +8,7 @@ use uutils_args::{Arguments, Options, Value};
// from this function are not relevant, so we can just return an `Option`.
// Once this gets into uutils, I highly recommend that we make this format
// optional at compile time. As the GNU docs explain, it's very error-prone.
fn parse_deprecated<I>(iter: I) -> Option<Settings>
fn parse_deprecated<I>(iter: I) -> Option<(Settings, Vec<OsString>)>
where
I: IntoIterator + Clone,
I::Item: Into<OsString>,
@@ -73,13 +73,15 @@ where
}
}
Some(Settings {
number: SigNum::Negative(num),
mode,
inputs: vec![input.into().into()],
verbose,
zero,
})
Some((
Settings {
number: SigNum::Negative(num),
mode,
verbose,
zero,
},
vec![input.into()],
))
}
#[derive(Arguments)]
@@ -98,9 +100,6 @@ enum Arg {
#[arg("-z", "--zero-terminated")]
Zero,
#[arg("FILES", ..)]
File(PathBuf),
}
// We need both negative and positive 0
@@ -185,7 +184,6 @@ struct Settings {
number: SigNum,
// TODO: Should be a dedicated PID type
verbose: bool,
inputs: Vec<PathBuf>,
zero: bool,
}
@@ -203,12 +201,11 @@ impl Options<Arg> for Settings {
Arg::Quiet => self.verbose = false,
Arg::Verbose => self.verbose = true,
Arg::Zero => self.zero = true,
Arg::File(input) => self.inputs.push(input),
}
}
}
fn parse_head<I>(iter: I) -> Result<Settings, uutils_args::Error>
fn parse_head<I>(iter: I) -> Result<(Settings, Vec<OsString>), uutils_args::Error>
where
I: IntoIterator + Clone,
I::Item: Into<OsString>,
@@ -221,51 +218,51 @@ where
#[test]
fn shorthand() {
let s = parse_head(["head", "-20", "some_file"]).unwrap();
let (s, _operands) = parse_head(["head", "-20", "some_file"]).unwrap();
assert_eq!(s.number, SigNum::Negative(20));
assert_eq!(s.mode, Mode::Lines);
let s = parse_head(["head", "-100cq", "some_file"]).unwrap();
let (s, _operands) = parse_head(["head", "-100cq", "some_file"]).unwrap();
assert_eq!(s.number, SigNum::Negative(100));
assert_eq!(s.mode, Mode::Bytes);
// Corner case where the shorthand does not apply
let s = parse_head(["head", "-c", "42"]).unwrap();
let (s, operands) = parse_head(["head", "-c", "42"]).unwrap();
assert_eq!(s.number, SigNum::Negative(42));
assert_eq!(s.mode, Mode::Bytes);
assert_eq!(s.inputs, Vec::<PathBuf>::new());
assert_eq!(operands, Vec::<PathBuf>::new());
}
#[test]
fn standard_input() {
let s = parse_head(["head", "-"]).unwrap();
assert_eq!(s.inputs, vec![PathBuf::from("-")])
let (_s, operands) = parse_head(["head", "-"]).unwrap();
assert_eq!(operands, vec![PathBuf::from("-")])
}
#[test]
fn normal_format() {
let s = parse_head(["head", "-c", "20", "some_file"]).unwrap();
let (s, _operands) = parse_head(["head", "-c", "20", "some_file"]).unwrap();
assert_eq!(s.number, SigNum::Negative(20));
assert_eq!(s.mode, Mode::Bytes);
}
#[test]
fn signum() {
let s = parse_head(["head", "-n", "20"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "20"]).unwrap();
assert_eq!(s.number, SigNum::Negative(20));
let s = parse_head(["head", "-n", "-20"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "-20"]).unwrap();
assert_eq!(s.number, SigNum::Negative(20));
let s = parse_head(["head", "-n", "+20"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "+20"]).unwrap();
assert_eq!(s.number, SigNum::Positive(20));
let s = parse_head(["head", "-n", "20b"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "20b"]).unwrap();
assert_eq!(s.number, SigNum::Negative(20 * 512));
let s = parse_head(["head", "-n", "+20b"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "+20b"]).unwrap();
assert_eq!(s.number, SigNum::Positive(20 * 512));
let s = parse_head(["head", "-n", "b"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "b"]).unwrap();
assert_eq!(s.number, SigNum::Negative(512));
let s = parse_head(["head", "-n", "+b"]).unwrap();
let (s, _operands) = parse_head(["head", "-n", "+b"]).unwrap();
assert_eq!(s.number, SigNum::Positive(512));
assert!(parse_head(["head", "-n", "20invalid_suffix"]).is_err());
+23 -31
View File
@@ -1,4 +1,3 @@
use std::path::PathBuf;
use uutils_args::{Arguments, Options, Value};
#[derive(Default, Debug, PartialEq, Eq, Value)]
@@ -276,9 +275,6 @@ enum Arg {
#[arg("--group-directories-first")]
GroupDirectoriesFirst,
#[arg("FILES", ..)]
File(PathBuf),
}
fn default_terminal_size() -> u16 {
@@ -305,7 +301,6 @@ fn default_terminal_size() -> u16 {
#[derive(Debug, PartialEq, Eq)]
struct Settings {
format: Format,
files: Vec<PathBuf>,
sort: Sort,
recursive: bool,
reverse: bool,
@@ -340,7 +335,6 @@ impl Default for Settings {
eol: '\n',
width: default_terminal_size(),
format: Default::default(),
files: Default::default(),
sort: Default::default(),
recursive: Default::default(),
reverse: Default::default(),
@@ -422,7 +416,6 @@ impl Options<Arg> for Settings {
// TODO: Zero changes more than just this
}
Arg::GroupDirectoriesFirst => self.group_directories_first = true,
Arg::File(f) => self.files.push(f),
}
}
}
@@ -430,10 +423,9 @@ impl Options<Arg> for Settings {
#[test]
fn default() {
assert_eq!(
Settings::default().parse(["ls"]),
Settings::default().parse(["ls"]).0,
Settings {
format: Format::Columns,
files: Vec::new(),
sort: Sort::Name,
recursive: false,
reverse: false,
@@ -462,87 +454,87 @@ fn default() {
#[test]
fn color() {
let s = Settings::default().parse(["ls", "--color"]);
let (s, _operands) = Settings::default().parse(["ls", "--color"]);
assert!(s.color);
let s = Settings::default().parse(["ls", "--color=always"]);
let (s, _operands) = Settings::default().parse(["ls", "--color=always"]);
assert!(s.color);
let s = Settings::default().parse(["ls", "--color=never"]);
let (s, _operands) = Settings::default().parse(["ls", "--color=never"]);
assert!(!s.color);
}
#[test]
fn format() {
let s = Settings::default().parse(["ls", "-l"]);
let (s, _operands) = Settings::default().parse(["ls", "-l"]);
assert_eq!(s.format, Format::Long);
let s = Settings::default().parse(["ls", "-m"]);
let (s, _operands) = Settings::default().parse(["ls", "-m"]);
assert_eq!(s.format, Format::Commas);
let s = Settings::default().parse(["ls", "--format=across"]);
let (s, _operands) = Settings::default().parse(["ls", "--format=across"]);
assert_eq!(s.format, Format::Across);
let s = Settings::default().parse(["ls", "--format=acr"]);
let (s, _operands) = Settings::default().parse(["ls", "--format=acr"]);
assert_eq!(s.format, Format::Across);
let s = Settings::default().parse(["ls", "-o"]);
let (s, _operands) = Settings::default().parse(["ls", "-o"]);
assert_eq!(s.format, Format::Long);
assert!(s.long_no_group && !s.long_no_owner && !s.long_numeric_uid_gid);
let s = Settings::default().parse(["ls", "-g"]);
let (s, _operands) = Settings::default().parse(["ls", "-g"]);
assert_eq!(s.format, Format::Long);
assert!(!s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid);
let s = Settings::default().parse(["ls", "-n"]);
let (s, _operands) = Settings::default().parse(["ls", "-n"]);
assert_eq!(s.format, Format::Long);
assert!(!s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid);
let s = Settings::default().parse(["ls", "-og"]);
let (s, _operands) = Settings::default().parse(["ls", "-og"]);
assert_eq!(s.format, Format::Long);
assert!(s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid);
let s = Settings::default().parse(["ls", "-on"]);
let (s, _operands) = Settings::default().parse(["ls", "-on"]);
assert_eq!(s.format, Format::Long);
assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid);
let s = Settings::default().parse(["ls", "-onCl"]);
let (s, _operands) = Settings::default().parse(["ls", "-onCl"]);
assert_eq!(s.format, Format::Long);
assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid);
}
#[test]
fn time() {
let s = Settings::default().parse(["ls", "--time=access"]);
let (s, _operands) = Settings::default().parse(["ls", "--time=access"]);
assert_eq!(s.time, Time::Access);
let s = Settings::default().parse(["ls", "--time=a"]);
let (s, _operands) = Settings::default().parse(["ls", "--time=a"]);
assert_eq!(s.time, Time::Access);
}
#[test]
fn classify() {
let s = Settings::default().parse(["ls", "--indicator-style=classify"]);
let (s, _operands) = Settings::default().parse(["ls", "--indicator-style=classify"]);
assert_eq!(s.indicator_style, IndicatorStyle::Classify);
let s = Settings::default().parse(["ls", "--classify"]);
let (s, _operands) = Settings::default().parse(["ls", "--classify"]);
assert_eq!(s.indicator_style, IndicatorStyle::Classify);
let s = Settings::default().parse(["ls", "--classify=always"]);
let (s, _operands) = Settings::default().parse(["ls", "--classify=always"]);
assert_eq!(s.indicator_style, IndicatorStyle::Classify);
let s = Settings::default().parse(["ls", "--classify=none"]);
let (s, _operands) = Settings::default().parse(["ls", "--classify=none"]);
assert_eq!(s.indicator_style, IndicatorStyle::None);
let s = Settings::default().parse(["ls", "-F"]);
let (s, _operands) = Settings::default().parse(["ls", "-F"]);
assert_eq!(s.indicator_style, IndicatorStyle::Classify);
}
#[test]
fn sort() {
let s = Settings::default().parse(["ls", "--sort=time"]);
let (s, _operands) = Settings::default().parse(["ls", "--sort=time"]);
assert_eq!(s.sort, Sort::Time);
let s = Settings::default().parse(["ls", "-X"]);
let (s, _operands) = Settings::default().parse(["ls", "-X"]);
assert_eq!(s.sort, Sort::Extension);
}
+9 -14
View File
@@ -21,9 +21,6 @@ enum Arg {
#[arg("-p DIR", "--tmpdir[=DIR]", value = ".".into())]
TmpDir(PathBuf),
#[arg("TEMPLATE", 0..=1)]
Template(String),
}
#[derive(Default)]
@@ -34,7 +31,6 @@ struct Settings {
tmp_dir: Option<PathBuf>,
suffix: Option<String>,
treat_as_template: bool,
template: String,
}
impl Options<Arg> for Settings {
@@ -46,41 +42,40 @@ impl Options<Arg> for Settings {
Arg::Suffix(s) => self.suffix = Some(s),
Arg::TreatAsTemplate => self.treat_as_template = true,
Arg::TmpDir(dir) => self.tmp_dir = Some(dir),
Arg::Template(s) => self.template = s,
}
}
}
#[test]
fn suffix() {
let s = Settings::default().parse(["mktemp", "--suffix=hello"]);
let (s, _operands) = Settings::default().parse(["mktemp", "--suffix=hello"]);
assert_eq!(s.suffix.unwrap(), "hello");
let s = Settings::default().parse(["mktemp", "--suffix="]);
let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]);
assert_eq!(s.suffix.unwrap(), "");
let s = Settings::default().parse(["mktemp", "--suffix="]);
let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]);
assert_eq!(s.suffix.unwrap(), "");
let s = Settings::default().parse(["mktemp"]);
let (s, _operands) = Settings::default().parse(["mktemp"]);
assert_eq!(s.suffix, None);
}
#[test]
fn tmpdir() {
let s = Settings::default().parse(["mktemp", "--tmpdir"]);
let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir"]);
assert_eq!(s.tmp_dir.unwrap(), Path::new("."));
let s = Settings::default().parse(["mktemp", "--tmpdir="]);
let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir="]);
assert_eq!(s.tmp_dir.unwrap(), Path::new(""));
let s = Settings::default().parse(["mktemp", "-p", "foo"]);
let (s, _operands) = Settings::default().parse(["mktemp", "-p", "foo"]);
assert_eq!(s.tmp_dir.unwrap(), Path::new("foo"));
let s = Settings::default().parse(["mktemp", "-pfoo"]);
let (s, _operands) = Settings::default().parse(["mktemp", "-pfoo"]);
assert_eq!(s.tmp_dir.unwrap(), Path::new("foo"));
let s = Settings::default().parse(["mktemp", "-p", ""]);
let (s, _operands) = Settings::default().parse(["mktemp", "-p", ""]);
assert_eq!(s.tmp_dir.unwrap(), Path::new(""));
assert!(Settings::default().try_parse(["mktemp", "-p"]).is_err());

Some files were not shown because too many files have changed in this diff Show More