From 4f1a01f81ce4e1fd9007a249d88e69c698e6864c Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Mon, 17 Nov 2025 18:11:32 +1100 Subject: [PATCH] Collect parse handler args into a struct with proper names. --- jaarg-nostd/examples/basic_nostd.rs | 13 ++++++------- jaarg/examples/basic.rs | 12 ++++++------ jaarg/examples/bin2h.rs | 14 +++++++------- jaarg/src/alloc.rs | 6 +++--- jaarg/src/argparse.rs | 23 +++++++++++++++++++---- jaarg/src/std.rs | 6 +++--- 6 files changed, 44 insertions(+), 30 deletions(-) diff --git a/jaarg-nostd/examples/basic_nostd.rs b/jaarg-nostd/examples/basic_nostd.rs index d64e71e..de90ae6 100644 --- a/jaarg-nostd/examples/basic_nostd.rs +++ b/jaarg-nostd/examples/basic_nostd.rs @@ -37,17 +37,16 @@ extern "C" fn safe_main(args: &[&str]) -> ExitCode { // Parse command-line arguments from argv match OPTIONS.parse( SimplePathBuf::from(*args.first().unwrap()).basename(), - args.iter().skip(1), - |program_name, id, _opt, _name, arg| { - match id { + args.iter().skip(1), |ctx| { + match ctx.id { Arg::Help => { - let ctx = HelpWriterContext { options: &OPTIONS, program_name }; + let ctx = HelpWriterContext { options: &OPTIONS, program_name: ctx.program_name }; print!("{}", StandardFullHelpWriter::<'_, Arg>::new(ctx)); return Ok(ParseControl::Quit); } - Arg::Number => { number = str::parse(arg)?; } - Arg::File => { file = arg.into(); } - Arg::Out => { out = Some(arg.into()); } + Arg::Number => { number = str::parse(ctx.arg)?; } + Arg::File => { file = ctx.arg.into(); } + Arg::Out => { out = Some(ctx.arg.into()); } } Ok(ParseControl::Continue) }, |program_name, error| { diff --git a/jaarg/examples/basic.rs b/jaarg/examples/basic.rs index 29ca0d9..907792b 100644 --- a/jaarg/examples/basic.rs +++ b/jaarg/examples/basic.rs @@ -26,15 +26,15 @@ fn main() { ]).with_description("My simple utility."); // Parse command-line arguments from `std::env::args()` - match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| { - match id { + match OPTIONS.parse_easy(|ctx| { + match ctx.id { Arg::Help => { - OPTIONS.print_full_help(program_name); + OPTIONS.print_full_help(ctx.program_name); return Ok(ParseControl::Quit); } - Arg::Number => { number = str::parse(arg)?; } - Arg::File => { file = arg.into(); } - Arg::Out => { out = Some(arg.into()); } + Arg::Number => { number = str::parse(ctx.arg)?; } + Arg::File => { file = ctx.arg.into(); } + Arg::Out => { out = Some(ctx.arg.into()); } } Ok(ParseControl::Continue) }) { diff --git a/jaarg/examples/bin2h.rs b/jaarg/examples/bin2h.rs index 9decbfe..1453082 100644 --- a/jaarg/examples/bin2h.rs +++ b/jaarg/examples/bin2h.rs @@ -210,14 +210,14 @@ pub fn main() -> ExitCode { Opt::value(Arg::Whitespace, &["--whitespace"], "\" \"").help_text("Emitted indentation (Default: \"\\t\")"), ]).with_description("Convert one or more binary and text file(s) to a C header file,\n\ as arrays and C strings respectively."); - match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| { - match id { - Arg::Out => { arguments.out = arg.into(); } - Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: arg.into() }); } - Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: arg.into() }); } - Arg::Whitespace => { arguments.whitespace = arg.into(); } + match OPTIONS.parse_easy(|ctx| { + match ctx.id { + Arg::Out => { arguments.out = ctx.arg.into(); } + Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: ctx.arg.into() }); } + Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: ctx.arg.into() }); } + Arg::Whitespace => { arguments.whitespace = ctx.arg.into(); } Arg::Help => { - OPTIONS.print_full_help(program_name); + OPTIONS.print_full_help(ctx.program_name); return Ok(ParseControl::Quit); } } diff --git a/jaarg/src/alloc.rs b/jaarg/src/alloc.rs index 66b693e..d6051ca 100644 --- a/jaarg/src/alloc.rs +++ b/jaarg/src/alloc.rs @@ -17,12 +17,12 @@ impl Opts<&'static str> { help: impl Fn(&str), error: impl FnOnce(&str, ParseError) ) -> ParseMapResult { let mut out: BTreeMap<&'static str, String> = BTreeMap::new(); - match self.parse(program_name, args, |_program_name, id, opt, _name, arg| { - if opt.is_help() { + match self.parse(program_name, args, |ctx| { + if ctx.option.is_help() { help(program_name); Ok(ParseControl::Quit) } else { - out.insert(id, arg.into()); + out.insert(ctx.id, ctx.arg.into()); Ok(ParseControl::Continue) } }, error) { diff --git a/jaarg/src/argparse.rs b/jaarg/src/argparse.rs index 87baa79..e550ac9 100644 --- a/jaarg/src/argparse.rs +++ b/jaarg/src/argparse.rs @@ -28,6 +28,21 @@ pub enum ParseControl { Quit, } +#[derive(Debug)] +pub struct ParseHandlerContext<'a, ID: 'static> { + /// Name of the program, for printing statuses to the user. + pub program_name: &'a str, + /// The generic argument ID that was matched. + pub id: &'a ID, + /// The option that was matched by the parser. + pub option: &'a Opt, + /// The name of the argument parameter that was matched, + /// for option parameters this is the token supplied by the user. + pub name: &'a str, + /// The argument provided to positional arguments and value options, else "". + pub arg: &'a str, +} + /// Result type used by the handler passed to the parser. pub(crate) type HandlerResult<'a, T> = core::result::Result>; @@ -118,7 +133,7 @@ impl Default for ParserState { impl Opts { /// Parses an iterator of strings as argument tokens. pub fn parse<'a, S: AsRef + 'a, I: Iterator>(&self, program_name: &str, args: I, - mut handler: impl FnMut(&str, &ID, &Opt, &str, &str) -> HandlerResult<'a, ParseControl>, + mut handler: impl FnMut(ParseHandlerContext) -> HandlerResult<'a, ParseControl>, error: impl FnOnce(&str, ParseError), ) -> ParseResult { let mut state = ParserState::default(); @@ -166,10 +181,10 @@ impl Opts { /// 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(&str, &ID, &Opt, &str, &str) -> HandlerResult<'a, ParseControl> + handler: &mut impl FnMut(ParseHandlerContext) -> HandlerResult<'a, ParseControl> ) -> HandlerResult<'b, ParseControl> where 'a: 'b { let mut call_handler = |option: &Opt, name, value| { - match handler(program_name, &option.id, option, 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)) @@ -232,7 +247,7 @@ impl Opts { // Find the next positional argument for (i, option) in self.options[state.positional_index..].iter().enumerate() { if matches!(option.r#type, OptType::Positional) { - handler(program_name, &option.id, option, option.first_name(), token)?; + call_handler(option, option.first_name(), token)?; state.positional_index += i + 1; return Ok(ParseControl::Continue); } diff --git a/jaarg/src/std.rs b/jaarg/src/std.rs index ee28484..d6c197a 100644 --- a/jaarg/src/std.rs +++ b/jaarg/src/std.rs @@ -6,8 +6,8 @@ extern crate std; use crate::{ - ErrorUsageWriter, ErrorUsageWriterContext, HandlerResult, HelpWriter, HelpWriterContext, Opt, Opts, - ParseControl, ParseError, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter, alloc::ParseMapResult + alloc::ParseMapResult, ErrorUsageWriter, ErrorUsageWriterContext, HandlerResult, HelpWriter, HelpWriterContext, + Opts, ParseControl, ParseError, ParseHandlerContext, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter }; use std::path::Path; use std::rc::Rc; @@ -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(&str, &ID, &Opt, &str, &str) -> HandlerResult<'a, ParseControl> + pub fn parse_easy<'a>(&self, handler: impl FnMut(ParseHandlerContext) -> HandlerResult<'a, ParseControl> ) -> ParseResult { let (program_name, argv) = Self::easy_args(); self.parse(&program_name, argv, handler,