#![warn(clippy::pedantic)]
#![warn(clippy::complexity)]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
use core::{error::Error, ffi::CStr, fmt::Display, iter::Peekable, mem, str::Utf8Error};
#[cfg(feature = "std")]
use std::{
borrow::Cow,
env,
ffi::{CString, OsStr, OsString},
fmt::Debug,
};
#[cfg(not(feature = "std"))]
use alloc::{
borrow::{Cow, ToOwned},
ffi::CString,
string::{String, ToString},
};
pub type Result<T, E = ParsingError> = core::result::Result<T, E>;
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone)]
pub enum Argument<'a> {
Long(&'a str),
Short(char),
Value(Cow<'a, str>),
Stdio,
}
impl Argument<'_> {
#[must_use]
pub fn unexpected(&self) -> ParsingError {
ParsingError::Unexpected {
argument: self.to_string(),
}
}
}
impl Display for Argument<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use Argument::{Long, Short, Stdio, Value};
match self {
Long(s) => write!(f, "--{s}"),
Short(ch) => write!(f, "-{ch}"),
Value(cow) => write!(f, "{cow}"),
Stdio => write!(f, "-"),
}
}
}
pub struct Parser<I: Iterator> {
iter: Peekable<I>,
state: State,
name: String,
last_arg: String,
}
enum State {
NotInteresting,
LeftoverValue(String),
Combined(usize, String),
End,
Poisoned,
}
#[cfg(feature = "std")]
impl Parser<env::Args> {
pub fn from_env() -> Result<Self> {
Self::from_arbitrary(env::args())
}
}
pub trait ArgLike: sealed::Sealed {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error>;
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error>;
}
impl ArgLike for String {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error> {
Ok(Cow::Owned(self))
}
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error> {
Ok(Cow::Borrowed(self.as_str()))
}
}
impl ArgLike for &str {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error> {
Ok(Cow::Owned(self.to_owned()))
}
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error> {
Ok(Cow::Borrowed(*self))
}
}
impl ArgLike for &CStr {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error> {
Ok(Cow::Owned(self.to_str()?.to_owned()))
}
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error> {
self.to_str().map(Cow::Borrowed)
}
}
impl ArgLike for CString {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error> {
self.into_string()
.map(Cow::Owned)
.map_err(|e| e.utf8_error())
}
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error> {
self.to_str().map(Cow::Borrowed)
}
}
#[cfg(feature = "std")]
impl ArgLike for &OsStr {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error> {
core::str::from_utf8(self.as_encoded_bytes()).map(|s| Cow::Owned(s.to_owned()))
}
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error> {
match self.to_str() {
Some(s) => Ok(Cow::Borrowed(s)),
None => Err(core::str::from_utf8(self.as_encoded_bytes()).unwrap_err()),
}
}
}
#[cfg(feature = "std")]
impl ArgLike for OsString {
fn into_arg<'v>(self) -> Result<Cow<'v, str>, Utf8Error> {
core::str::from_utf8(self.as_encoded_bytes()).map(|s| Cow::Owned(s.to_owned()))
}
fn as_arg(&self) -> Result<Cow<'_, str>, Utf8Error> {
match self.to_str() {
Some(s) => Ok(Cow::Borrowed(s)),
None => Err(core::str::from_utf8(self.as_encoded_bytes()).unwrap_err()),
}
}
}
mod sealed {
#[cfg(not(feature = "std"))]
use alloc::ffi::CString;
use core::ffi::CStr;
#[cfg(feature = "std")]
use std::ffi::{CString, OsStr, OsString};
pub trait Sealed {}
impl Sealed for String {}
impl Sealed for &str {}
impl Sealed for &CStr {}
impl Sealed for CString {}
#[cfg(feature = "std")]
impl Sealed for &OsStr {}
#[cfg(feature = "std")]
impl Sealed for OsString {}
}
impl<'a, I, V> Parser<I>
where
I: Iterator<Item = V>,
V: ArgLike,
{
pub fn from_arbitrary<A>(iter: A) -> Result<Parser<I>>
where
A: IntoIterator<IntoIter = I>,
{
let mut iter = iter.into_iter().peekable();
let name = iter
.next()
.ok_or(ParsingError::Empty)?
.into_arg()?
.into_owned();
Ok(Parser {
iter,
state: State::NotInteresting,
name,
last_arg: String::new(),
})
}
pub fn forward(&'a mut self) -> Result<Option<Argument<'a>>> {
loop {
match self.state {
State::Poisoned => return Ok(None),
State::End => {
return match self.iter.next() {
Some(v) => Ok(Some(Argument::Value(v.into_arg()?))),
None => Ok(None),
};
}
State::Combined(index, ref mut options) => {
let options = mem::take(options);
match options.chars().nth(index) {
Some(char) => {
if char == '=' {
self.state = State::Poisoned;
return Err(ParsingError::InvalidSyntax {
reason: "Short options do not support values",
});
}
self.state = State::Combined(index + 1, options);
return Ok(Some(Argument::Short(char)));
}
None => self.state = State::NotInteresting,
}
}
State::NotInteresting => {
let next = match self.iter.next() {
Some(s) => s.into_arg()?,
None => return Ok(None),
};
match next.strip_prefix("-") {
Some("") => return Ok(Some(Argument::Stdio)),
Some("-") => {
self.state = State::End;
}
Some(rest) => {
if rest.starts_with('-') {
self.last_arg = next.into_owned();
if let Some(index) = self.last_arg.find('=') {
self.state =
State::LeftoverValue(self.last_arg[index + 1..].to_owned());
return Ok(Some(Argument::Long(&self.last_arg[2..index])));
}
return Ok(Some(Argument::Long(&self.last_arg[2..])));
}
self.state = State::Combined(0, rest.to_owned());
}
None => {
return Ok(Some(Argument::Value(next)));
}
}
}
State::LeftoverValue(ref mut value) => {
let value = mem::take(value);
self.state = State::Poisoned;
return Err(ParsingError::UnconsumedValue { value });
}
}
}
}
pub fn value(&mut self) -> Result<Option<String>> {
match self.state {
State::Combined(index, ref options) if index >= options.len() => {
self.state = State::NotInteresting;
}
_ => {}
}
match self.state {
State::End | State::Poisoned | State::Combined(..) => Ok(None),
State::LeftoverValue(ref mut value) => {
let value = mem::take(value);
self.state = State::NotInteresting;
Ok(Some(value))
}
State::NotInteresting => {
let arg = match self.iter.peek() {
Some(v) => {
let arg = v.as_arg()?;
if arg.starts_with('-') {
return Ok(None);
}
arg.into_owned()
}
None => return Ok(None),
};
self.iter.next();
Ok(Some(arg))
}
}
}
pub fn ignore_value(&mut self) {
let _ = self.value();
}
pub fn name(&self) -> &str {
&self.name
}
pub const fn is_poisoned(&self) -> bool {
matches!(self.state, State::Poisoned)
}
pub const fn has_leftover_value(&self) -> bool {
matches!(self.state, State::LeftoverValue(_))
}
pub fn into_inner(self) -> Peekable<I> {
self.iter
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsingError {
Empty,
InvalidSyntax {
reason: &'static str,
},
UnconsumedValue {
value: String,
},
Unexpected {
argument: String,
},
Utf8Error(Utf8Error),
}
impl Display for ParsingError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Empty => write!(f, "argument list is empty"),
Self::InvalidSyntax { reason } => write!(f, "invalid syntax: {reason}"),
Self::UnconsumedValue { value } => write!(f, "unconsumed value: {value}"),
Self::Unexpected { argument } => write!(f, "unexpected argument: {argument}"),
Self::Utf8Error(err) => Display::fmt(err, f),
}
}
}
impl Error for ParsingError {}
impl From<Utf8Error> for ParsingError {
fn from(err: Utf8Error) -> Self {
ParsingError::Utf8Error(err)
}
}