#![no_std]
extern crate alloc;
use alloc::borrow::{Cow, ToOwned};
use alloc::string::{String, ToString};
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "std")]
use std::ffi::{OsStr, OsString};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Opt {
val: char,
erropt: Option<char>,
arg: Option<Cow<'static, str>>,
}
impl Opt {
#[must_use]
pub fn val(&self) -> char {
self.val
}
#[must_use]
pub fn erropt(&self) -> Option<char> {
self.erropt
}
#[must_use]
pub fn arg(&self) -> Option<&str> {
self.arg.as_deref()
}
#[must_use]
pub fn into_arg(self) -> Option<Cow<'static, str>> {
self.arg
}
}
impl PartialEq<char> for Opt {
fn eq(&self, other: &char) -> bool {
self.val == *other
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for &str {}
impl Sealed for &&str {}
impl Sealed for alloc::string::String {}
impl Sealed for &core::ffi::CStr {}
#[cfg(feature = "std")]
impl Sealed for std::ffi::OsString {}
#[cfg(feature = "std")]
impl Sealed for &std::ffi::OsStr {}
}
pub trait ArgV: sealed::Sealed {
fn into_argv(self) -> Cow<'static, str>;
}
impl ArgV for &'static str {
fn into_argv(self) -> Cow<'static, str> {
Cow::Borrowed(self)
}
}
impl ArgV for &&'static str {
fn into_argv(self) -> Cow<'static, str> {
Cow::Borrowed(*self)
}
}
impl ArgV for String {
fn into_argv(self) -> Cow<'static, str> {
Cow::Owned(self)
}
}
#[cfg(feature = "std")]
impl ArgV for OsString {
fn into_argv(self) -> Cow<'static, str> {
match self.into_string() {
Ok(s) => Cow::Owned(s),
Err(s) => Cow::Owned(s.to_string_lossy().into_owned()),
}
}
}
#[cfg(feature = "std")]
impl ArgV for &'static OsStr {
fn into_argv(self) -> Cow<'static, str> {
self.to_string_lossy()
}
}
impl ArgV for &'static core::ffi::CStr {
fn into_argv(self) -> Cow<'static, str> {
self.to_string_lossy()
}
}
pub struct Getopt<'a, V, I: Iterator<Item = V>> {
iter: I,
current_arg: Option<Cow<'static, str>>,
prog_name: Cow<'static, str>,
sp: usize,
#[cfg_attr(not(feature = "std"), allow(dead_code))]
opterr: bool,
optstring: &'a [u8],
}
macro_rules! err {
($self:ident, $fmt:literal $(, $arg:expr)*) => {
{
#[cfg(feature = "std")]
if $self.opterr && !$self.optstring.is_empty() && $self.optstring[0] != b':' {
std::eprintln!($fmt, $self.prog_name() $(, $arg)*);
}
}
};
}
impl<'a, V: ArgV, I: Iterator<Item = V>> Getopt<'a, V, I> {
pub fn new<A: IntoIterator<Item = V, IntoIter = I>>(args: A, optstring: &'a str) -> Self {
let mut iter = args.into_iter();
let prog_name = iter.next().map(ArgV::into_argv).unwrap_or_default();
Getopt {
iter,
current_arg: None,
prog_name,
sp: 1,
opterr: true,
optstring: optstring.as_bytes(),
}
}
pub fn set_opterr(&mut self, opterr: bool) {
self.opterr = opterr;
}
fn next_arg(&mut self) -> Option<V> {
self.iter.next()
}
pub fn remaining(self) -> I {
self.iter
}
pub fn prog_name(&self) -> &str {
#[cfg(feature = "std")]
const PATH_SEPARATOR: char = std::path::MAIN_SEPARATOR;
#[cfg(all(not(feature = "std"), windows))]
const PATH_SEPARATOR: char = '\\';
#[cfg(all(not(feature = "std"), not(windows)))]
const PATH_SEPARATOR: char = '/';
let s = &self.prog_name;
match s.rfind(PATH_SEPARATOR) {
Some(idx) => &s[(idx + 1)..],
None => s,
}
}
fn parse_short(&self, c: char) -> Option<usize> {
if !c.is_ascii() || c == ':' || c == '(' || c == ')' {
return None;
}
let mut i = 0;
while i < self.optstring.len() {
if self.optstring[i] == c as u8 {
return Some(i);
}
while i < self.optstring.len() && self.optstring[i] == b'(' {
while i < self.optstring.len() && self.optstring[i] != b')' {
i += 1;
}
}
i += 1;
}
None
}
fn parse_long(&self, opt: &'a str) -> Option<(usize, Option<&'a str>)> {
if self.optstring.is_empty() {
return None;
}
let opt = opt.as_bytes();
let mut cp_idx = 0usize;
let mut ip_idx = 0usize;
let mut op_idx: usize;
let mut is_match: bool;
loop {
if self.optstring[ip_idx] != b'(' {
ip_idx += 1;
if ip_idx == self.optstring.len() {
break;
}
}
if self.optstring[ip_idx] == b':' {
ip_idx += 1;
if ip_idx == self.optstring.len() {
break;
}
}
while self.optstring[ip_idx] == b'(' {
ip_idx += 1;
if ip_idx == self.optstring.len() {
break;
}
is_match = true;
op_idx = 0;
while ip_idx < self.optstring.len()
&& op_idx < opt.len()
&& self.optstring[ip_idx] != b')'
{
is_match = self.optstring[ip_idx] == opt[op_idx] && is_match;
ip_idx += 1;
op_idx += 1;
}
if ip_idx >= self.optstring.len() {
break;
}
if is_match
&& self.optstring[ip_idx] == b')'
&& (op_idx == opt.len() || opt[op_idx] == b'=')
{
let longoptarg = if op_idx != opt.len() && opt[op_idx] == b'=' {
Some(unsafe { core::str::from_utf8_unchecked(&opt[op_idx + 1..]) })
} else {
None
};
return Some((cp_idx, longoptarg));
}
if self.optstring[ip_idx] == b')' {
ip_idx += 1;
if ip_idx == self.optstring.len() {
break;
}
}
}
cp_idx = ip_idx;
while cp_idx < self.optstring.len() && cp_idx > 0 && self.optstring[cp_idx] == b':' {
cp_idx -= 1;
}
if cp_idx == self.optstring.len() {
break;
}
}
None
}
#[allow(clippy::too_many_lines)]
fn parse_next(&mut self) -> Option<Opt> {
if self.sp == 1 {
if let Some(arg) = self.next_arg() {
self.current_arg = Some(arg.into_argv());
} else {
return None;
}
}
let current_arg = match &self.current_arg {
Some(arg) => arg,
None => return None,
};
if self.sp == 1 {
if !current_arg.starts_with('-') || current_arg == "-" {
return None;
}
if current_arg == "--" {
self.current_arg = None;
return None;
}
}
let mut optopt = current_arg.as_bytes()[self.sp] as char;
let is_longopt = self.sp == 1 && optopt == '-';
let cp_result = if is_longopt {
self.parse_long(¤t_arg[2..])
} else {
self.parse_short(optopt).map(|idx| (idx, None))
};
let (cp, longoptarg) = if let Some(result) = cp_result {
result
} else {
#[cfg_attr(not(feature = "std"), allow(unused_variables))]
let opt_display = if is_longopt {
current_arg[2..].to_string()
} else {
optopt.to_string()
};
err!(self, "{}: illegal option -- {}", opt_display);
if current_arg.len() > self.sp + 1 && !is_longopt {
self.sp += 1;
} else {
self.current_arg = None;
self.sp = 1;
}
return Some(Opt {
val: '?',
erropt: Some(optopt),
arg: None,
});
};
optopt = self.optstring[cp] as char;
let takes_arg = self.optstring.get(cp + 1).map_or(false, |&b| b == b':');
let optarg: Option<Cow<'static, str>>;
if takes_arg {
if !is_longopt && current_arg.len() > self.sp + 1 {
optarg = Some(Cow::Owned(current_arg[self.sp + 1..].to_owned()));
self.current_arg = None;
self.sp = 1;
} else if is_longopt && longoptarg.is_some() {
optarg = longoptarg.map(|s| Cow::Owned(s.to_owned()));
self.current_arg = None;
self.sp = 1;
} else if let Some(next_arg) = self.next_arg() {
optarg = Some(next_arg.into_argv());
self.current_arg = None;
self.sp = 1;
} else {
err!(self, "{}: option requires an argument -- {}", optopt);
self.sp = 1;
self.current_arg = None;
return if !self.optstring.is_empty() && self.optstring[0] == (b':') {
Some(Opt {
val: ':',
erropt: Some(optopt),
arg: None,
})
} else {
Some(Opt {
val: '?',
erropt: Some(optopt),
arg: None,
})
};
}
} else {
if is_longopt && longoptarg.is_some() {
err!(
self,
"{}: option doesn't take an argument -- {}",
¤t_arg[2..]
);
self.current_arg = None;
self.sp = 1;
return Some(Opt {
val: '?',
erropt: Some(optopt),
arg: None,
});
}
if is_longopt || self.sp + 1 >= current_arg.len() {
self.sp = 1;
self.current_arg = None;
} else {
self.sp += 1;
}
optarg = None;
}
Some(Opt {
val: optopt,
erropt: None,
arg: optarg,
})
}
}
impl<V: ArgV, I: Iterator<Item = V>> Iterator for Getopt<'_, V, I> {
type Item = Opt;
fn next(&mut self) -> Option<Self::Item> {
self.parse_next()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_argv_conversion() {
use core::ffi::CStr;
fn convert<T: ArgV>(arg: T) -> Cow<'static, str> {
arg.into_argv()
}
let s: &str = "hello";
assert_eq!(convert(s), "hello");
let s: &str = "world";
let ss: &&str = &s;
assert_eq!(convert(ss), "world");
let s = String::from("test");
assert_eq!(convert(s), "test");
let bytes = b"hello\0";
let cstr = CStr::from_bytes_with_nul(bytes).unwrap();
assert_eq!(convert(cstr), "hello");
let bytes_with_invalid_utf8 = b"hello\xFF\xFEworld\0";
let cstr = CStr::from_bytes_with_nul(bytes_with_invalid_utf8).unwrap();
assert_eq!(convert(cstr), "hello��world");
#[cfg(feature = "std")]
{
use std::ffi::{OsStr, OsString};
let os = OsString::from("valid");
assert_eq!(convert(os), "valid");
#[cfg(unix)]
{
let os = unsafe {
OsString::from_encoded_bytes_unchecked(b"hello\xFF\xFEworld".to_vec())
};
assert_eq!(convert(os), "hello��world");
}
let os = OsString::from("test123");
assert_eq!(convert(os), "test123");
let os: &'static OsStr = OsStr::new("static_osstr");
assert_eq!(convert(os), "static_osstr");
}
}
#[test]
fn test_single_short_option() {
let args = &["prog", "-a"];
let mut getopt = Getopt::new(args, "ab");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
}
#[test]
fn test_multiple_short_options() {
let args = &["prog", "-a", "-b"];
let mut getopt = Getopt::new(args, "ab");
let r1 = getopt.next();
assert_eq!(
r1,
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
let r2 = getopt.next();
assert_eq!(
r2,
Some(Opt {
val: 'b',
erropt: None,
arg: None
})
);
}
#[test]
fn test_aggregated_short_options() {
let args = &["prog", "-abc"];
let mut getopt = Getopt::new(args, "abc");
let r1 = getopt.next();
assert_eq!(
r1,
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
let r2 = getopt.next();
assert_eq!(
r2,
Some(Opt {
val: 'b',
erropt: None,
arg: None
})
);
let r3 = getopt.next();
assert_eq!(
r3,
Some(Opt {
val: 'c',
erropt: None,
arg: None
})
);
}
#[test]
fn test_short_option_with_attached_argument() {
let args = &["prog", "-avalue"];
let mut getopt = Getopt::new(args, "a:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: Some("value".into())
})
);
}
#[test]
fn test_short_option_with_separate_argument() {
let args = &["prog", "-a", "value"];
let mut getopt = Getopt::new(args, "a:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: Some("value".into())
})
);
}
#[test]
fn test_long_option_simple() {
let args = &["prog", "--help"];
let mut getopt = Getopt::new(args, ":h(help)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'h',
erropt: None,
arg: None
})
);
}
#[test]
fn test_long_short_mixed() {
let args = &["prog", "-V"];
let mut getopt = Getopt::new(args, ":h(help)V(version)x:(execute)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'V',
erropt: None,
arg: None
})
);
let args = &["prog", "-x"];
let mut getopt = Getopt::new(args, ":h(help)V(version)x:(execute)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: ':',
erropt: Some('x'),
arg: None
})
);
let args = &["prog", "--execute", "cmd"];
let mut getopt = Getopt::new(args, ":h(help)V(version)x:(execute)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'x',
erropt: None,
arg: Some("cmd".into()),
})
);
}
#[test]
fn test_long_option_with_argument() {
let args = &["prog", "--output=file.txt"];
let mut getopt = Getopt::new(args, "o:(output)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn test_long_option_with_argument_double_colon() {
let args = &["prog", "--output=file.txt"];
let mut getopt = Getopt::new(args, "o::(output)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn test_multiple_option_with_argument() {
let args = &["prog", "--output=file.txt"];
let mut getopt = Getopt::new(args, "o:(outfile)(output)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("file.txt".into())
})
);
assert!(getopt.next().is_none());
let args = &["prog", "--outfile=file.txt"];
let mut getopt = Getopt::new(args, "o:(outfile)(output)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("file.txt".into())
})
);
assert!(getopt.next().is_none());
}
#[test]
fn test_long_option_without_argument() {
let args = &["prog", "--verbose=file.txt"];
let mut getopt = Getopt::new(args, ":v(verbose)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: '?',
erropt: Some('v'),
arg: None,
})
);
}
#[test]
fn test_end_of_options() {
let args = &["prog", "-a", "file.txt"];
let mut getopt = Getopt::new(args, "a");
let r1 = getopt.next();
assert_eq!(
r1,
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
let r2 = getopt.next();
assert_eq!(r2, None);
}
#[test]
fn test_double_dash_ends_options() {
let args = &["prog", "--", "-a"];
let mut getopt = Getopt::new(args, "a");
let result = getopt.next();
assert_eq!(result, None);
}
#[test]
fn test_unrecognized_option() {
let args = &["prog", "-x"];
let mut getopt = Getopt::new(args, "ab");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: '?',
erropt: Some('x'),
arg: None
})
);
}
#[test]
fn test_remaining() {
let args = &["prog", "-a", "file1.txt", "file2.txt"];
let mut getopt = Getopt::new(args, "a");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
let mut remaining = getopt.remaining();
assert_eq!(remaining.next(), Some("file1.txt").as_ref());
assert_eq!(remaining.next(), Some("file2.txt").as_ref());
assert_eq!(remaining.next(), None);
}
#[test]
fn posix_single_dash_alone_terminates_options() {
let args = &["prog", "-", "-a"];
let mut getopt = Getopt::new(args, "a");
let result = getopt.next();
assert_eq!(result, None); }
#[test]
fn posix_option_argument_attached() {
let args = &["prog", "-ofile.txt"];
let mut getopt = Getopt::new(args, "o:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn posix_option_argument_separate() {
let args = &["prog", "-o", "file.txt"];
let mut getopt = Getopt::new(args, "o:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn posix_aggregated_options() {
let args = &["prog", "-abc"];
let mut getopt = Getopt::new(args, "abc");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'b',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'c',
erropt: None,
arg: None
})
);
}
#[test]
fn posix_aggregated_with_argument() {
let args = &["prog", "-abf", "file.txt"];
let mut getopt = Getopt::new(args, "abf:");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'b',
erropt: None,
arg: None
})
);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'f',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn posix_unknown_option_returns_question_mark() {
let args = &["prog", "-x"];
let mut getopt = Getopt::new(args, "ab");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: '?',
erropt: Some('x'),
arg: None
})
);
}
#[test]
fn posix_missing_argument_returns_question_mark() {
let args = &["prog", "-a"];
let mut getopt = Getopt::new(args, "a:");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: '?',
erropt: Some('a'),
arg: None
})
);
}
#[test]
fn posix_missing_argument_returns_colon() {
let args = &["prog", "-a"];
let mut getopt = Getopt::new(args, ":a:");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: ':',
erropt: Some('a'),
arg: None
})
);
}
#[test]
fn posix_double_dash_terminates_options() {
let args = &["prog", "-a", "--", "-b"];
let mut getopt = Getopt::new(args, "ab");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(getopt.next(), None); }
#[test]
fn posix_no_error_on_colon_prefix() {
let args = &["prog", "-x"];
let mut getopt = Getopt::new(args, ":ab");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: '?',
erropt: Some('x'),
arg: None
})
);
}
#[test]
fn posix_option_with_no_argument() {
let args = &["prog", "-a", "file.txt"];
let mut getopt = Getopt::new(args, "a");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
}
#[test]
fn posix_mixed_options_and_operands() {
let args = &["prog", "-a", "-b", "file1", "file2"];
let mut getopt = Getopt::new(args, "ab");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'b',
erropt: None,
arg: None
})
);
assert_eq!(getopt.next(), None);
}
#[test]
fn posix_permutation_variant_1() {
let args = &["prog", "-ao", "arg", "path"];
let mut getopt = Getopt::new(args, "a:o:");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: Some("o".into())
})
);
assert_eq!(getopt.next(), None); }
#[test]
fn posix_permutation_variant_2() {
let args = &["prog", "-a", "-o", "arg", "path"];
let mut getopt = Getopt::new(args, "ao:");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'o',
erropt: None,
arg: Some("arg".into())
})
);
assert_eq!(getopt.next(), None);
}
#[test]
fn posix_option_order_independence() {
let args = &["prog", "-o", "arg", "-a", "path"];
let mut getopt = Getopt::new(args, "a:o:");
let r1 = getopt.next();
assert_eq!(
r1,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("arg".into())
})
);
let r2 = getopt.next();
assert_eq!(
r2,
Some(Opt {
val: 'a',
erropt: None,
arg: Some("path".into())
})
);
assert_eq!(getopt.next(), None);
}
#[test]
fn posix_attached_argument_in_aggregated() {
let args = &["prog", "-oarg", "path"];
let mut getopt = Getopt::new(args, "o:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("arg".into())
})
);
assert_eq!(getopt.next(), None);
}
#[test]
fn posix_double_dash_with_dash_option() {
let args = &["prog", "-a", "-o", "arg", "--", "path", "path"];
let mut getopt = Getopt::new(args, "ao:");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'o',
erropt: None,
arg: Some("arg".into())
})
);
assert_eq!(getopt.next(), None);
}
#[test]
fn posix_long_option_with_equals() {
let args = &["prog", "--config=app.conf"];
let mut getopt = Getopt::new(args, "c:(config)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'c',
erropt: None,
arg: Some("app.conf".into())
})
);
}
#[test]
fn posix_long_option_separate_argument() {
let args = &["prog", "--config", "app.conf"];
let mut getopt = Getopt::new(args, "c:(config)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'c',
erropt: None,
arg: Some("app.conf".into())
})
);
}
#[test]
fn posix_long_option_no_argument() {
let args = &["prog", "--help"];
let mut getopt = Getopt::new(args, "h(help)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'h',
erropt: None,
arg: None
})
);
}
#[test]
fn posix_mixed_short_and_long_options() {
let args = &["prog", "-v", "--config=app.conf", "-d"];
let mut getopt = Getopt::new(args, "vdc:(config)");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'v',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'c',
erropt: None,
arg: Some("app.conf".into())
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'd',
erropt: None,
arg: None
})
);
}
#[test]
fn posix_mixed_short_and_long_options_with_nil_value() {
let args = &["prog", "-v", "--config=", "-d"];
let mut getopt = Getopt::new(args, "vdc:(config)");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'v',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'c',
erropt: None,
arg: Some("".into())
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'd',
erropt: None,
arg: None
})
);
}
#[test]
fn posix_all_options_consumed_returns_none() {
let args = &["prog", "-a"];
let mut getopt = Getopt::new(args, "a");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(getopt.next(), None);
assert_eq!(getopt.next(), None); }
#[test]
fn posix_empty_optstring() {
let args = &["prog", "-a", "file"];
let mut getopt = Getopt::new(args, "");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: '?',
erropt: Some('a'),
arg: None
})
);
}
#[test]
fn gnu_optional_argument_double_colon_attached() {
let args = &["prog", "-avalue"];
let mut getopt = Getopt::new(args, "a::");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: Some("value".into())
})
);
}
#[test]
fn gnu_optional_argument_double_colon_separate() {
let args = &["prog", "-a", "file.txt"];
let mut getopt = Getopt::new(args, "a::");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'a',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn gnu_optional_argument_long_option_with_equals() {
let args = &["prog", "--output=result.txt"];
let mut getopt = Getopt::new(args, "o:(output):");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'o',
erropt: None,
arg: Some("result.txt".into())
})
);
}
#[test]
fn gnu_optional_argument_long_option_no_equals() {
let args = &["prog", "--config", "file.txt"];
let mut getopt = Getopt::new(args, "c:(config):");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'c',
erropt: None,
arg: Some("file.txt".into())
})
);
}
#[test]
fn gnu_w_semicolon_long_option_syntax() {
let args = &["prog", "-W", "output=file.txt"];
let mut getopt = Getopt::new(args, "Wo:");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'W',
erropt: None,
arg: None
})
);
}
#[test]
fn gnu_permutation_mode_plus_prefix() {
let args = &["prog", "-a", "file.txt", "-b"];
let mut getopt = Getopt::new(args, "+ab");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(getopt.next(), None);
}
#[test]
fn gnu_non_option_dash_prefix() {
let args = &["prog", "-a", "file.txt", "-b"];
let mut getopt = Getopt::new(args, "-ab");
getopt.set_opterr(false);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
}
#[test]
fn gnu_multiple_option_styles_short_and_long() {
let args = &["prog", "-a", "-d", "file.txt", "-b"];
let mut getopt = Getopt::new(args, "abd:");
assert_eq!(
getopt.next(),
Some(Opt {
val: 'a',
erropt: None,
arg: None
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'd',
erropt: None,
arg: Some("file.txt".into())
})
);
assert_eq!(
getopt.next(),
Some(Opt {
val: 'b',
erropt: None,
arg: None
})
);
}
#[test]
fn gnu_long_option_abbreviation() {
let args = &["prog", "--hel"];
let mut getopt = Getopt::new(args, "h(help)");
getopt.set_opterr(false);
let result = getopt.next();
assert!(result.is_some());
}
#[test]
fn gnu_error_on_unrecognized_long_option() {
let args = &["prog", "--invalid"];
let mut getopt = Getopt::new(args, "a(add)");
getopt.set_opterr(false);
let result = getopt.next();
assert!(result.is_some());
}
#[test]
fn gnu_long_option_with_required_argument() {
let args = &["prog", "--file=myfile.txt"];
let mut getopt = Getopt::new(args, "f:(file)");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'f',
erropt: None,
arg: Some("myfile.txt".into())
})
);
}
#[test]
fn gnu_consecutive_short_options_stress_test() {
let args = &["prog", "-abcdefg"];
let mut getopt = Getopt::new(args, "abcdefg");
for expected_char in &['a', 'b', 'c', 'd', 'e', 'f', 'g'] {
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: *expected_char,
erropt: None,
arg: None
})
);
}
assert_eq!(getopt.next(), None);
}
#[test]
fn gnu_option_argument_edge_case_equals_zero() {
let args = &["prog", "-v0"];
let mut getopt = Getopt::new(args, "v:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'v',
erropt: None,
arg: Some("0".into())
})
);
}
#[test]
fn gnu_option_argument_equals_dash() {
let args = &["prog", "-f", "-"];
let mut getopt = Getopt::new(args, "f:");
let result = getopt.next();
assert_eq!(
result,
Some(Opt {
val: 'f',
erropt: None,
arg: Some("-".into())
})
);
}
#[test]
fn prog_name_simple() {
let args = &["myapp", "-a"];
let getopt = Getopt::new(args, "a");
assert_eq!(getopt.prog_name(), "myapp");
}
#[test]
fn prog_name_with_absolute_path() {
#[cfg(unix)]
let args = &["/usr/bin/myapp", "-a"];
#[cfg(windows)]
let args = &["C:\\Program Files\\myapp", "-a"];
let getopt = Getopt::new(args, "a");
assert_eq!(getopt.prog_name(), "myapp");
}
#[test]
fn prog_name_with_relative_path() {
#[cfg(unix)]
let args = &["./bin/myapp", "-a"];
#[cfg(windows)]
let args = &[".\\bin\\myapp", "-a"];
let getopt = Getopt::new(args, "a");
assert_eq!(getopt.prog_name(), "myapp");
}
#[test]
fn prog_name_empty_args() {
let args: &[&str] = &[];
let getopt = Getopt::new(args, "a");
assert_eq!(getopt.prog_name(), "");
}
#[test]
fn prog_name_empty_string() {
let args = &["", "-a"];
let getopt = Getopt::new(args, "a");
assert_eq!(getopt.prog_name(), "");
}
#[test]
fn prog_name_persists_through_parsing() {
let args = &["testapp", "-a", "-b"];
let mut getopt = Getopt::new(args, "ab");
let _ = getopt.next(); assert_eq!(getopt.prog_name(), "testapp");
let _ = getopt.next(); assert_eq!(getopt.prog_name(), "testapp");
let _ = getopt.next(); assert_eq!(getopt.prog_name(), "testapp");
}
#[test]
fn fuzz_regression_empty_optstring_longopt() {
let args = ["prog", "--help"];
let getopt = Getopt::new(args.iter().copied(), "");
for opt in getopt {
let _ = opt.val();
}
}
#[test]
fn fuzz_regression_empty_optstring_any_arg() {
for arg in &["-a", "-", "--", "--xyz", "--x=y"] {
let args = ["prog", arg];
let getopt = Getopt::new(args.iter().copied(), "");
for opt in getopt {
let _ = opt.val();
}
}
}
#[test]
fn fuzz_regression_parse_short_unclosed_paren() {
let args = ["prog", "-x"];
let getopt = Getopt::new(args.iter().copied(), "a(unclosed");
for opt in getopt {
let _ = opt.val();
}
}
#[test]
fn fuzz_regression_parse_long_unclosed_paren() {
let args = ["prog", "--help"];
let getopt = Getopt::new(args.iter().copied(), "a(unclosed");
for opt in getopt {
let _ = opt.val();
}
}
#[test]
fn non_ascii_option_char_does_not_panic() {
for arg in &["-é", "-ñfoo", "-\u{1F600}", "-a\u{c3}"] {
let args = ["prog", arg];
let mut getopt = Getopt::new(args.iter().copied(), "a:");
getopt.set_opterr(false);
for opt in &mut getopt {
let _ = (opt.val(), opt.erropt(), opt.into_arg());
}
}
}
#[test]
fn non_ascii_optstring_byte_is_not_matchable() {
let args = ["prog", "-é"];
let mut getopt = Getopt::new(args.iter().copied(), "\u{c3}");
getopt.set_opterr(false);
let result = getopt.next();
assert!(matches!(result, Some(ref o) if o.val() == '?'));
}
#[test]
fn close_paren_is_not_a_valid_short_option() {
let args = ["prog", "-)"];
let mut getopt = Getopt::new(args.iter().copied(), "a)b");
getopt.set_opterr(false);
let result = getopt.next();
assert_eq!(result.as_ref().map(Opt::val), Some('?'));
assert_eq!(result.and_then(|o| o.erropt()), Some(')'));
}
}