From 40cdb24004bc68ff1d6b9ef565dd76d0a94226e4 Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Tue, 18 Nov 2025 15:59:05 +1100 Subject: [PATCH] Introduce `parse_slice` that allows value borrowing. Technically this works, but I am happy with precisely NONE of it. --- jaarg-nostd/examples/basic_nostd.rs | 17 ++- jaarg/src/alloc.rs | 4 +- jaarg/src/argparse.rs | 174 ++++++++++++++++++++++++---- jaarg/src/std.rs | 4 +- 4 files changed, 163 insertions(+), 36 deletions(-) diff --git a/jaarg-nostd/examples/basic_nostd.rs b/jaarg-nostd/examples/basic_nostd.rs index 044b46f..cf6d935 100644 --- a/jaarg-nostd/examples/basic_nostd.rs +++ b/jaarg-nostd/examples/basic_nostd.rs @@ -6,8 +6,6 @@ #![no_std] #![no_main] -extern crate alloc; - use jaarg::{ ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts, ParseControl, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter @@ -18,8 +16,8 @@ use jaarg_nostd::{print, println, harness::ExitCode, simplepathbuf::SimplePathBu #[allow(improper_ctypes_definitions)] extern "C" fn safe_main(args: &[&str]) -> ExitCode { // Variables for arguments to fill - let mut file = SimplePathBuf::default(); - let mut out: Option = None; + let mut file: Option<&str> = None; + let mut out: Option<&str> = None; let mut number = 0; // Set up arguments table @@ -35,9 +33,9 @@ extern "C" fn safe_main(args: &[&str]) -> ExitCode { ]).with_description("My simple utility."); // Parse command-line arguments from argv - match OPTIONS.parse( + match OPTIONS.parse_slice( SimplePathBuf::from(*args.first().unwrap()).basename(), - args.iter().skip(1), |ctx| { + &args[1..], |ctx| { match ctx.id { Arg::Help => { let ctx = HelpWriterContext { options: &OPTIONS, program_name: ctx.program_name }; @@ -45,8 +43,8 @@ extern "C" fn safe_main(args: &[&str]) -> ExitCode { return Ok(ParseControl::Quit); } Arg::Number => { number = str::parse(ctx.arg.unwrap())?; } - Arg::File => { file = ctx.arg.unwrap().into(); } - Arg::Out => { out = Some(ctx.arg.unwrap().into()); } + Arg::File => { file = ctx.arg; } + Arg::Out => { out = ctx.arg; } } Ok(ParseControl::Continue) }, |program_name, error| { @@ -60,8 +58,9 @@ extern "C" fn safe_main(args: &[&str]) -> ExitCode { } // Print the result variables + let file = SimplePathBuf::from(file.unwrap()); println!("{file} -> {out} (number: {number})", - out = out.unwrap_or(file.with_extension("out"))); + out = out.map_or(file.with_extension("out"), |out| SimplePathBuf::from(out))); ExitCode::SUCCESS } diff --git a/jaarg/src/alloc.rs b/jaarg/src/alloc.rs index 68ab2b3..edb277e 100644 --- a/jaarg/src/alloc.rs +++ b/jaarg/src/alloc.rs @@ -13,7 +13,7 @@ impl Opts<&'static str> { /// Parse an iterator of strings as arguments and return the results in a [`BTreeMap`]. /// /// Requires `features = ["alloc"]`. - pub fn parse_map<'a, S: AsRef + 'a, I: Iterator>(&self, program_name: &str, args: I, + pub fn parse_map<'opt, 't, S: AsRef + 't, I: Iterator>(&'opt self, program_name: &str, args: I, help: impl Fn(&str), error: impl FnOnce(&str, ParseError) ) -> ParseMapResult { let mut out: BTreeMap<&'static str, String> = BTreeMap::new(); @@ -22,7 +22,7 @@ impl Opts<&'static str> { help(program_name); Ok(ParseControl::Quit) } else { - out.insert(ctx.id, ctx.arg.unwrap().to_string()); + out.insert(ctx.id, ctx.arg.map_or(String::new(), |o| o.to_string())); Ok(ParseControl::Continue) } }, error) { diff --git a/jaarg/src/argparse.rs b/jaarg/src/argparse.rs index 56b2e38..fc83ce8 100644 --- a/jaarg/src/argparse.rs +++ b/jaarg/src/argparse.rs @@ -29,9 +29,9 @@ pub enum ParseControl { } #[derive(Debug)] -pub struct ParseHandlerContext<'a, ID: 'static> { +pub struct ParseHandlerContext<'a, 'name, ID: 'static> { /// Name of the program, for printing statuses to the user. - pub program_name: &'a str, + pub program_name: &'name str, /// The generic argument ID that was matched. pub id: &'a ID, /// The option that was matched by the parser. @@ -47,12 +47,12 @@ pub struct ParseHandlerContext<'a, ID: 'static> { pub(crate) type HandlerResult<'a, T> = core::result::Result>; #[derive(Debug)] -pub enum ParseError<'a> { - UnknownOption(&'a str), - UnexpectedToken(&'a str), - ExpectArgument(&'a str), - UnexpectedArgument(&'a str), - ArgumentError(&'static str, &'a str, ParseErrorKind), +pub enum ParseError<'t> { + UnknownOption(&'t str), + UnexpectedToken(&'t str), + ExpectArgument(&'t str), + UnexpectedArgument(&'t str), + ArgumentError(&'static str, &'t str, ParseErrorKind), //TODO //Exclusive(&'static str, &'a str), RequiredPositional(&'static str), @@ -154,6 +154,30 @@ impl Opts { self.validate_state(program_name, state, error) } + /// Parses a slice of strings as argument tokens. + /// Like [Opts::parse] but allows borrowing argument tokens outside the handler. + pub fn parse_slice<'opts, 't, S: AsRef>(&'opts self, program_name: &str, args: &'t [S], + mut handler: impl FnMut(ParseHandlerContext<'opts, '_, ID>) -> HandlerResult<'opts, ParseControl>, + error: impl FnOnce(&str, ParseError), + ) -> ParseResult where 't: 'opts { + let mut state = ParserState::default(); + for arg in args { + // Fetch the next token + match self.next_borrow(&mut state, arg.as_ref(), program_name, &mut handler) { + Ok(ParseControl::Continue) => {} + Ok(ParseControl::Stop) => { break; } + Ok(ParseControl::Quit) => { return ParseResult::ExitSuccess; } + Err(err) => { + // Call the error handler + error(program_name, err); + return ParseResult::ExitFailure; + } + } + } + + self.validate_state(program_name, state, error) + } + fn validate_state(&self, program_name: &str, mut state: ParserState, error: impl FnOnce(&str, ParseError) ) -> ParseResult { // Ensure that value options are provided a value @@ -184,10 +208,10 @@ impl Opts { ParseResult::ContinueSuccess } - /// Parse the next token in the argument stream - fn next<'a, 'b>(&self, state: &mut ParserState, token: &'b str, program_name: &str, - handler: &mut impl FnMut(ParseHandlerContext) -> HandlerResult<'a, ParseControl> - ) -> HandlerResult<'b, ParseControl> where 'a: 'b { + /// Parse the next token in the argument stream. + fn next<'r, 't>(&self, state: &mut ParserState, token: &'t str, program_name: &str, + handler: &mut impl FnMut(ParseHandlerContext) -> HandlerResult<'r, ParseControl> + ) -> HandlerResult<'t, ParseControl> where 'r: 't { let mut call_handler = |option: &Opt, name, value| { match handler(ParseHandlerContext{ program_name, id: &option.id, option, name, arg: value }) { // HACK: Ensure the string fields are set properly, because coerced @@ -261,28 +285,105 @@ impl Opts { } } } + + /// I absolutely hate that this needs to be DUPLICATED + fn next_borrow<'opts, 't>(&'opts self, state: &mut ParserState, token: &'t str, program_name: &str, + handler: &mut impl FnMut(ParseHandlerContext<'opts, '_, ID>) -> HandlerResult<'opts, ParseControl> + ) -> HandlerResult<'opts, ParseControl> where 't: 'opts { + let mut call_handler = |option: &'opts Opt, name, value| { + match handler(ParseHandlerContext{ program_name, id: &option.id, option, name, arg: value }) { + // HACK: Ensure the string fields are set properly, because coerced + // ParseIntError/ParseFloatError will have the string fields blanked. + Err(ParseError::ArgumentError("", "", kind)) + => Err(ParseError::ArgumentError(name, value.unwrap(), kind)), + Err(err) => Err(err), + Ok(ctl) => Ok(ctl), + } + }; + + // If the previous token is expecting an argument, ie: value a value option + // was matched and didn't have an equals sign separating a value, + // then call the handler here. + if let Some((name, option)) = state.expects_arg.take() { + call_handler(option, name, Some(token)) + } else { + // Check if the next argument token starts with an option flag + if self.flag_chars.chars().any(|c| token.starts_with(c)) { + // Value options can have their value delineated by an equals sign or with whitespace. + // In the latter case; the value will be in the next token. + let (option_str, value_str) = token.split_once("=") + .map_or((token, None), |(k, v)| (k, Some(v))); + + // Keep track of how many required options we've seen + let mut required_idx = 0; + + // Match a suitable option by name (ignoring the first flag character & skipping positional arguments) + let (name, option) = self.iter() + .filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| { + if let Some(name) = opt.match_name(option_str, 1) { + Some((name, opt)) + } else { + if opt.is_required() { + required_idx += 1 + } + None + } + }).ok_or(ParseError::UnknownOption(option_str))?; + + // Mark required option as visited + if option.is_required() { + state.required_param_presences.insert(required_idx, true); + } + + match (&option.r#type, value_str) { + // Call handler for flag-only options + (OptType::Flag, None) => call_handler(option, name, None), + // Value was provided this token, so call the handler right now + (OptType::Value, Some(value)) => call_handler(option, name, Some(value)), + // No value available in this token, delay handling to next token + (OptType::Value, None) => { + state.expects_arg = Some((name, option)); + Ok(ParseControl::Continue) + } + // Flag-only options do not support arguments + (OptType::Flag, Some(_)) => Err(ParseError::UnexpectedArgument(option_str)), + // Positional arguments are filtered out so this is impossible + (OptType::Positional, _) => unreachable!("Won't parse a positional argument as an option"), + } + } else { + // Find the next positional argument + for (i, option) in self.options[state.positional_index..].iter().enumerate() { + if matches!(option.r#type, OptType::Positional) { + call_handler(option, option.first_name(), Some(token))?; + state.positional_index += i + 1; + return Ok(ParseControl::Continue); + } + } + Err(ParseError::UnexpectedToken(token)) + } + } + } } #[cfg(test)] mod tests { use super::*; + enum ArgID { One, Two, Three, Four, Five } + const OPTIONS: Opts = Opts::new(&[ + Opt::positional(ArgID::One, "one"), + Opt::flag(ArgID::Two, &["--two"]), + Opt::value(ArgID::Three, &["--three"], "value"), + Opt::value(ArgID::Four, &["--four"], "value"), + Opt::value(ArgID::Five, &["--five"], "value"), + ]); + const ARGUMENTS: &[&str] = &["one", "--two", "--three=three", "--five=", "--four", "four"]; + #[test] fn test_parse() { extern crate alloc; use alloc::string::String; - enum ArgID { One, Two, Three, Four, Five } - const OPTIONS: Opts = Opts::new(&[ - Opt::positional(ArgID::One, "one"), - Opt::flag(ArgID::Two, &["--two"]), - Opt::value(ArgID::Three, &["--three"], "value"), - Opt::value(ArgID::Four, &["--four"], "value"), - Opt::value(ArgID::Five, &["--five"], "value"), - ]); - const ARGUMENTS: &[&str] = &["one", "--two", "--three=three", "--five=", "--four", "four"]; - - //TODO: currently needs alloc to deal with arguments not being able to escape handler let mut one: Option = None; let mut two = false; let mut three: Option = None; @@ -307,4 +408,31 @@ mod tests { assert_eq!(four, Some("four".into())); assert_eq!(five, Some("".into())); } + + #[test] + fn test_parse_slice() { + let mut one: Option<&str> = None; + let mut two = false; + let mut three: Option<&str> = None; + let mut four: Option<&str> = None; + let mut five: Option<&str> = None; + assert!(matches!(OPTIONS.parse_slice("", &ARGUMENTS, |ctx| { + match ctx.id { + ArgID::One => { one = ctx.arg; } + ArgID::Two => { two = true; } + ArgID::Three => { three = ctx.arg; } + ArgID::Four => { four = ctx.arg; } + ArgID::Five => { five = ctx.arg; } + } + Ok(ParseControl::Continue) + }, |_, error| { + panic!("unreachable: {error:?}"); + }), ParseResult::ContinueSuccess)); + + assert_eq!(one, Some("one")); + assert!(two); + assert_eq!(three, Some("three")); + assert_eq!(four, Some("four")); + assert_eq!(five, Some("")); + } } diff --git a/jaarg/src/std.rs b/jaarg/src/std.rs index d6c197a..49967e4 100644 --- a/jaarg/src/std.rs +++ b/jaarg/src/std.rs @@ -18,7 +18,7 @@ impl Opts { /// The errors are formatted in a standard user-friendly format. /// /// Requires `features = ["std"]`. - pub fn parse_easy<'a>(&self, handler: impl FnMut(ParseHandlerContext) -> HandlerResult<'a, ParseControl> + pub fn parse_easy<'a>(&'static self, handler: impl FnMut(ParseHandlerContext) -> HandlerResult<'a, ParseControl> ) -> ParseResult { let (program_name, argv) = Self::easy_args(); self.parse(&program_name, argv, handler, @@ -69,7 +69,7 @@ impl Opts<&'static str> { /// Help and errors are formatted in a standard user-friendly format. /// /// Requires `features = ["std"]`. - pub fn parse_map_easy(&self) -> ParseMapResult { + pub fn parse_map_easy(&'static self) -> ParseMapResult { let (program_name, argv) = Self::easy_args(); self.parse_map(&program_name, argv, |name| self.print_full_help(name),