use std::env;
use std::error;
use std::fmt;
use std::ops;
use CmdOpt::*;
use InfoCode::*;
use OptValSpec::*;
use ParseError::*;
use ProcessCode::*;
#[derive(Clone, Debug, PartialEq)]
pub enum CmdOpt {
Short(char),
Long(String),
}
impl fmt::Display for CmdOpt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Self::Short(o) => write!(f, "-{}", o),
Self::Long(o) => write!(f, "--{}", o),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ParseError {
EmptyOptFound,
InvalidOpt(CmdOpt, String),
GenericErr(String),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Self::EmptyOptFound => {
write!(f, "Empty option")
}
Self::InvalidOpt(opt, desc) => {
write!(f, "{}: {}", opt.to_string(), desc)?;
Ok(())
}
Self::GenericErr(desc) => {
write!(f, "{}", desc)?;
Ok(())
}
}
}
}
impl error::Error for ParseError {}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ProcessCode {
Continue,
Break,
ToggleParsingMode,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum InfoCode {
ValueOpt,
NoValueOpt,
InvalidOpt,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OptValSpec {
Standalone,
StandaloneEqu,
Group,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OptVal {
pub val: String,
pub val_spec: OptValSpec,
}
pub struct OptConstr(u32);
impl Default for OptConstr {
fn default() -> Self {
OptConstr(0)
}
}
impl OptConstr {
const NOT_IN_GROUP: u32 = 1;
#[inline]
pub fn not_in_group(&mut self) {
self.0 |= Self::NOT_IN_GROUP;
}
#[inline]
fn is_not_in_group(&self) -> bool {
(self.0 & Self::NOT_IN_GROUP) != 0
}
}
macro_rules! process_h_rc {
($rc:expr, $mod:expr) => {
match $rc {
Break => {
return Ok(());
}
ToggleParsingMode => {
$mod = !$mod;
}
_ => {}
}
};
}
fn parse_opts_iter<I, Fi, Fh>(opts: I, mut opt_i: Fi, mut opt_h: Fh) -> Result<(), ParseError>
where
I: Iterator<Item = String>,
Fi: FnMut(&CmdOpt, &mut OptConstr) -> InfoCode,
Fh: FnMut(&Option<CmdOpt>, &Option<OptVal>) -> Result<ProcessCode, ParseError>,
{
let mut val_mode = false; let mut val_req = false;
let mut opt: Option<CmdOpt> = None;
for tkn in opts.into_iter() {
if val_mode || val_req || !tkn.starts_with("-") {
let val = Some(OptVal {
val: tkn,
val_spec: Standalone,
});
let h_rc = opt_h(&opt, &val)?;
process_h_rc!(h_rc, val_mode);
val_req = false;
opt = None;
continue;
}
if tkn.starts_with("--") && tkn.len() > 2 {
let _opt;
let mut constr: OptConstr = Default::default();
if let Some(equ) = tkn.find('=') {
_opt = Long(tkn[ops::Range { start: 2, end: equ }].to_string());
let val = if tkn.len() <= equ + 1 {
None
} else {
Some(OptVal {
val: tkn[ops::RangeFrom { start: equ + 1 }].to_string(),
val_spec: StandaloneEqu,
})
};
match opt_i(&_opt, &mut constr) {
InfoCode::InvalidOpt => {
return Err(ParseError::InvalidOpt(
_opt.clone(),
"Invalid option".to_string(),
));
}
ValueOpt => {
let h_rc = opt_h(&Some(_opt), &val)?;
process_h_rc!(h_rc, val_mode);
}
NoValueOpt => {
return Err(ParseError::InvalidOpt(
_opt.clone(),
"Argument not expected".to_string(),
));
}
}
} else {
_opt = Long(tkn[2..].to_string());
match opt_i(&_opt, &mut constr) {
InfoCode::InvalidOpt => {
return Err(ParseError::InvalidOpt(
_opt.clone(),
"Invalid option".to_string(),
));
}
ValueOpt => {
opt = Some(_opt);
val_req = true;
}
NoValueOpt => {
let h_rc = opt_h(&Some(_opt), &None)?;
process_h_rc!(h_rc, val_mode);
}
}
}
} else {
let opts_grp = tkn[1..].to_string();
let opts_grp_len = opts_grp.len();
if opts_grp_len <= 0 {
return Err(EmptyOptFound);
}
for (i, c) in opts_grp.char_indices() {
let mut constr: OptConstr = Default::default();
let _opt = Short(c);
match opt_i(&_opt, &mut constr) {
InfoCode::InvalidOpt => {
return Err(ParseError::InvalidOpt(
_opt.clone(),
"Invalid option".to_string(),
));
}
ValueOpt => {
if constr.is_not_in_group() && i > 0 {
return Err(ParseError::InvalidOpt(
_opt.clone(),
"The option may not exist in group".to_string(),
));
}
if i + 1 >= opts_grp_len {
opt = Some(_opt);
val_req = true;
} else {
let val = Some(OptVal {
val: opts_grp[ops::RangeFrom { start: i + 1 }].to_string(),
val_spec: Group,
});
let h_rc = opt_h(&Some(_opt), &val)?;
process_h_rc!(h_rc, val_mode);
}
break;
}
NoValueOpt => {
if constr.is_not_in_group() && opts_grp.chars().count() > 1 {
return Err(ParseError::InvalidOpt(
_opt.clone(),
"The option may not exist in group".to_string(),
));
}
let h_rc = opt_h(&Some(_opt), &None)?;
process_h_rc!(h_rc, val_mode);
}
}
}
}
}
if opt.is_some() {
if val_req {
return Err(ParseError::InvalidOpt(
opt.unwrap().clone(),
"Argument required".to_string(),
));
} else {
opt_h(&opt, &None)?;
}
}
Ok(())
}
#[inline]
pub fn parse_opts<Fi, Fh>(opt_i: Fi, opt_h: Fh) -> Result<(), ParseError>
where
Fi: FnMut(&CmdOpt, &mut OptConstr) -> InfoCode,
Fh: FnMut(&Option<CmdOpt>, &Option<OptVal>) -> Result<ProcessCode, ParseError>,
{
parse_opts_iter(env::args().skip(1), opt_i, opt_h)
}
#[allow(unused_imports)]
mod tests {
use super::*;
#[test]
fn test_empty_opt() {
let opts = vec!["-"].into_iter().map(|v| v.to_string());
let rc = parse_opts_iter(
opts,
|_, _| {
return NoValueOpt;
},
|opt, val| {
match (opt, val) {
_ => {
println!("UNEXPECTED opt:{:?}, val:{:?}", opt, val);
assert!(false);
}
};
Ok(Continue)
},
);
assert_eq!(rc, Err(EmptyOptFound));
}
#[test]
fn test_short_noval() {
let opts = vec![
"-abc", "-d", "-e", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|opt, _| {
if let Short(o) = opt {
if "abcd".contains(*o) {
NoValueOpt
} else {
InfoCode::InvalidOpt
}
} else {
InfoCode::InvalidOpt
}
},
|opt, val| {
match (i, opt, val) {
(0, Some(Short('a')), None) => {}
(1, Some(Short('b')), None) => {}
(2, Some(Short('c')), None) => {}
(3, Some(Short('d')), None) => {}
(4, Some(Short('e')), None) => {}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
Ok(Continue)
},
);
assert_eq!(i, 4);
if let Err(ParseError::InvalidOpt(Short(o), _)) = rc {
assert_eq!(o, 'e');
} else {
assert!(false);
}
}
#[test]
fn test_short_val() {
let opts = vec![
"-abc", "-d", "-e", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|opt, _| {
if let Short(o) = opt {
if "ad".contains(*o) {
ValueOpt
} else {
InfoCode::InvalidOpt
}
} else {
InfoCode::InvalidOpt
}
},
|opt, val| {
match (i, opt, val) {
(0, Some(Short('a')), Some(v)) if v.val == "bc" && v.val_spec == Group => {}
(1, Some(Short('d')), Some(v)) if v.val == "-e" && v.val_spec == Standalone => {
}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
Ok(Continue)
},
);
assert_eq!(i, 2);
assert_eq!(rc, Ok(()));
}
#[test]
fn test_long() {
let opts = vec![
"--a", "--b=v", "--b", "v", "--c=v", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|opt, _| {
if let Long(o) = opt {
match o.as_str() {
"a" | "c" => InfoCode::NoValueOpt,
"b" => InfoCode::ValueOpt,
_ => InfoCode::InvalidOpt,
}
} else {
InfoCode::InvalidOpt
}
},
|opt, val| {
match (i, opt, val) {
(0, Some(Long(o)), None) if o == "a" => {}
(1, Some(Long(o)), Some(v))
if o == "b" && v.val == "v" && v.val_spec == StandaloneEqu => {}
(2, Some(Long(o)), Some(v))
if o == "b" && v.val == "v" && v.val_spec == Standalone => {}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
Ok(Continue)
},
);
assert_eq!(i, 3);
if let Err(ParseError::InvalidOpt(Long(o), _)) = rc {
assert_eq!(o, "c");
} else {
assert!(false);
}
}
#[test]
fn test_inv_long_opt() {
let opts = vec!["--a", "--a=v"];
for a in opts.into_iter() {
let opt = vec![a].into_iter().map(|v| v.to_string());
let rc = parse_opts_iter(
opt,
|_, _| InfoCode::InvalidOpt,
|opt, val| {
match (opt, val) {
_ => {
println!("UNEXPECTED opt:{:?}, val:{:?}", opt, val);
assert!(false);
}
}
Ok(Continue)
},
);
if let Err(ParseError::InvalidOpt(Long(o), _)) = rc {
assert_eq!(o, "a");
} else {
assert!(false);
}
}
}
#[test]
fn test_last_opt() {
let opt = vec!["--a", "v"].into_iter().map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opt,
|_, _| InfoCode::ValueOpt,
|opt, val| {
match (i, opt, val) {
(0, Some(Long(o)), Some(v))
if o == "a" && v.val == "v" && v.val_spec == Standalone => {}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
}
i += 1;
Ok(Continue)
},
);
assert_eq!(i, 1);
assert_eq!(rc, Ok(()));
let opt = vec!["-a"].into_iter().map(|v| v.to_string());
let rc = parse_opts_iter(
opt,
|_, _| InfoCode::ValueOpt,
|opt, val| {
match (i, opt, val) {
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
}
Ok(Continue)
},
);
if let Err(ParseError::InvalidOpt(Short(o), _)) = rc {
assert_eq!(o, 'a');
} else {
assert!(false);
}
}
#[test]
fn test_modes_switch() {
let opts = vec![
"-a", "--", "--b", "-c", "--", "-d-e", "-f", "FILE", "--", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|_, _| InfoCode::NoValueOpt,
|opt, val| {
let mut ret = Ok(Continue);
match (i, opt, val) {
(0, Some(Short('a')), None) => {}
(1, Some(Short('-')), None) => {
ret = Ok(ToggleParsingMode);
}
(2, None, Some(v)) if v.val == "--b" && v.val_spec == Standalone => {}
(3, None, Some(v)) if v.val == "-c" && v.val_spec == Standalone => {}
(4, None, Some(v)) if v.val == "--" => {
ret = Ok(ToggleParsingMode);
}
(5, Some(Short('d')), None) => {}
(6, Some(Short('-')), None) => {
ret = Ok(ToggleParsingMode);
}
(7, Some(Short('e')), None) => {}
(8, None, Some(v)) if v.val == "-f" && v.val_spec == Standalone => {}
(9, None, Some(v)) if v.val == "FILE" && v.val_spec == Standalone => {}
(10, None, Some(v)) if v.val == "--" && v.val_spec == Standalone => {
ret = Ok(ToggleParsingMode);
}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
ret
},
);
assert_eq!(i, 11);
assert_eq!(rc, Ok(()));
}
#[test]
fn test_break() {
let opts = vec![
"-a", "--help", "-b", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|_, _| InfoCode::NoValueOpt,
|opt, val| {
let mut ret = Ok(Continue);
match (i, opt, val) {
(0, Some(Short('a')), None) => {}
(1, Some(Long(opt)), None) if opt == "help" => {
ret = Ok(Break);
}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
ret
},
);
assert_eq!(i, 2);
assert_eq!(rc, Ok(()));
}
#[test]
fn test_not_in_group_constr() {
let opts = vec![
"-a", "-ab", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|opt, constr| {
if let Short('a') = opt {
constr.not_in_group();
}
InfoCode::NoValueOpt
},
|opt, val| {
match (i, opt, val) {
(0, Some(Short('a')), None) => {}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
Ok(Continue)
},
);
assert_eq!(i, 1);
if let Err(ParseError::InvalidOpt(Short(o), _)) = rc {
assert_eq!(o, 'a');
} else {
assert!(false);
}
let opts = vec![
"-av", "-bav", ]
.into_iter()
.map(|v| v.to_string());
let mut i = 0;
let rc = parse_opts_iter(
opts,
|opt, constr| {
if let Short('a') = opt {
constr.not_in_group();
InfoCode::ValueOpt
} else {
InfoCode::NoValueOpt
}
},
|opt, val| {
match (i, opt, val) {
(0, Some(Short('a')), Some(v)) if v.val == "v" && v.val_spec == Group => {}
(1, Some(Short('b')), None) => {}
_ => {
println!("UNEXPECTED i:{}, opt:{:?}, val:{:?}", i, opt, val);
assert!(false);
}
};
i += 1;
Ok(Continue)
},
);
assert_eq!(i, 2);
if let Err(ParseError::InvalidOpt(Short(o), _)) = rc {
assert_eq!(o, 'a');
} else {
assert!(false);
}
}
}