Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take positional arguments out of arguments enum #72

Merged
merged 1 commit into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading