Implement required non-positional option enforcement

This commit is contained in:
2025-11-02 06:59:58 +11:00
parent 57791b1a93
commit 82b16238d2
4 changed files with 123 additions and 11 deletions

View File

@@ -37,6 +37,7 @@ pub enum ParseError<'a> {
//TODO
//Exclusive(&'static str, &'a str),
RequiredPositional(&'static str),
RequiredParameter(&'static str),
}
/// The type of parsing error
@@ -63,6 +64,7 @@ impl core::fmt::Display for ParseError<'_> {
=> write!(f, "Argument for option '{o}' cannot be empty"),
//Self::Exclusive(l, r) => write!(f, "Argument {l}: not allowed with argument {r}"),
Self::RequiredPositional(o) => write!(f, "Missing required positional argument '{o}'"),
Self::RequiredParameter(o) => write!(f, "Missing required option '{o}'"),
}
}
}
@@ -92,17 +94,21 @@ impl From<core::num::ParseFloatError> for ParseError<'_> {
impl core::error::Error for ParseError<'_> {}
type RequiredParamsBitSet = ordered_bitset::OrderedBitSet<BitSetType, BITSET_SLOTS>;
/// Internal state tracked by the parser
struct ParserState<ID: 'static> {
positional_index: usize,
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
required_param_presences: RequiredParamsBitSet,
}
impl<ID> Default for ParserState<ID> {
fn default() -> Self {
Self {
positional_index: 0,
expects_arg: None
expects_arg: None,
required_param_presences: Default::default(),
}
}
}
@@ -134,13 +140,21 @@ impl<ID: 'static> Opts<ID> {
return ParseResult::ExitError;
}
//TODO: Ensure all required parameter arguments have been provided
// 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.is_required() {
error(program_name, ParseError::RequiredPositional(option.first_name()));
return ParseResult::ExitError;
// Ensure that all required arguments have been provided
let mut required_flag_idx = 0;
for (i, option) in self.options.iter().enumerate() {
match option.r#type {
OptType::Positional => if i >= state.positional_index && option.is_required() {
error(program_name, ParseError::RequiredPositional(option.first_name()));
return ParseResult::ExitError;
}
OptType::Flag | OptType::Value => if option.is_required() {
if !state.required_param_presences.get(required_flag_idx) {
error(program_name, ParseError::RequiredParameter(option.first_name()));
return ParseResult::ExitError;
}
required_flag_idx += 1;
}
}
}
@@ -176,11 +190,26 @@ impl<ID: 'static> Opts<ID> {
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.options.iter()
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value))
.find_map(|opt| opt.match_name(option_str, 1).map(|name| (name, opt)))
.ok_or(ParseError::UnknownOption(option_str))?;
.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

View File

@@ -6,6 +6,7 @@
#![no_std]
mod const_utf8;
mod ordered_bitset;
include!("option.rs");
include!("options.rs");

View File

@@ -13,9 +13,26 @@ pub struct Opts<ID: 'static> {
description: Option<&'static str>,
}
type BitSetType = u32;
const BITSET_SLOTS: usize = 4;
/// The maximum amount of allowed required non-positional options.
pub const MAX_REQUIRED_OPTIONS: usize = BitSetType::BITS as usize * BITSET_SLOTS;
impl<ID: 'static> Opts<ID> {
/// Build argument parser options with the default flag character of '-'
pub const fn new(options: &'static[Opt<ID>]) -> Self {
// Validate passed options
let mut opt_idx = 0;
let mut num_required_parameters = 0;
while opt_idx < options.len() {
if matches!(options[opt_idx].r#type, OptType::Flag | OptType::Value) && options[opt_idx].is_required() {
num_required_parameters += 1;
}
opt_idx += 1;
}
assert!(num_required_parameters <= MAX_REQUIRED_OPTIONS,
"More than 128 non-positional required option entries is not supported at this time");
Self {
options,
flag_chars: "-",

65
src/ordered_bitset.rs Normal file
View File

@@ -0,0 +1,65 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
#![allow(private_bounds)]
use core::ops::{BitAnd, BitAndAssign, BitOrAssign, Not, Shl};
pub(crate) struct OrderedBitSet<T: OrderedBitSetStorage, const S: usize>([T; S]);
impl<T: OrderedBitSetStorage, const S: usize> Default for OrderedBitSet<T, S> {
fn default() -> Self { Self::new() }
}
// TODO: Obvious target for improvement when const traits land
impl<T: OrderedBitSetStorage, const S: usize> OrderedBitSet<T, S> {
pub(crate) const fn new() -> Self { Self([T::ZERO; S]) }
pub(crate) fn insert(&mut self, index: usize, value: bool) {
let array_idx = index >> T::SHIFT;
debug_assert!(array_idx < S, "Index out of range");
let bit_idx = index & T::MASK;
let bit_mask = T::from_usize(0b1) << T::from_usize(bit_idx);
if value {
self.0[array_idx] |= bit_mask;
} else {
self.0[array_idx] &= !bit_mask;
}
}
pub(crate) fn get(&self, index: usize) -> bool {
let array_idx = index >> T::SHIFT;
debug_assert!(array_idx < S, "Index out of range");
let bit_idx = index & T::MASK;
let bit_mask = T::from_usize(0b1) << T::from_usize(bit_idx);
(self.0[array_idx] & bit_mask) != T::from_usize(0)
}
}
trait OrderedBitSetStorage: Default + Copy + Clone + Eq + PartialEq
+ BitAnd<Output = Self> + Shl<Output = Self> + Not<Output = Self>
+ BitAndAssign + BitOrAssign {
const ZERO: Self;
const SHIFT: u32;
const MASK: usize;
fn from_usize(value: usize) -> Self;
}
macro_rules! impl_bitset_storage {
($t:ty, $b:expr) => {
impl OrderedBitSetStorage for $t {
const ZERO: $t = 0;
const SHIFT: u32 = $b.ilog2();
const MASK: usize = $b as usize - 1;
fn from_usize(value: usize) -> $t { value as $t }
}
};
}
impl_bitset_storage!(u8, u8::BITS);
impl_bitset_storage!(u16, u16::BITS);
impl_bitset_storage!(u32, u32::BITS);
impl_bitset_storage!(u64, u64::BITS);
impl_bitset_storage!(u128, u128::BITS);