#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(clippy::unused_trait_names)]
#![warn(missing_docs)]
#![warn(unused_results)]
pub mod argument;
#[cfg(test)]
mod tests;
pub mod util;
#[cfg(all(doc, feature = "std"))]
use std::ffi::OsStr;
use crate::argument::{Argument, NotAscii};
#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Token<Arg>
where
Arg: Argument,
{
Freestanding(Arg),
Short(u8),
InvalidShort(Arg),
Long(Arg::LongOpt),
Hanging(Arg),
}
enum State<Arg> {
Normal,
InShort {
rest: Arg,
},
InLong {
value: Arg,
},
}
pub struct ArgTokens<Args>
where
Args: Iterator<Item: Argument>,
{
args: core::iter::Peekable<Args>,
state: State<Args::Item>,
}
impl<Args> ArgTokens<Args>
where
Args: Iterator<Item: Argument>,
{
#[inline]
#[must_use]
pub fn new(args: impl IntoIterator<IntoIter = Args>) -> Self {
Self {
args: args.into_iter().peekable(),
state: State::Normal,
}
}
#[expect(clippy::should_implement_trait)]
#[cfg_attr(feature = "inline-next", inline)]
pub fn next(&mut self) -> Option<Token<Args::Item>> {
let token = loop {
break match self.state {
State::Normal => {
let current = self.args.next()?;
if let Some((opt, value)) = current.parse_long_option() {
if let Some(value) = value {
self.state = State::InLong { value };
}
Token::Long(opt)
} else if let Some(rest) = current.parse_short_option_group() {
self.state = State::InShort { rest };
continue;
} else {
Token::Freestanding(current)
}
}
State::InShort { rest } => match rest.split_short_option() {
None => {
self.state = State::Normal;
continue;
}
Some(Err(NotAscii)) => {
self.state = State::Normal;
Token::InvalidShort(rest)
}
Some(Ok((opt, rest))) => {
self.state = State::InShort { rest };
Token::Short(opt)
}
},
State::InLong { value } => {
self.state = State::Normal;
Token::Hanging(value)
}
};
};
Some(token)
}
#[cfg_attr(feature = "inline-next", inline)]
pub fn next_with_check(
&mut self,
force_freestanding: impl FnOnce(Args::Item) -> bool,
) -> Option<Token<Args::Item>> {
let freestanding_forced = !self.peek_value_is_attached()
&& self.args.peek().copied().is_some_and(force_freestanding);
if freestanding_forced {
self.args.next().map(Token::Freestanding)
} else {
self.next()
}
}
pub fn next_value(&mut self) -> Option<Args::Item> {
match self.state {
State::Normal => self.args.next(),
State::InShort { rest } if rest.is_empty() => self.args.next(),
State::InShort { rest: value } | State::InLong { value } => {
self.state = State::Normal;
Some(value)
}
}
}
#[must_use]
pub fn peek_value(&mut self) -> Option<Args::Item> {
match self.state {
State::Normal => self.args.peek().copied(),
State::InShort { rest } if rest.is_empty() => self.args.peek().copied(),
State::InShort { rest: value } | State::InLong { value } => Some(value),
}
}
#[must_use]
pub fn peek_value_is_attached(&self) -> bool {
matches!(self.state, State::InShort { rest } if !rest.is_empty())
|| matches!(self.state, State::InLong { .. })
}
}