diff --git a/README.md b/README.md index 04c10af..b7e7efa 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ println!("{file:?} -> {out:?} (number: {number:?})", main: * Moved `Opts::parse_map` into newly introduced `alloc` crate, making it accessible for `no_std` users. - * API updates, enough public constructs to roll a custom help writer. - * Generalised internal error & usage into `StandardErrorUsageWriter` for reuse outside the easy API & in `no_std`. - * Fixed forced newline in user display in easy API. + * More generic & flexible help API: removed forced newline, moved error writer to `StandardErrorUsageWriter`, + generalised "Usage" line in standard full writer, enough public constructs to roll a custom help writer. + * Added the ability to exclude options from short usage, full help, or both. * More tests for validating internal behaviour & enabled CI on GitHub. - * New `no_std` examples. + * Added new `no_std` examples. v0.1.1: * Fixed incorrect error message format for coerced parsing errors. diff --git a/jaarg/examples/basic.rs b/jaarg/examples/basic.rs index c3c84b0..34f5fcf 100644 --- a/jaarg/examples/basic.rs +++ b/jaarg/examples/basic.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MIT */ -use jaarg::{Opt, Opts, ParseControl, ParseResult}; +use jaarg::{Opt, OptHide, Opts, ParseControl, ParseResult}; use std::path::PathBuf; fn main() { @@ -15,7 +15,8 @@ fn main() { // Set up arguments table enum Arg { Help, Number, File, Out } const OPTIONS: Opts = Opts::new(&[ - Opt::help_flag(Arg::Help, &["-h", "--help"]).help_text("Show this help and exit."), + Opt::help_flag(Arg::Help, &["-h", "--help"]).hide_usage(OptHide::Short) + .help_text("Show this help and exit."), Opt::value(Arg::Number, &["-n", "--number"], "value") .help_text("Optionally specify a number (default: 0)"), Opt::positional(Arg::File, "file").required() diff --git a/jaarg/src/help.rs b/jaarg/src/help.rs index a96189a..2a970b2 100644 --- a/jaarg/src/help.rs +++ b/jaarg/src/help.rs @@ -31,7 +31,7 @@ impl core::fmt::Display for StandardShortUsageWriter<'_, ID> { // Write option parameter arguments for option in self.0.options.iter() - .filter(|o| matches!(o.r#type, OptType::Value | OptType::Flag)) { + .filter(|o| matches!((o.r#type, o.is_short_visible()), (OptType::Value | OptType::Flag, true))) { 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}")?, @@ -47,7 +47,7 @@ impl core::fmt::Display for StandardShortUsageWriter<'_, ID> { // Write positional arguments for option in self.0.options.iter() - .filter(|o| matches!(o.r#type, OptType::Positional)) { + .filter(|o| matches!((o.r#type, o.is_short_visible()), (OptType::Positional, true))) { let name = option.first_name(); match option.is_required() { true => write!(f, " <{name}>")?, @@ -66,8 +66,6 @@ impl<'a, ID: 'static> HelpWriter<'a, ID> for StandardFullHelpWriter<'a, ID> { impl core::fmt::Display for StandardFullHelpWriter<'_, ID> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - use core::fmt::Write; - // Base short usage writeln!(f, "{}", StandardShortUsageWriter::new(self.0.clone()))?; @@ -91,7 +89,7 @@ impl core::fmt::Display for StandardFullHelpWriter<'_, ID> { // Write positional argument descriptions let mut first = true; for option in self.0.options.iter() - .filter(|o| matches!(o.r#type, OptType::Positional)) { + .filter(|o| matches!((o.r#type, o.is_full_visible()), (OptType::Positional, true))) { if first { // Write separator and positional section header writeln!(f)?; @@ -111,7 +109,7 @@ impl core::fmt::Display for StandardFullHelpWriter<'_, ID> { // Write option parameter argument descriptions first = true; for option in self.0.options.iter() - .filter(|o| matches!(o.r#type, OptType::Flag | OptType::Value)) { + .filter(|o| matches!((o.r#type, o.is_full_visible()), (OptType::Flag | OptType::Value, true))) { if first { // Write separator and options section header writeln!(f)?; diff --git a/jaarg/src/option.rs b/jaarg/src/option.rs index b669653..6eca92b 100644 --- a/jaarg/src/option.rs +++ b/jaarg/src/option.rs @@ -27,14 +27,24 @@ pub struct Opt { flags: OptFlag, } +pub enum OptHide { + Short, + Full, + All, +} + #[derive(Debug, PartialEq)] struct OptFlag(u8); impl OptFlag { - pub const REQUIRED: Self = OptFlag(1 << 0); - pub const HELP: Self = OptFlag(1 << 1); + #[allow(dead_code)] + pub const NONE: Self = Self(0); + pub const REQUIRED: Self = OptFlag(1 << 0); + pub const HELP: Self = OptFlag(1 << 1); + pub const VISIBLE_SHORT: Self = OptFlag(1 << 2); + pub const VISIBLE_FULL: Self = OptFlag(1 << 3); - pub const NONE: Self = OptFlag(0); + pub const DEFAULT: Self = Self(Self::VISIBLE_SHORT.0 | Self::VISIBLE_FULL.0); } // TODO: Improve this interface by making the name field take AsOptIdentifier when const traits are stabilised @@ -45,7 +55,7 @@ impl Opt { OptIdentifier::Single(_) => true, OptIdentifier::Multi(names) => !names.is_empty(), }, "Option names cannot be an empty slice"); - Self { id, names, value_name, help_string: None, r#type, flags: OptFlag::NONE } + Self { id, names, value_name, help_string: None, r#type, flags: OptFlag::DEFAULT } } /// A positional argument that is parsed sequentially without being invoked by an option flag. @@ -81,6 +91,17 @@ impl Opt { self } + /// Marks the option to exclude it from appearing in short usage text, full help text, or both. + #[inline] + pub const fn hide_usage(mut self, from: OptHide) -> Self { + self.flags.0 &= !match from { + OptHide::Short => OptFlag::VISIBLE_SHORT.0, + OptHide::Full => OptFlag::VISIBLE_FULL.0, + OptHide::All => OptFlag::VISIBLE_SHORT.0 | OptFlag::VISIBLE_FULL.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"); @@ -89,14 +110,26 @@ impl Opt { } /// Returns true if this is a required positional argument, or required option argument. - #[inline(always)] pub const fn is_required(&self) -> bool { + #[inline(always)] + pub const fn is_required(&self) -> bool { (self.flags.0 & OptFlag::REQUIRED.0) != 0 } /// Returns true if this is the help option. - #[inline(always)] pub const fn is_help(&self) -> bool { + #[inline(always)] + pub const fn is_help(&self) -> bool { (self.flags.0 & OptFlag::HELP.0) != 0 } + + #[inline(always)] + const fn is_short_visible(&self) -> bool { + (self.flags.0 & OptFlag::VISIBLE_SHORT.0) != 0 + } + + #[inline(always)] + const fn is_full_visible(&self) -> bool { + (self.flags.0 & OptFlag::VISIBLE_FULL.0) != 0 + } } impl Opt { @@ -199,6 +232,10 @@ impl Opt { } } +impl core::ops::BitOr for OptFlag { + type Output = Self; + fn bitor(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) } +} #[cfg(test)] mod opt_tests { @@ -214,19 +251,19 @@ mod opt_tests { fn test_public_initialisers() { assert_eq!(Opt::positional((), "name"), Opt { id: (), names: OptIdentifier::Single("name"), value_name: None, help_string: None, - r#type: OptType::Positional, flags: OptFlag::NONE, + r#type: OptType::Positional, flags: OptFlag::DEFAULT, }); assert_eq!(Opt::help_flag((), &["name"]), Opt { id: (), names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None, - r#type: OptType::Flag, flags: OptFlag::HELP, + r#type: OptType::Flag, flags: OptFlag::DEFAULT | OptFlag::HELP, }); assert_eq!(Opt::flag((), &["name"]), Opt { id: (), names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None, - r#type: OptType::Flag, flags: OptFlag::NONE, + r#type: OptType::Flag, flags: OptFlag::DEFAULT, }); assert_eq!(Opt::value((), &["name"], "value"), Opt { id: (), names: OptIdentifier::Multi(&["name"]), value_name: Some("value"), help_string: None, - r#type: OptType::Value, flags: OptFlag::NONE, + r#type: OptType::Value, flags: OptFlag::DEFAULT, }); } @@ -234,16 +271,32 @@ mod opt_tests { fn test_valid_with_chains() { assert_eq!(Opt::positional((), "").required(), Opt { id: (), names: OptIdentifier::Single(""), value_name: None, help_string: None, - r#type: OptType::Positional, flags: OptFlag::REQUIRED, + r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED, }); assert_eq!(Opt::positional((), "").required().help_text("help string"), Opt { id: (), names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"), - r#type: OptType::Positional, flags: OptFlag::REQUIRED, + r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED, }); assert_eq!(Opt::positional((), "").help_text("help string"), Opt { id: (), names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"), + r#type: OptType::Positional, flags: OptFlag::DEFAULT, + }); + assert_eq!(Opt::positional((), "").hide_usage(OptHide::Short), Opt { id: (), + names: OptIdentifier::Single(""), value_name: None, help_string: None, + r#type: OptType::Positional, flags: OptFlag::VISIBLE_FULL, + }); + assert_eq!(Opt::positional((), "").hide_usage(OptHide::Full), Opt { id: (), + names: OptIdentifier::Single(""), value_name: None, help_string: None, + r#type: OptType::Positional, flags: OptFlag::VISIBLE_SHORT, + }); + assert_eq!(Opt::positional((), "").hide_usage(OptHide::All), Opt { id: (), + names: OptIdentifier::Single(""), value_name: None, help_string: None, r#type: OptType::Positional, flags: OptFlag::NONE, }); + assert_eq!(Opt::positional((), "").required().hide_usage(OptHide::All), Opt { id: (), + names: OptIdentifier::Single(""), value_name: None, help_string: None, + r#type: OptType::Positional, flags: OptFlag::REQUIRED, + }); } #[test]