Skip to content

Commit

Permalink
take positional arguments out of arguments enum
Browse files Browse the repository at this point in the history
  • Loading branch information
tertsdiepraam committed Dec 19, 2023
1 parent 932a471 commit 462898c
Show file tree
Hide file tree
Showing 25 changed files with 285 additions and 690 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 13 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)]
Expand All @@ -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('!');
Expand Down
104 changes: 0 additions & 104 deletions derive/src/argument.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -27,10 +25,6 @@ pub enum ArgType {
takes_value: bool,
default: TokenStream,
},
Positional {
num_args: RangeInclusive<usize>,
last: bool,
},
Free {
filters: Vec<syn::Ident>,
},
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
};

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
})
}
84 changes: 2 additions & 82 deletions derive/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -69,7 +66,6 @@ impl ArgumentsAttr {

pub enum ArgAttr {
Option(OptionAttr),
Positional(PositionalAttr),
Free(FreeAttr),
}

Expand All @@ -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)
Expand Down Expand Up @@ -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>,
Expand Down
1 change: 0 additions & 1 deletion derive/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. } => {}
}
Expand Down
Loading

0 comments on commit 462898c

Please sign in to comment.