#![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations, elided_lifetimes_in_paths)]
#![allow(clippy::should_implement_trait)]
use std::{
ffi::{OsStr, OsString},
fmt::Display,
mem::replace,
str::FromStr,
};
#[cfg(unix)]
use std::os::unix::ffi::{OsStrExt, OsStringExt};
#[cfg(target_os = "wasi")]
use std::os::wasi::ffi::{OsStrExt, OsStringExt};
#[cfg(windows)]
use std::os::windows::ffi::{OsStrExt, OsStringExt};
type InnerIter = std::vec::IntoIter<OsString>;
fn make_iter(iter: impl Iterator<Item = OsString>) -> InnerIter {
iter.collect::<Vec<_>>().into_iter()
}
#[derive(Debug, Clone)]
pub struct Parser {
source: InnerIter,
state: State,
last_option: LastOption,
bin_name: Option<String>,
}
#[derive(Debug, Clone)]
enum State {
None,
PendingValue(OsString),
Shorts(Vec<u8>, usize),
#[cfg(windows)]
ShortsU16(Vec<u16>, usize),
FinishedOpts,
}
#[derive(Debug, Clone)]
enum LastOption {
None,
Short(char),
Long(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Arg<'a> {
Short(char),
Long(&'a str),
Value(OsString),
}
impl Parser {
pub fn next(&mut self) -> Result<Option<Arg<'_>>, Error> {
match self.state {
State::PendingValue(ref mut value) => {
let value = replace(value, OsString::new());
self.state = State::None;
return Err(Error::UnexpectedValue {
option: self
.format_last_option()
.expect("Should only have pending value after long option"),
value,
});
}
State::Shorts(ref arg, ref mut pos) => {
match first_codepoint(&arg[*pos..]) {
Ok(None) => {
self.state = State::None;
}
Ok(Some('=')) if *pos > 1 => {
return Err(Error::UnexpectedValue {
option: self.format_last_option().unwrap(),
value: self.optional_value().unwrap(),
});
}
Ok(Some(ch)) => {
*pos += ch.len_utf8();
self.last_option = LastOption::Short(ch);
return Ok(Some(Arg::Short(ch)));
}
Err(_) => {
*pos += 1;
self.last_option = LastOption::Short('�');
return Ok(Some(Arg::Short('�')));
}
}
}
#[cfg(windows)]
State::ShortsU16(ref arg, ref mut pos) => match first_utf16_codepoint(&arg[*pos..]) {
Ok(None) => {
self.state = State::None;
}
Ok(Some('=')) if *pos > 1 => {
return Err(Error::UnexpectedValue {
option: self.format_last_option().unwrap(),
value: self.optional_value().unwrap(),
});
}
Ok(Some(ch)) => {
*pos += ch.len_utf16();
self.last_option = LastOption::Short(ch);
return Ok(Some(Arg::Short(ch)));
}
Err(_) => {
*pos += 1;
self.last_option = LastOption::Short('�');
return Ok(Some(Arg::Short('�')));
}
},
State::FinishedOpts => {
return Ok(self.source.next().map(Arg::Value));
}
State::None => (),
}
match self.state {
State::None => (),
ref state => panic!("unexpected state {:?}", state),
}
let arg = match self.source.next() {
Some(arg) => arg,
None => return Ok(None),
};
if arg == "--" {
self.state = State::FinishedOpts;
return self.next();
}
#[cfg(any(unix, target_os = "wasi"))]
{
let mut arg = arg.into_vec();
if arg.starts_with(b"--") {
if let Some(ind) = arg.iter().position(|&b| b == b'=') {
self.state = State::PendingValue(OsString::from_vec(arg[ind + 1..].into()));
arg.truncate(ind);
}
let option = match String::from_utf8(arg) {
Ok(text) => text,
Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
};
Ok(Some(self.set_long(option)))
} else if arg.len() > 1 && arg[0] == b'-' {
self.state = State::Shorts(arg, 1);
self.next()
} else {
Ok(Some(Arg::Value(OsString::from_vec(arg))))
}
}
#[cfg(not(any(unix, target_os = "wasi")))]
{
#[cfg(windows)]
{
let mut bytes = arg.encode_wide();
const DASH: u16 = b'-' as u16;
match (bytes.next(), bytes.next()) {
(Some(DASH), Some(_)) => {
}
_ => {
return Ok(Some(Arg::Value(arg)));
}
}
}
let mut arg = match arg.into_string() {
Ok(arg) => arg,
Err(arg) => {
#[cfg(windows)]
{
let mut arg: Vec<u16> = arg.encode_wide().collect();
const DASH: u16 = b'-' as u16;
const EQ: u16 = b'=' as u16;
if arg.starts_with(&[DASH, DASH]) {
if let Some(ind) = arg.iter().position(|&u| u == EQ) {
self.state =
State::PendingValue(OsString::from_wide(&arg[ind + 1..]));
arg.truncate(ind);
}
let long = self.set_long(String::from_utf16_lossy(&arg));
return Ok(Some(long));
} else {
assert!(arg.len() > 1);
assert_eq!(arg[0], DASH);
self.state = State::ShortsU16(arg, 1);
return self.next();
}
};
#[cfg(not(windows))]
{
let text = arg.to_string_lossy();
if text.starts_with('-') {
text.into_owned()
} else {
return Ok(Some(Arg::Value(arg)));
}
}
}
};
if arg.starts_with("--") {
if let Some(ind) = arg.find('=') {
self.state = State::PendingValue(arg[ind + 1..].into());
arg.truncate(ind);
}
Ok(Some(self.set_long(arg)))
} else if arg.starts_with('-') && arg != "-" {
self.state = State::Shorts(arg.into(), 1);
self.next()
} else {
Ok(Some(Arg::Value(arg.into())))
}
}
}
pub fn value(&mut self) -> Result<OsString, Error> {
if let Some(value) = self.optional_value() {
return Ok(value);
}
if let Some(value) = self.source.next() {
return Ok(value);
}
Err(Error::MissingValue {
option: self.format_last_option(),
})
}
pub fn values(&mut self) -> Result<ValuesIter<'_>, Error> {
if self.has_pending() || self.next_is_normal() {
Ok(ValuesIter {
took_first: false,
parser: Some(self),
})
} else {
Err(Error::MissingValue {
option: self.format_last_option(),
})
}
}
fn next_if_normal(&mut self) -> Option<OsString> {
if self.next_is_normal() {
self.source.next()
} else {
None
}
}
fn next_is_normal(&self) -> bool {
assert!(!self.has_pending());
let arg = match self.source.as_slice().first() {
None => return false,
Some(arg) => arg,
};
if let State::FinishedOpts = self.state {
return true;
}
if arg == "-" {
return true;
}
#[cfg(any(unix, target_os = "wasi"))]
let lead_dash = arg.as_bytes().first() == Some(&b'-');
#[cfg(windows)]
let lead_dash = arg.encode_wide().next() == Some(b'-' as u16);
#[cfg(not(any(unix, target_os = "wasi", windows)))]
let lead_dash = arg.to_string_lossy().as_bytes().first() == Some(&b'-');
!lead_dash
}
pub fn raw_args(&mut self) -> Result<RawArgs<'_>, Error> {
if let Some(value) = self.optional_value() {
return Err(Error::UnexpectedValue {
option: self.format_last_option().unwrap(),
value,
});
}
Ok(RawArgs(&mut self.source))
}
pub fn try_raw_args(&mut self) -> Option<RawArgs<'_>> {
if self.has_pending() {
None
} else {
Some(RawArgs(&mut self.source))
}
}
fn has_pending(&self) -> bool {
match self.state {
State::None | State::FinishedOpts => false,
State::PendingValue(_) => true,
State::Shorts(ref arg, pos) => pos < arg.len(),
#[cfg(windows)]
State::ShortsU16(ref arg, pos) => pos < arg.len(),
}
}
#[inline(never)]
fn format_last_option(&self) -> Option<String> {
match self.last_option {
LastOption::None => None,
LastOption::Short(ch) => Some(format!("-{}", ch)),
LastOption::Long(ref option) => Some(option.clone()),
}
}
pub fn bin_name(&self) -> Option<&str> {
Some(self.bin_name.as_ref()?)
}
pub fn optional_value(&mut self) -> Option<OsString> {
Some(self.raw_optional_value()?.0)
}
fn raw_optional_value(&mut self) -> Option<(OsString, bool)> {
match replace(&mut self.state, State::None) {
State::PendingValue(value) => Some((value, true)),
State::Shorts(mut arg, mut pos) => {
if pos >= arg.len() {
return None;
}
let mut had_eq_sign = false;
if arg[pos] == b'=' {
pos += 1;
had_eq_sign = true;
}
arg.drain(..pos); #[cfg(any(unix, target_os = "wasi"))]
{
Some((OsString::from_vec(arg), had_eq_sign))
}
#[cfg(not(any(unix, target_os = "wasi")))]
{
let arg = String::from_utf8(arg)
.expect("short option args on exotic platforms must be unicode");
Some((arg.into(), had_eq_sign))
}
}
#[cfg(windows)]
State::ShortsU16(arg, mut pos) => {
if pos >= arg.len() {
return None;
}
let mut had_eq_sign = false;
if arg[pos] == b'=' as u16 {
pos += 1;
had_eq_sign = true;
}
Some((OsString::from_wide(&arg[pos..]), had_eq_sign))
}
State::FinishedOpts => {
self.state = State::FinishedOpts;
None
}
State::None => None,
}
}
fn new(bin_name: Option<OsString>, source: InnerIter) -> Parser {
Parser {
source,
state: State::None,
last_option: LastOption::None,
bin_name: bin_name.map(|s| match s.into_string() {
Ok(text) => text,
Err(text) => text.to_string_lossy().into_owned(),
}),
}
}
pub fn from_env() -> Parser {
let mut source = make_iter(std::env::args_os());
Parser::new(source.next(), source)
}
pub fn from_iter<I>(args: I) -> Parser
where
I: IntoIterator,
I::Item: Into<OsString>,
{
let mut args = make_iter(args.into_iter().map(Into::into));
Parser::new(args.next(), args)
}
pub fn from_args<I>(args: I) -> Parser
where
I: IntoIterator,
I::Item: Into<OsString>,
{
Parser::new(None, make_iter(args.into_iter().map(Into::into)))
}
fn set_long(&mut self, option: String) -> Arg<'_> {
self.last_option = LastOption::Long(option);
match self.last_option {
LastOption::Long(ref option) => Arg::Long(&option[2..]),
_ => unreachable!(),
}
}
}
impl Arg<'_> {
pub fn unexpected(self) -> Error {
match self {
Arg::Short(short) => Error::UnexpectedOption(format!("-{}", short)),
Arg::Long(long) => Error::UnexpectedOption(format!("--{}", long)),
Arg::Value(value) => Error::UnexpectedArgument(value),
}
}
}
#[derive(Debug)]
pub struct ValuesIter<'a> {
took_first: bool,
parser: Option<&'a mut Parser>,
}
impl Iterator for ValuesIter<'_> {
type Item = OsString;
fn next(&mut self) -> Option<Self::Item> {
let parser = self.parser.as_mut()?;
if self.took_first {
parser.next_if_normal()
} else if let Some((value, had_eq_sign)) = parser.raw_optional_value() {
if had_eq_sign {
self.parser = None;
}
self.took_first = true;
Some(value)
} else {
let value = parser
.next_if_normal()
.expect("ValuesIter must yield at least one value");
self.took_first = true;
Some(value)
}
}
}
#[derive(Debug)]
pub struct RawArgs<'a>(&'a mut InnerIter);
impl Iterator for RawArgs<'_> {
type Item = OsString;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl RawArgs<'_> {
pub fn peek(&self) -> Option<&OsStr> {
Some(self.0.as_slice().first()?.as_os_str())
}
pub fn next_if(&mut self, func: impl FnOnce(&OsStr) -> bool) -> Option<OsString> {
match self.peek() {
Some(arg) if func(arg) => self.next(),
_ => None,
}
}
pub fn as_slice(&self) -> &[OsString] {
self.0.as_slice()
}
}
pub enum Error {
MissingValue {
option: Option<String>,
},
UnexpectedOption(String),
UnexpectedArgument(OsString),
UnexpectedValue {
option: String,
value: OsString,
},
ParsingFailed {
value: String,
error: Box<dyn std::error::Error + Send + Sync + 'static>,
},
NonUnicodeValue(OsString),
Custom(Box<dyn std::error::Error + Send + Sync + 'static>),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use crate::Error::*;
match self {
MissingValue { option: None } => write!(f, "missing argument"),
MissingValue {
option: Some(option),
} => {
write!(f, "missing argument for option '{}'", option)
}
UnexpectedOption(option) => write!(f, "invalid option '{}'", option),
UnexpectedArgument(value) => write!(f, "unexpected argument {:?}", value),
UnexpectedValue { option, value } => {
write!(
f,
"unexpected argument for option '{}': {:?}",
option, value
)
}
NonUnicodeValue(value) => write!(f, "argument is invalid unicode: {:?}", value),
ParsingFailed { value, error } => {
write!(f, "cannot parse argument {:?}: {}", value, error)
}
Custom(err) => write!(f, "{}", err),
}
}
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::ParsingFailed { error, .. } | Error::Custom(error) => Some(error.as_ref()),
_ => None,
}
}
}
impl From<String> for Error {
fn from(msg: String) -> Self {
Error::Custom(msg.into())
}
}
impl<'a> From<&'a str> for Error {
fn from(msg: &'a str) -> Self {
Error::Custom(msg.into())
}
}
impl From<OsString> for Error {
fn from(arg: OsString) -> Self {
Error::NonUnicodeValue(arg)
}
}
mod private {
pub trait Sealed {}
impl Sealed for std::ffi::OsString {}
}
pub trait ValueExt: private::Sealed {
fn parse<T: FromStr>(&self) -> Result<T, Error>
where
T::Err: Into<Box<dyn std::error::Error + Send + Sync + 'static>>;
fn parse_with<F, T, E>(&self, func: F) -> Result<T, Error>
where
F: FnOnce(&str) -> Result<T, E>,
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>;
fn string(self) -> Result<String, Error>;
}
impl ValueExt for OsString {
fn parse<T: FromStr>(&self) -> Result<T, Error>
where
T::Err: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
self.parse_with(FromStr::from_str)
}
fn parse_with<F, T, E>(&self, func: F) -> Result<T, Error>
where
F: FnOnce(&str) -> Result<T, E>,
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
match self.to_str() {
Some(text) => match func(text) {
Ok(value) => Ok(value),
Err(err) => Err(Error::ParsingFailed {
value: text.to_owned(),
error: err.into(),
}),
},
None => Err(Error::NonUnicodeValue(self.into())),
}
}
fn string(self) -> Result<String, Error> {
match self.into_string() {
Ok(string) => Ok(string),
Err(raw) => Err(Error::NonUnicodeValue(raw)),
}
}
}
pub mod prelude {
pub use super::Arg::*;
pub use super::ValueExt;
}
fn first_codepoint(bytes: &[u8]) -> Result<Option<char>, u8> {
let bytes = bytes.get(..4).unwrap_or(bytes);
let text = match std::str::from_utf8(bytes) {
Ok(text) => text,
Err(err) if err.valid_up_to() > 0 => {
std::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap()
}
Err(_) => return Err(bytes[0]),
};
Ok(text.chars().next())
}
#[cfg(windows)]
fn first_utf16_codepoint(units: &[u16]) -> Result<Option<char>, u16> {
match std::char::decode_utf16(units.iter().cloned()).next() {
Some(Ok(ch)) => Ok(Some(ch)),
Some(Err(_)) => Err(units[0]),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::prelude::*;
use super::*;
fn parse(args: &'static str) -> Parser {
Parser::from_args(args.split_whitespace().map(bad_string))
}
macro_rules! assert_matches {
($expression: expr, $( $pattern: pat )|+) => {
match $expression {
$( $pattern )|+ => (),
_ => panic!(
"{:?} does not match {:?}",
stringify!($expression),
stringify!($( $pattern )|+)
),
}
};
}
#[test]
fn test_basic() -> Result<(), Error> {
let mut p = parse("-n 10 foo - -- baz -qux");
assert_eq!(p.next()?.unwrap(), Short('n'));
assert_eq!(p.value()?.parse::<i32>()?, 10);
assert_eq!(p.next()?.unwrap(), Value("foo".into()));
assert_eq!(p.next()?.unwrap(), Value("-".into()));
assert_eq!(p.next()?.unwrap(), Value("baz".into()));
assert_eq!(p.next()?.unwrap(), Value("-qux".into()));
assert_eq!(p.next()?, None);
assert_eq!(p.next()?, None);
assert_eq!(p.next()?, None);
Ok(())
}
#[test]
fn test_combined() -> Result<(), Error> {
let mut p = parse("-abc -fvalue -xfvalue");
assert_eq!(p.next()?.unwrap(), Short('a'));
assert_eq!(p.next()?.unwrap(), Short('b'));
assert_eq!(p.next()?.unwrap(), Short('c'));
assert_eq!(p.next()?.unwrap(), Short('f'));
assert_eq!(p.value()?, "value");
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.next()?.unwrap(), Short('f'));
assert_eq!(p.value()?, "value");
assert_eq!(p.next()?, None);
Ok(())
}
#[test]
fn test_long() -> Result<(), Error> {
let mut p = parse("--foo --bar=qux --foobar=qux=baz");
assert_eq!(p.next()?.unwrap(), Long("foo"));
assert_eq!(p.next()?.unwrap(), Long("bar"));
assert_eq!(p.value()?, "qux");
assert_eq!(p.next()?.unwrap(), Long("foobar"));
match p.next().unwrap_err() {
Error::UnexpectedValue { option, value } => {
assert_eq!(option, "--foobar");
assert_eq!(value, "qux=baz");
}
_ => panic!(),
}
assert_eq!(p.next()?, None);
Ok(())
}
#[test]
fn test_dash_args() -> Result<(), Error> {
let mut p = parse("-x -- -y");
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.next()?.unwrap(), Value("-y".into()));
assert_eq!(p.next()?, None);
let mut p = parse("-x -- -y");
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.value()?, "--");
assert_eq!(p.next()?.unwrap(), Short('y'));
assert_eq!(p.next()?, None);
let mut p = parse("-x - -y");
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.next()?.unwrap(), Value("-".into()));
assert_eq!(p.next()?.unwrap(), Short('y'));
assert_eq!(p.next()?, None);
let mut p = parse("-x-y");
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.next()?.unwrap(), Short('-'));
assert_eq!(p.next()?.unwrap(), Short('y'));
assert_eq!(p.next()?, None);
Ok(())
}
#[test]
fn test_missing_value() -> Result<(), Error> {
let mut p = parse("-o");
assert_eq!(p.next()?.unwrap(), Short('o'));
match p.value() {
Err(Error::MissingValue {
option: Some(option),
}) => assert_eq!(option, "-o"),
_ => panic!(),
}
let mut q = parse("--out");
assert_eq!(q.next()?.unwrap(), Long("out"));
match q.value() {
Err(Error::MissingValue {
option: Some(option),
}) => assert_eq!(option, "--out"),
_ => panic!(),
}
let mut r = parse("");
assert_matches!(r.value(), Err(Error::MissingValue { option: None }));
Ok(())
}
#[test]
fn test_weird_args() -> Result<(), Error> {
let mut p = Parser::from_args(&[
"", "--=", "--=3", "-", "-x", "--", "-", "-x", "--", "", "-", "-x",
]);
assert_eq!(p.next()?.unwrap(), Value(OsString::from("")));
assert_eq!(p.next()?.unwrap(), Long(""));
assert_eq!(p.value()?, OsString::from(""));
assert_eq!(p.next()?.unwrap(), Long(""));
assert_eq!(p.value()?, OsString::from("3"));
assert_eq!(p.next()?.unwrap(), Value(OsString::from("-")));
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.value()?, OsString::from("--"));
assert_eq!(p.next()?.unwrap(), Value(OsString::from("-")));
assert_eq!(p.next()?.unwrap(), Short('x'));
assert_eq!(p.next()?.unwrap(), Value(OsString::from("")));
assert_eq!(p.next()?.unwrap(), Value(OsString::from("-")));
assert_eq!(p.next()?.unwrap(), Value(OsString::from("-x")));
assert_eq!(p.next()?, None);
#[cfg(any(unix, target_os = "wasi", windows))]
{
let mut q = parse("--=@");
assert_eq!(q.next()?.unwrap(), Long(""));
assert_eq!(q.value()?, bad_string("@"));
assert_eq!(q.next()?, None);
}
let mut r = parse("");
assert_eq!(r.next()?, None);
Ok(())
}
#[test]
fn test_unicode() -> Result<(), Error> {
let mut p = parse("-aµ --µ=10 µ --foo=µ");
assert_eq!(p.next()?.unwrap(), Short('a'));
assert_eq!(p.next()?.unwrap(), Short('µ'));
assert_eq!(p.next()?.unwrap(), Long("µ"));
assert_eq!(p.value()?, "10");
assert_eq!(p.next()?.unwrap(), Value("µ".into()));
assert_eq!(p.next()?.unwrap(), Long("foo"));
assert_eq!(p.value()?, "µ");
Ok(())
}
#[cfg(any(unix, target_os = "wasi", windows))]
#[test]
fn test_mixed_invalid() -> Result<(), Error> {
let mut p = parse("--foo=@@@");
assert_eq!(p.next()?.unwrap(), Long("foo"));
assert_eq!(p.value()?, bad_string("@@@"));
let mut q = parse("-💣@@@");
assert_eq!(q.next()?.unwrap(), Short('💣'));
assert_eq!(q.value()?, bad_string("@@@"));
let mut r = parse("-f@@@");
assert_eq!(r.next()?.unwrap(), Short('f'));
assert_eq!(r.next()?.unwrap(), Short('�'));
assert_eq!(r.next()?.unwrap(), Short('�'));
assert_eq!(r.next()?.unwrap(), Short('�'));
assert_eq!(r.next()?, None);
let mut s = parse("--foo=bar=@@@");
assert_eq!(s.next()?.unwrap(), Long("foo"));
assert_eq!(s.value()?, bad_string("bar=@@@"));
Ok(())
}
#[cfg(any(unix, target_os = "wasi", windows))]
#[test]
fn test_separate_invalid() -> Result<(), Error> {
let mut p = parse("--foo @@@");
assert_eq!(p.next()?.unwrap(), Long("foo"));
assert_eq!(p.value()?, bad_string("@@@"));
Ok(())
}
#[cfg(any(unix, target_os = "wasi", windows))]
#[test]
fn test_invalid_long_option() -> Result<(), Error> {
let mut p = parse("--@=10");
assert_eq!(p.next()?.unwrap(), Long("�"));
assert_eq!(p.value().unwrap(), OsString::from("10"));
assert_eq!(p.next()?, None);
let mut q = parse("--@");
assert_eq!(q.next()?.unwrap(), Long("�"));
assert_eq!(q.next()?, None);
Ok(())
}
#[test]
fn short_opt_equals_sign() -> Result<(), Error> {
let mut p = parse("-a=b");
assert_eq!(p.next()?.unwrap(), Short('a'));
assert_eq!(p.value()?, OsString::from("b"));
assert_eq!(p.next()?, None);
let mut p = parse("-a=b");
assert_eq!(p.next()?.unwrap(), Short('a'));
match p.next().unwrap_err() {
Error::UnexpectedValue { option, value } => {
assert_eq!(option, "-a");
assert_eq!(value, "b");
}
_ => panic!(),
}
assert_eq!(p.next()?, None);
let mut p = parse("-a=");
assert_eq!(p.next()?.unwrap(), Short('a'));
assert_eq!(p.value()?, OsString::from(""));
assert_eq!(p.next()?, None);
let mut p = parse("-a=");
assert_eq!(p.next()?.unwrap(), Short('a'));
match p.next().unwrap_err() {
Error::UnexpectedValue { option, value } => {
assert_eq!(option, "-a");
assert_eq!(value, "");
}
_ => panic!(),
}
assert_eq!(p.next()?, None);
let mut p = parse("-=");
assert_eq!(p.next()?.unwrap(), Short('='));
assert_eq!(p.next()?, None);
let mut p = parse("-=a");
assert_eq!(p.next()?.unwrap(), Short('='));
assert_eq!(p.value()?, "a");
Ok(())
}
#[cfg(any(unix, target_os = "wasi", windows))]
#[test]
fn short_opt_equals_sign_invalid() -> Result<(), Error> {
let mut p = parse("-a=@");
assert_eq!(p.next()?.unwrap(), Short('a'));
assert_eq!(p.value()?, bad_string("@"));
assert_eq!(p.next()?, None);
let mut p = parse("-a=@");
assert_eq!(p.next()?.unwrap(), Short('a'));
match p.next().unwrap_err() {
Error::UnexpectedValue { option, value } => {
assert_eq!(option, "-a");
assert_eq!(value, bad_string("@"));
}
_ => panic!(),
}
assert_eq!(p.next()?, None);
let mut p = parse("-=@");
assert_eq!(p.next()?.unwrap(), Short('='));
assert_eq!(p.value()?, bad_string("@"));
Ok(())
}
#[test]
fn multi_values() -> Result<(), Error> {
for &case in &["-a b c d", "-ab c d", "-a b c d --", "--a b c d"] {
let mut p = parse(case);
p.next()?.unwrap();
let mut iter = p.values()?;
let values: Vec<_> = iter.by_ref().collect();
assert_eq!(values, &["b", "c", "d"]);
assert!(iter.next().is_none());
assert!(p.next()?.is_none());
}
for &case in &["-a=b c", "--a=b c"] {
let mut p = parse(case);
p.next()?.unwrap();
let mut iter = p.values()?;
let values: Vec<_> = iter.by_ref().collect();
assert_eq!(values, &["b"]);
assert!(iter.next().is_none());
assert_eq!(p.next()?.unwrap(), Value("c".into()));
assert!(p.next()?.is_none());
}
for &case in &["-a", "--a", "-a -b", "-a -- b", "-a --"] {
let mut p = parse(case);
p.next()?.unwrap();
assert!(p.values().is_err());
assert!(p.next().is_ok());
assert!(p.next().unwrap().is_none());
}
for &case in &["-a=", "--a="] {
let mut p = parse(case);
p.next()?.unwrap();
let mut iter = p.values()?;
let values: Vec<_> = iter.by_ref().collect();
assert_eq!(values, &[""]);
assert!(iter.next().is_none());
assert!(p.next()?.is_none());
}
for &case in &["-a=b", "--a=b", "-a b"] {
let mut p = parse(case);
p.next()?.unwrap();
assert!(p.values().is_ok());
assert_eq!(p.value()?, "b");
}
{
let mut p = parse("-ab");
p.next()?.unwrap();
assert!(p.values().is_ok());
assert_eq!(p.next()?.unwrap(), Short('b'));
}
Ok(())
}
#[test]
fn raw_args() -> Result<(), Error> {
let mut p = parse("-a b c d");
assert!(p.try_raw_args().is_some());
assert_eq!(p.raw_args()?.collect::<Vec<_>>(), &["-a", "b", "c", "d"]);
assert!(p.try_raw_args().is_some());
assert!(p.next()?.is_none());
assert!(p.try_raw_args().is_some());
assert_eq!(p.raw_args()?.as_slice().len(), 0);
let mut p = parse("-ab c d");
p.next()?;
assert!(p.try_raw_args().is_none());
assert!(p.raw_args().is_err());
assert_eq!(p.try_raw_args().unwrap().collect::<Vec<_>>(), &["c", "d"]);
assert!(p.next()?.is_none());
assert_eq!(p.try_raw_args().unwrap().as_slice().len(), 0);
let mut p = parse("-a b c d");
assert_eq!(p.raw_args()?.take(3).collect::<Vec<_>>(), &["-a", "b", "c"]);
assert_eq!(p.next()?, Some(Value("d".into())));
assert!(p.next()?.is_none());
let mut p = parse("a");
let mut it = p.raw_args()?;
assert_eq!(it.peek(), Some("a".as_ref()));
assert_eq!(it.next_if(|_| false), None);
assert_eq!(p.next()?, Some(Value("a".into())));
assert!(p.next()?.is_none());
Ok(())
}
#[test]
fn bin_name() {
assert_eq!(
Parser::from_iter(&["foo", "bar", "baz"]).bin_name(),
Some("foo")
);
assert_eq!(Parser::from_args(&["foo", "bar", "baz"]).bin_name(), None);
assert_eq!(Parser::from_iter(&[] as &[&str]).bin_name(), None);
assert_eq!(Parser::from_iter(&[""]).bin_name(), Some(""));
assert!(Parser::from_env().bin_name().is_some());
#[cfg(any(unix, target_os = "wasi", windows))]
assert_eq!(
Parser::from_iter(vec![bad_string("foo@bar")]).bin_name(),
Some("foo�bar")
);
}
#[test]
fn test_value_ext() -> Result<(), Error> {
let s = OsString::from("-10");
assert_eq!(s.parse::<i32>()?, -10);
assert_eq!(
s.parse_with(|s| match s {
"-10" => Ok(0),
_ => Err("bad"),
})?,
0,
);
match s.parse::<u32>() {
Err(Error::ParsingFailed { value, .. }) => assert_eq!(value, "-10"),
_ => panic!(),
}
match s.parse_with(|s| match s {
"11" => Ok(0_i32),
_ => Err("bad"),
}) {
Err(Error::ParsingFailed { value, .. }) => assert_eq!(value, "-10"),
_ => panic!(),
}
assert_eq!(s.string()?, "-10");
Ok(())
}
#[cfg(any(unix, target_os = "wasi", windows))]
#[test]
fn test_value_ext_invalid() -> Result<(), Error> {
let s = bad_string("foo@");
assert_matches!(s.parse::<i32>(), Err(Error::NonUnicodeValue(_)));
assert_matches!(
s.parse_with(<f32 as FromStr>::from_str),
Err(Error::NonUnicodeValue(_))
);
assert_matches!(s.string(), Err(Error::NonUnicodeValue(_)));
Ok(())
}
#[test]
fn test_first_codepoint() {
assert_eq!(first_codepoint(b"foo").unwrap(), Some('f'));
assert_eq!(first_codepoint(b"").unwrap(), None);
assert_eq!(first_codepoint(b"f\xFF\xFF").unwrap(), Some('f'));
assert_eq!(first_codepoint(b"\xC2\xB5bar").unwrap(), Some('µ'));
first_codepoint(b"\xFF").unwrap_err();
assert_eq!(first_codepoint(b"foo\xC2\xB5").unwrap(), Some('f'));
}
fn bad_string(text: &str) -> OsString {
#[cfg(any(unix, target_os = "wasi"))]
{
let mut text = text.as_bytes().to_vec();
for ch in &mut text {
if *ch == b'@' {
*ch = b'\xFF';
}
}
OsString::from_vec(text)
}
#[cfg(windows)]
{
let mut out = Vec::new();
for ch in text.chars() {
if ch == '@' {
out.push(0xD800);
} else {
let mut buf = [0; 2];
out.extend(&*ch.encode_utf16(&mut buf));
}
}
OsString::from_wide(&out)
}
#[cfg(not(any(unix, target_os = "wasi", windows)))]
{
if text.contains('@') {
unimplemented!("Don't know how to create invalid OsStrings on this platform");
}
text.into()
}
}
#[test]
fn basic_fuzz() {
#[cfg(any(windows, unix, target_os = "wasi"))]
const VOCABULARY: &[&str] = &[
"", "-", "--", "---", "a", "-a", "-aa", "@", "-@", "-a@", "-@a", "--a", "--@", "--a=a",
"--a=", "--a=@", "--@=a", "--=", "--=@", "--=a", "-@@", "-a=a", "-a=", "-=", "-a-",
];
#[cfg(not(any(windows, unix, target_os = "wasi")))]
const VOCABULARY: &[&str] = &[
"", "-", "--", "---", "a", "-a", "-aa", "--a", "--a=a", "--a=", "--=", "--=a", "-a=a",
"-a=", "-=", "-a-",
];
exhaust(Parser::new(None, Vec::new().into_iter()), 0);
let vocabulary: Vec<OsString> = VOCABULARY.iter().map(|&s| bad_string(s)).collect();
let mut permutations = vec![vec![]];
for _ in 0..3 {
let mut new = Vec::new();
for old in permutations {
for word in &vocabulary {
let mut extended = old.clone();
extended.push(word);
new.push(extended);
}
}
permutations = new;
for permutation in &permutations {
println!("{:?}", permutation);
let p = Parser::from_args(permutation);
exhaust(p, 0);
}
}
}
fn exhaust(mut parser: Parser, depth: u16) {
if depth > 100 {
panic!("Stuck in loop");
}
if parser.has_pending() {
{
let mut parser = parser.clone();
assert!(parser.try_raw_args().is_none());
assert!(parser.try_raw_args().is_none());
assert!(parser.raw_args().is_err());
assert!(parser.raw_args().is_ok());
assert!(parser.try_raw_args().is_some());
}
{
let mut parser = parser.clone();
assert!(parser.optional_value().is_some());
exhaust(parser, depth + 1);
}
} else {
let prev_state = parser.state.clone();
let prev_remaining = parser.source.as_slice().len();
assert!(parser.optional_value().is_none());
assert!(parser.raw_args().is_ok());
assert!(parser.try_raw_args().is_some());
match prev_state {
State::None | State::PendingValue(_) => {
assert_matches!(parser.state, State::None);
}
State::Shorts(arg, pos) => {
assert_eq!(pos, arg.len());
assert_matches!(parser.state, State::None);
}
#[cfg(windows)]
State::ShortsU16(arg, pos) => {
assert_eq!(pos, arg.len());
assert_matches!(parser.state, State::None);
}
State::FinishedOpts => assert_matches!(parser.state, State::FinishedOpts),
}
assert_eq!(parser.source.as_slice().len(), prev_remaining);
}
{
let mut parser = parser.clone();
match parser.next() {
Ok(None) => {
assert_matches!(parser.state, State::None | State::FinishedOpts);
assert_eq!(parser.source.as_slice().len(), 0);
}
_ => exhaust(parser, depth + 1),
}
}
{
let mut parser = parser.clone();
match parser.value() {
Err(_) => {
assert_matches!(parser.state, State::None | State::FinishedOpts);
assert_eq!(parser.source.as_slice().len(), 0);
}
Ok(_) => {
assert_matches!(parser.state, State::None | State::FinishedOpts);
exhaust(parser, depth + 1);
}
}
}
{
match parser.values() {
Err(_) => (),
Ok(iter) => {
assert!(iter.count() > 0);
exhaust(parser, depth + 1);
}
}
}
}
}