Add with chain for excluding options from showing up in help

This commit is contained in:
2025-11-15 16:50:30 +11:00
parent 741dfd4d7e
commit 8c30e5c526
4 changed files with 76 additions and 24 deletions

View File

@@ -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.

View File

@@ -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<Arg> = 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()

View File

@@ -31,7 +31,7 @@ impl<ID: 'static> 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<ID: 'static> 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<ID> 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<ID> 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<ID> 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)?;

View File

@@ -27,14 +27,24 @@ pub struct Opt<ID> {
flags: OptFlag,
}
pub enum OptHide {
Short,
Full,
All,
}
#[derive(Debug, PartialEq)]
struct OptFlag(u8);
impl OptFlag {
#[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<ID> Opt<ID> {
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<ID> Opt<ID> {
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<ID> Opt<ID> {
}
/// 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<ID: 'static> Opt<ID> {
@@ -199,6 +232,10 @@ impl<ID: 'static> Opt<ID> {
}
}
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]