diff --git a/src/argparse.rs b/src/argparse.rs index dd2db87..55c35e8 100644 --- a/src/argparse.rs +++ b/src/argparse.rs @@ -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 for ParseError<'_> { impl core::error::Error for ParseError<'_> {} +type RequiredParamsBitSet = ordered_bitset::OrderedBitSet; + /// Internal state tracked by the parser struct ParserState { positional_index: usize, expects_arg: Option<(&'static str, &'static Opt)>, + required_param_presences: RequiredParamsBitSet, } impl Default for ParserState { fn default() -> Self { Self { positional_index: 0, - expects_arg: None + expects_arg: None, + required_param_presences: Default::default(), } } } @@ -134,13 +140,21 @@ impl Opts { 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 Opts { 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 diff --git a/src/lib.rs b/src/lib.rs index aad691e..42c15a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ #![no_std] mod const_utf8; +mod ordered_bitset; include!("option.rs"); include!("options.rs"); diff --git a/src/options.rs b/src/options.rs index d63f64c..960d780 100644 --- a/src/options.rs +++ b/src/options.rs @@ -13,9 +13,26 @@ pub struct Opts { 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 Opts { /// Build argument parser options with the default flag character of '-' pub const fn new(options: &'static[Opt]) -> 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: "-", diff --git a/src/ordered_bitset.rs b/src/ordered_bitset.rs new file mode 100644 index 0000000..9784327 --- /dev/null +++ b/src/ordered_bitset.rs @@ -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; S]); + +impl Default for OrderedBitSet { + fn default() -> Self { Self::new() } +} + +// TODO: Obvious target for improvement when const traits land +impl OrderedBitSet { + 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 + Shl + Not + + 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);