From 0d9b86c767a7204fdb6fc34291675f769b83ba8e Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Sun, 2 Nov 2025 00:44:25 +1100 Subject: [PATCH] Move special treatment of help flag into lib to improve usage display and BTreeMap use case ergonomics --- examples/bin2h.rs | 2 +- examples/btreemap.rs | 9 ++------ src/argparse.rs | 2 +- src/help.rs | 12 +++++------ src/option.rs | 49 ++++++++++++++++++++++++++++++++++++-------- src/std.rs | 25 +++++++++++++++------- 6 files changed, 68 insertions(+), 31 deletions(-) diff --git a/examples/bin2h.rs b/examples/bin2h.rs index 16f9b07..b2beb95 100644 --- a/examples/bin2h.rs +++ b/examples/bin2h.rs @@ -203,7 +203,7 @@ pub fn main() -> ExitCode { // Read & parse arguments from the command line, store results into the above structure enum Arg { Out, Bin, Txt, Whitespace, Help } const OPTIONS: Opts = Opts::new(&[ - Opt::flag(Arg::Help, &["--help", "-h"], "Show this help message and exit"), + Opt::help_flag(Arg::Help, &["--help", "-h"], "Show this help message and exit"), Opt::positional_required(Arg::Out, "out", "Path to generated header file"), Opt::value(Arg::Bin, &["--bin", "-b"], "data.bin", "Add a binary file"), Opt::value(Arg::Txt, &["--txt", "-t"], "text.txt", "Add a text file"), diff --git a/examples/btreemap.rs b/examples/btreemap.rs index 5fc442b..f8ff3d2 100644 --- a/examples/btreemap.rs +++ b/examples/btreemap.rs @@ -8,18 +8,13 @@ use std::process::ExitCode; fn main() -> ExitCode { const OPTIONS: Opts<&'static str> = Opts::new(&[ - Opt::flag("help", &["--help"], "Show this help"), + Opt::help_flag("help", &["--help"], "Show this help"), Opt::positional("positional", "positional", "Positional argument"), - Opt::value("value", &["-v", "--value"], "path", "Value option"), + Opt::value("value", &["-v", "--value"], "string", "Value option"), Opt::flag("flag", &["-f", "--flag"], "Flag option"), ]); let map = match OPTIONS.parse_map_easy() { - // TODO: There should probably be a more efficient way to make jaarg handle help for us - ParseMapResult::Map(map) if map.contains_key("help") => { - OPTIONS.print_full_help("btreemap"); - return ExitCode::SUCCESS; - } ParseMapResult::Map(map) => map, ParseMapResult::Exit(code) => { return code; } }; diff --git a/src/argparse.rs b/src/argparse.rs index fd9e608..dd2db87 100644 --- a/src/argparse.rs +++ b/src/argparse.rs @@ -138,7 +138,7 @@ impl Opts { // Ensure that all required positional arguments have been provided for option in self.options[state.positional_index..].iter() { - if matches!(option.r#type, OptType::Positional) && option.required { + if matches!(option.r#type, OptType::Positional) && option.is_required() { error(program_name, ParseError::RequiredPositional(option.first_name())); return ParseResult::ExitError; } diff --git a/src/help.rs b/src/help.rs index 984929c..82bd2fd 100644 --- a/src/help.rs +++ b/src/help.rs @@ -25,7 +25,7 @@ impl core::fmt::Display for StandardShortUsageWriter<'_, ID> { // Write option parameter arguments for option in self.0.options.options.iter() .filter(|o| matches!(o.r#type, OptType::Value | OptType::Flag)) { - write!(f, " {}", if option.required { '<' } else { '[' })?; + write!(f, " {}", if option.is_required() { '<' } else { '[' })?; match (option.first_short_name(), option.first_long_name()) { (Some(short_name), Some(long_name)) => write!(f, "{short_name}|{long_name}")?, (Some(short_name), None) => f.write_str(short_name)?, @@ -35,14 +35,14 @@ impl core::fmt::Display for StandardShortUsageWriter<'_, ID> { if let Some(value_name) = option.value_name { write!(f, " {value_name}")?; } - write!(f, "{}", if option.required { '>' } else { ']' })?; + write!(f, "{}", if option.is_required() { '>' } else { ']' })?; } // Write positional arguments for option in self.0.options.options.iter() .filter(|o| matches!(o.r#type, OptType::Positional)) { let name = option.first_name(); - match option.required { + match option.is_required() { true => write!(f, " <{name}>")?, false => write!(f, " [{name}]")?, } @@ -68,7 +68,7 @@ impl core::fmt::Display for StandardFullHelpWriter<'_, ID> { // Write optional short options let mut first = true; for option in self.0.options.options { - if let (OptType::Flag | OptType::Value, false) = (option.r#type, option.required) { + if let (OptType::Flag | OptType::Value, false) = (option.r#type, option.is_required()) { if let Some(c) = option.first_short_name_char() { if first { write!(f, " [{short_flag}")?; @@ -85,7 +85,7 @@ impl core::fmt::Display for StandardFullHelpWriter<'_, ID> { // Write required short options first = true; for option in self.0.options.options { - if let (OptType::Flag | OptType::Value, true) = (option.r#type, option.required) { + if let (OptType::Flag | OptType::Value, true) = (option.r#type, option.is_required()) { if let Some(c) = option.first_short_name_char() { if first { write!(f, " <{short_flag}")?; @@ -103,7 +103,7 @@ impl core::fmt::Display for StandardFullHelpWriter<'_, ID> { for option in self.0.options.options.iter() .filter(|o| matches!(o.r#type, OptType::Positional)) { let name = option.first_name(); - match option.required { + match option.is_required() { true => write!(f, " <{name}>")?, false => write!(f, " [{name}]")?, } diff --git a/src/option.rs b/src/option.rs index adf1f6d..1035ed4 100644 --- a/src/option.rs +++ b/src/option.rs @@ -24,44 +24,75 @@ pub struct Opt { value_name: Option<&'static str>, help_string: &'static str, r#type: OptType, - required: bool, + flags: OptFlag, +} + +#[derive(Debug)] +struct OptFlag(u8); + +impl OptFlag { + pub const REQUIRED: Self = OptFlag(1 << 0); + pub const HELP: Self = OptFlag(1 << 1); + + pub const NONE: Self = OptFlag(0); } // TODO: Improve this interface by making the name field take AsOptIdentifier when const traits are stabilised impl Opt { #[inline] - const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, help_string: &'static str, r#type: OptType, required: bool) -> Self { + const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, help_string: &'static str, r#type: OptType) -> Self { assert!(match names { OptIdentifier::Single(_) => true, OptIdentifier::Multi(names) => !names.is_empty(), }, "Option names cannot be an empty slice"); - Self { id, names, value_name, help_string, r#type, required } + Self { id, names, value_name, help_string, r#type, flags: OptFlag::NONE } } /// A positional argument that is parsed sequentially without being invoked by an option flag pub const fn positional(id: ID, name: &'static str, help_string: &'static str) -> Self { - Self { id, names: OptIdentifier::Single(name), value_name: None, help_string, r#type: OptType::Positional, required: false } + Self::new(id, OptIdentifier::Single(name), None, help_string, OptType::Positional) } /// A required positional argument that is parsed sequentially without being invoked by an option flag pub const fn positional_required(id: ID, name: &'static str, help_string: &'static str) -> Self { - Self::new(id, OptIdentifier::Single(name), None, help_string, OptType::Positional, true) + Self::new(id, OptIdentifier::Single(name), None, help_string, OptType::Positional).with_required() + } + /// A flag-type option that serves as the interface's help flag + pub const fn help_flag(id: ID, names: &'static[&'static str], help_string: &'static str) -> Self { + Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag).with_help_flag() } /// A flag-type option that takes no value pub const fn flag(id: ID, names: &'static[&'static str], help_string: &'static str) -> Self { - Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag, false) + Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag) } /// A required flag-type option that takes no value pub const fn flag_required(id: ID, names: &'static[&'static str], help_string: &'static str) -> Self { - Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag, true) + Self::new(id, OptIdentifier::Multi(names), None, help_string, OptType::Flag).with_required() } /// An option argument that takes a value pub const fn value(id: ID, names: &'static[&'static str], value_name: &'static str, help_string: &'static str) -> Self { - Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value, false) + Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value) } /// A required option argument that takes a value pub const fn value_required(id: ID, names: &'static[&'static str], value_name: &'static str, help_string: &'static str) -> Self { - Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value, true) + Self::new(id, OptIdentifier::Multi(names), Some(value_name), help_string, OptType::Value).with_required() } + + #[inline] + const fn with_required(mut self) -> Self { + assert!(!self.is_help(), "Help flag cannot be made required"); + self.flags.0 |= OptFlag::REQUIRED.0; + self + } + + #[inline] + const fn with_help_flag(mut self) -> Self { + assert!(matches!(self.r#type, OptType::Flag), "Only flags are allowed to be help options"); + self.flags.0 |= OptFlag::HELP.0; + self + } + + #[inline(always)] const fn is_required(&self) -> bool { (self.flags.0 & OptFlag::REQUIRED.0) != 0 } + #[inline(always)] const fn is_help(&self) -> bool { (self.flags.0 & OptFlag::HELP.0) != 0 } } impl Opt { diff --git a/src/std.rs b/src/std.rs index 16a0354..db848ba 100644 --- a/src/std.rs +++ b/src/std.rs @@ -56,8 +56,10 @@ impl Opts { fn easy_error(&self, program_name: &str, err: ParseError) { eprintln!("{program_name}: {err}"); self.eprint_help::>(program_name); - // TODO: only show when an option is marked help - eprintln!("Run '{program_name} --help' to view all available options."); + if let Some(help_option) = self.options.iter().find(|o| o.is_help()) { + eprintln!("Run '{program_name} {help}' to view all available options.", + help = help_option.first_long_name().unwrap_or(help_option.first_name())); + } } } @@ -72,11 +74,17 @@ impl Opts<&'static str> { /// /// Requires features = [std] pub fn parse_map<'a, S: AsRef + 'a, I: Iterator>(&self, program_name: &str, args: I, - error: impl FnOnce(&str, ParseError)) -> ParseMapResult { + 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| { - out.insert(id, arg.into()); - Ok(ParseControl::Continue) + match self.parse(&program_name, args, |_program_name, id, opt, _name, arg| { + if opt.is_help() { + help(program_name); + Ok(ParseControl::Quit) + } else { + out.insert(id, arg.into()); + Ok(ParseControl::Continue) + } }, error) { ParseResult::ContinueSuccess => ParseMapResult::Map(out), ParseResult::ExitSuccess => ParseMapResult::Exit(std::process::ExitCode::SUCCESS), @@ -85,10 +93,13 @@ impl Opts<&'static str> { } /// Parse arguments from the command line and return the results in a BTreeMap. + /// Help and errors are formatted in a standard user-friendly format. /// /// Requires features = [std] pub fn parse_map_easy(&self) -> ParseMapResult { let (program_name, argv) = Self::easy_args(); - self.parse_map(&program_name, argv, |name, e| self.easy_error(name, e)) + self.parse_map(&program_name, argv, + |name| self.print_full_help(name), + |name, e| self.easy_error(name, e)) } }