mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-19 07:20:18 +00:00
Implement required non-positional option enforcement
This commit is contained in:
@@ -37,6 +37,7 @@ pub enum ParseError<'a> {
|
|||||||
//TODO
|
//TODO
|
||||||
//Exclusive(&'static str, &'a str),
|
//Exclusive(&'static str, &'a str),
|
||||||
RequiredPositional(&'static str),
|
RequiredPositional(&'static str),
|
||||||
|
RequiredParameter(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of parsing error
|
/// The type of parsing error
|
||||||
@@ -63,6 +64,7 @@ impl core::fmt::Display for ParseError<'_> {
|
|||||||
=> write!(f, "Argument for option '{o}' cannot be empty"),
|
=> write!(f, "Argument for option '{o}' cannot be empty"),
|
||||||
//Self::Exclusive(l, r) => write!(f, "Argument {l}: not allowed with argument {r}"),
|
//Self::Exclusive(l, r) => write!(f, "Argument {l}: not allowed with argument {r}"),
|
||||||
Self::RequiredPositional(o) => write!(f, "Missing required positional argument '{o}'"),
|
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<'_> {}
|
impl core::error::Error for ParseError<'_> {}
|
||||||
|
|
||||||
|
type RequiredParamsBitSet = ordered_bitset::OrderedBitSet<BitSetType, BITSET_SLOTS>;
|
||||||
|
|
||||||
/// Internal state tracked by the parser
|
/// Internal state tracked by the parser
|
||||||
struct ParserState<ID: 'static> {
|
struct ParserState<ID: 'static> {
|
||||||
positional_index: usize,
|
positional_index: usize,
|
||||||
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
|
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
|
||||||
|
required_param_presences: RequiredParamsBitSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ID> Default for ParserState<ID> {
|
impl<ID> Default for ParserState<ID> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
positional_index: 0,
|
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;
|
return ParseResult::ExitError;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Ensure all required parameter arguments have been provided
|
// Ensure that all required arguments have been provided
|
||||||
|
let mut required_flag_idx = 0;
|
||||||
// Ensure that all required positional arguments have been provided
|
for (i, option) in self.options.iter().enumerate() {
|
||||||
for option in self.options[state.positional_index..].iter() {
|
match option.r#type {
|
||||||
if matches!(option.r#type, OptType::Positional) && option.is_required() {
|
OptType::Positional => if i >= state.positional_index && option.is_required() {
|
||||||
error(program_name, ParseError::RequiredPositional(option.first_name()));
|
error(program_name, ParseError::RequiredPositional(option.first_name()));
|
||||||
return ParseResult::ExitError;
|
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("=")
|
let (option_str, value_str) = token.split_once("=")
|
||||||
.map_or((token, None), |(k, v)| (k, Some(v)));
|
.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)
|
// Match a suitable option by name (ignoring the first flag character & skipping positional arguments)
|
||||||
let (name, option) = self.options.iter()
|
let (name, option) = self.options.iter()
|
||||||
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value))
|
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)) .find_map(|opt| {
|
||||||
.find_map(|opt| opt.match_name(option_str, 1).map(|name| (name, opt)))
|
if let Some(name) = opt.match_name(option_str, 1) {
|
||||||
.ok_or(ParseError::UnknownOption(option_str))?;
|
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) {
|
match (&option.r#type, value_str) {
|
||||||
// Call handler for flag-only options
|
// Call handler for flag-only options
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
mod const_utf8;
|
mod const_utf8;
|
||||||
|
mod ordered_bitset;
|
||||||
|
|
||||||
include!("option.rs");
|
include!("option.rs");
|
||||||
include!("options.rs");
|
include!("options.rs");
|
||||||
|
|||||||
@@ -13,9 +13,26 @@ pub struct Opts<ID: 'static> {
|
|||||||
description: Option<&'static str>,
|
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> {
|
impl<ID: 'static> Opts<ID> {
|
||||||
/// Build argument parser options with the default flag character of '-'
|
/// Build argument parser options with the default flag character of '-'
|
||||||
pub const fn new(options: &'static[Opt<ID>]) -> Self {
|
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 {
|
Self {
|
||||||
options,
|
options,
|
||||||
flag_chars: "-",
|
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