use std::ffi::OsStr;
use crate::__private::{
Action, Arg, Command, FIELDS_FLAG, Flag, Named, OUTPUT_FLAG, SCHEMA_ACTION, resolve_long,
resolve_short,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event<'t, 'v> {
Action {
action: &'t Action<'t>,
long: bool,
},
Flag {
flag: &'t Flag<'t>,
value: Option<&'v [u8]>,
},
Arg {
arg: &'t Arg<'t>,
value: &'v [u8],
},
Command {
command: &'t Command<'t>,
},
Output {
value: &'v [u8],
},
Fields {
value: &'v [u8],
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error<'t, 'v> {
UnexpectedActionValue {
action: &'t Action<'t>,
},
UnknownFlag {
token: &'v [u8],
},
MissingFlagValue {
flag: &'t Flag<'t>,
},
UnexpectedFlagValue {
flag: &'t Flag<'t>,
},
UnexpectedArg {
token: &'v [u8],
},
UnknownCommand {
token: &'v [u8],
},
}
#[derive(Debug)]
pub struct ArgvParser<'t, 'a, 'v> {
command: &'t Command<'t>,
ancestors: Vec<&'t Command<'t>>,
argv: &'a [&'v OsStr],
position: usize,
arg_position: usize,
bundle: &'v [u8],
bundle_token: &'v [u8],
flags_stopped: bool,
done: bool,
schema_enabled: bool,
}
impl<'t, 'a, 'v> ArgvParser<'t, 'a, 'v> {
#[must_use]
pub const fn new(command: &'t Command<'t>, argv: &'a [&'v OsStr]) -> Self {
Self::new_with_schema(command, argv, false)
}
#[must_use]
pub const fn new_with_schema(
command: &'t Command<'t>,
argv: &'a [&'v OsStr],
schema_enabled: bool,
) -> Self {
Self {
command,
ancestors: Vec::new(),
argv,
position: 0,
arg_position: 0,
bundle: &[],
bundle_token: &[],
flags_stopped: false,
done: false,
schema_enabled,
}
}
pub fn next_event(&mut self) -> Option<Result<Event<'t, 'v>, Error<'t, 'v>>> {
if self.done {
return None;
}
let event = self.step();
if matches!(event.as_ref(), Some(Err(_) | Ok(Event::Action { .. }))) {
self.done = true;
}
event
}
fn step(&mut self) -> Option<Result<Event<'t, 'v>, Error<'t, 'v>>> {
if !self.bundle.is_empty() {
return Some(self.short_flag());
}
let token = bytes(self.argv.get(self.position)?);
self.position += 1;
if self.flags_stopped {
return Some(self.word(token));
}
if token == b"--" {
self.flags_stopped = true;
return self.step();
}
if routes_negative_number_to_arg(self.command, &self.ancestors, self.next_arg(), token) {
return Some(self.word(token));
}
if is_flag_like(token) {
if token.starts_with(b"--") {
return Some(self.long_flag(token));
}
if let Err(error) = self.check_short_bundle(token) {
return Some(Err(error));
}
self.bundle = &token[1..];
self.bundle_token = token;
return Some(self.short_flag());
}
Some(self.word(token))
}
fn long_flag(&mut self, token: &'v [u8]) -> Result<Event<'t, 'v>, Error<'t, 'v>> {
let body = &token[2..];
let (name, attached) = body
.iter()
.position(|byte| *byte == b'=')
.map_or((body, None), |index| (&body[..index], Some(&body[index + 1..])));
if name == b"output" {
let value = match attached {
Some(value) => value,
None => self.take_detached_value(&OUTPUT_FLAG)?,
};
return Ok(Event::Output { value });
}
if name == b"fields" {
let value = match attached {
Some(value) => value,
None => self.take_detached_value(&FIELDS_FLAG)?,
};
return Ok(Event::Fields { value });
}
if self.schema_enabled && name == b"schema" {
return if attached.is_some() {
Err(Error::UnexpectedActionValue { action: &SCHEMA_ACTION })
} else {
Ok(Event::Action { action: &SCHEMA_ACTION, long: true })
};
}
let flag = match resolve_long(self.command, &self.ancestors, name) {
Some(Named::Action(action)) => {
return if attached.is_some() {
Err(Error::UnexpectedActionValue { action })
} else {
Ok(Event::Action { action, long: true })
};
}
Some(Named::Flag { flag, .. }) => flag,
None => return Err(Error::UnknownFlag { token }),
};
let value = if flag.takes_value {
match attached {
Some(value) => Some(value),
None => Some(self.take_detached_value(flag)?),
}
} else if attached.is_some() {
return Err(Error::UnexpectedFlagValue { flag });
} else {
None
};
Ok(Event::Flag { flag, value })
}
fn check_short_bundle(&self, token: &'v [u8]) -> Result<(), Error<'t, 'v>> {
let mut remaining = &token[1..];
while let Some((&short, tail)) = remaining.split_first() {
if short == b'O' {
return Ok(());
}
if short == b'F' {
return Ok(());
}
if self.schema_enabled && short == b'S' {
remaining = tail;
continue;
}
match resolve_short(self.command, &self.ancestors, short) {
Some(Named::Flag { flag, .. }) if flag.takes_value => return Ok(()),
Some(Named::Action(_) | Named::Flag { .. }) => remaining = tail,
None => return Err(Error::UnknownFlag { token }),
}
}
Ok(())
}
fn short_flag(&mut self) -> Result<Event<'t, 'v>, Error<'t, 'v>> {
let Some((&short, rest)) = self.bundle.split_first() else {
return Err(Error::UnknownFlag { token: self.bundle_token });
};
if short == b'O' {
self.bundle = &[];
let value = if rest.is_empty() {
self.take_detached_value(&OUTPUT_FLAG)?
} else {
rest.strip_prefix(b"=").unwrap_or(rest)
};
return Ok(Event::Output { value });
}
if short == b'F' {
self.bundle = &[];
let value = if rest.is_empty() {
self.take_detached_value(&FIELDS_FLAG)?
} else {
rest.strip_prefix(b"=").unwrap_or(rest)
};
return Ok(Event::Fields { value });
}
if self.schema_enabled && short == b'S' {
self.bundle = &[];
return Ok(Event::Action { action: &SCHEMA_ACTION, long: false });
}
let flag = match resolve_short(self.command, &self.ancestors, short) {
Some(Named::Action(action)) => {
self.bundle = &[];
return Ok(Event::Action { action, long: false });
}
Some(Named::Flag { flag, .. }) => flag,
None => {
self.bundle = &[];
return Err(Error::UnknownFlag { token: self.bundle_token });
}
};
if !flag.takes_value {
self.bundle = rest;
return Ok(Event::Flag { flag, value: None });
}
self.bundle = &[];
let value = if rest.is_empty() {
self.take_detached_value(flag)?
} else {
rest.strip_prefix(b"=").unwrap_or(rest)
};
Ok(Event::Flag { flag, value: Some(value) })
}
fn take_detached_value(&mut self, flag: &'t Flag<'t>) -> Result<&'v [u8], Error<'t, 'v>> {
let Some(value) = self.argv.get(self.position).copied().map(bytes) else {
return Err(Error::MissingFlagValue { flag });
};
if !accepts_detached_flag_value(flag, value) {
return Err(Error::MissingFlagValue { flag });
}
self.position += 1;
Ok(value)
}
fn word(&mut self, token: &'v [u8]) -> Result<Event<'t, 'v>, Error<'t, 'v>> {
if !self.flags_stopped
&& let Some(command) = self.find_subcommand(token)
{
self.ancestors.push(self.command);
self.command = command;
self.arg_position = 0;
return Ok(Event::Command { command });
}
let Some(arg) = self.next_arg() else {
return if !self.flags_stopped && !self.command.subcommands.is_empty() {
Err(Error::UnknownCommand { token })
} else {
Err(Error::UnexpectedArg { token })
};
};
if !arg.variadic {
self.arg_position += 1;
}
Ok(Event::Arg { arg, value: token })
}
pub(crate) fn next_arg(&self) -> Option<&'t Arg<'t>> {
self.command.args.get(self.arg_position).copied()
}
fn find_subcommand(&self, name: &[u8]) -> Option<&'t Command<'t>> {
self.command.subcommands.iter().copied().find(|command| {
command.name.as_bytes() == name
|| command.aliases.iter().any(|alias| alias.as_bytes() == name)
})
}
pub(crate) const fn command(&self) -> &'t Command<'t> {
self.command
}
pub(crate) fn ancestors(&self) -> &[&'t Command<'t>] {
&self.ancestors
}
pub(crate) const fn flags_stopped(&self) -> bool {
self.flags_stopped
}
pub(crate) const fn at_end(&self) -> bool {
self.position == self.argv.len() && self.bundle.is_empty()
}
pub(crate) fn remaining_args(&self) -> &[&'v OsStr] {
&self.argv[self.position..]
}
pub(crate) fn command_path(&self) -> impl DoubleEndedIterator<Item = &'t Command<'t>> + '_ {
self.ancestors.iter().copied().chain(std::iter::once(self.command))
}
}
#[cfg(unix)]
fn bytes(value: &OsStr) -> &[u8] {
use std::os::unix::ffi::OsStrExt as _;
value.as_bytes()
}
#[cfg(not(unix))]
fn bytes(value: &OsStr) -> &[u8] {
value.as_encoded_bytes()
}
const fn is_flag_like(token: &[u8]) -> bool {
matches!(token, [b'-', rest @ ..] if !rest.is_empty())
}
pub(crate) fn accepts_detached_flag_value(flag: &Flag<'_>, value: &[u8]) -> bool {
flag.allow_hyphen_values
|| !is_flag_like(value)
|| (flag.allow_negative_numbers && is_negative_number(value))
}
pub(crate) fn routes_negative_number_to_arg<'t>(
command: &'t Command<'t>,
ancestors: &[&'t Command<'t>],
next_arg: Option<&'t Arg<'t>>,
token: &[u8],
) -> bool {
let declared_numeric_short = matches!(token, [b'-', short]
if short.is_ascii_digit()
&& matches!(
resolve_short(command, ancestors, *short),
Some(Named::Flag { .. })
));
!declared_numeric_short
&& is_negative_number(token)
&& next_arg.is_some_and(|argument| argument.allow_negative_numbers)
}
fn is_negative_number(token: &[u8]) -> bool {
token.strip_prefix(b"-").is_some_and(is_number)
}
fn is_number(token: &[u8]) -> bool {
let (mantissa, exponent) = token
.iter()
.position(|byte| matches!(byte, b'e' | b'E'))
.map_or((token, None), |index| (&token[..index], Some(&token[index + 1..])));
let mut seen_digit = false;
let mut seen_dot = false;
for &byte in mantissa {
match byte {
b'0'..=b'9' => seen_digit = true,
b'.' if !seen_dot => seen_dot = true,
_ => return false,
}
}
if !seen_digit {
return false;
}
exponent.is_none_or(|exponent| {
let digits =
exponent.strip_prefix(b"+").or_else(|| exponent.strip_prefix(b"-")).unwrap_or(exponent);
!digits.is_empty() && digits.iter().all(u8::is_ascii_digit)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognizes_supported_number_shapes() {
for value in [
&b"1"[..],
&b"1.5"[..],
&b".5"[..],
&b"1."[..],
&b"1e5"[..],
&b"1e-5"[..],
&b"1E+5"[..],
] {
assert!(is_number(value), "{value:?}");
}
for value in [&b""[..], &b"."[..], &b"e1"[..], &b"1e"[..], &b"1.2.3"[..], &b"1x"[..]] {
assert!(!is_number(value), "{value:?}");
}
assert!(is_negative_number(b"-1"));
assert!(is_negative_number(b"-1.5e2"));
assert!(!is_negative_number(b"--1"));
assert!(!is_negative_number(b"-inf"));
}
}