mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-18 23:10:17 +00:00
Implement required non-positional option enforcement
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#![no_std]
|
||||
|
||||
mod const_utf8;
|
||||
mod ordered_bitset;
|
||||
|
||||
include!("option.rs");
|
||||
include!("options.rs");
|
||||
|
||||
@@ -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
65
src/ordered_bitset.rs
Normal 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);
|
||||
Reference in New Issue
Block a user