#[derive(Debug, Default, PartialEq, Eq)]
pub struct StrictArgs {
pub dry_run: bool,
pub recursive: bool,
pub verbose: bool,
pub list_sequences: bool,
pub sequence: Option<String>,
pub config_file: Option<String>,
pub paths: Vec<String>,
pub help: bool,
pub version: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum StrictParseError {
UnknownFlag(char),
UnknownLongFlag(String),
MissingArgument(String),
}
impl std::fmt::Display for StrictParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StrictParseError::UnknownFlag(c) => {
write!(f, "rusty-detox: invalid option -- '{c}'")
}
StrictParseError::UnknownLongFlag(s) => {
write!(f, "rusty-detox: unrecognized option '--{s}'")
}
StrictParseError::MissingArgument(s) => {
write!(f, "rusty-detox: option requires an argument -- '{s}'")
}
}
}
}
impl std::error::Error for StrictParseError {}
pub fn parse(args: &[String]) -> Result<StrictArgs, StrictParseError> {
let mut out = StrictArgs::default();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "--" {
out.paths.extend(args[i + 1..].iter().cloned());
break;
} else if let Some(long) = arg.strip_prefix("--") {
match long {
"dry-run" => out.dry_run = true,
"recursive" => out.recursive = true,
"verbose" => out.verbose = true,
"list-sequences" => out.list_sequences = true,
"help" => out.help = true,
"version" => out.version = true,
"strict" | "no-strict" => {} "sequence" => {
i += 1;
out.sequence = Some(
args.get(i)
.cloned()
.ok_or_else(|| StrictParseError::MissingArgument("sequence".into()))?,
);
}
"config-file" => {
i += 1;
out.config_file =
Some(args.get(i).cloned().ok_or_else(|| {
StrictParseError::MissingArgument("config-file".into())
})?);
}
_ => return Err(StrictParseError::UnknownLongFlag(long.to_string())),
}
} else if let Some(shorts) = arg.strip_prefix('-') {
if shorts.is_empty() {
out.paths.push(arg.clone());
} else {
let mut chars = shorts.chars();
while let Some(c) = chars.next() {
match c {
'n' => out.dry_run = true,
'r' => out.recursive = true,
'v' => out.verbose = true,
'L' => out.list_sequences = true,
'h' => out.help = true,
'V' => out.version = true,
's' => {
let rest: String = chars.clone().collect();
if !rest.is_empty() {
out.sequence = Some(rest);
break;
}
i += 1;
out.sequence =
Some(args.get(i).cloned().ok_or_else(|| {
StrictParseError::MissingArgument("s".into())
})?);
break;
}
'f' => {
let rest: String = chars.clone().collect();
if !rest.is_empty() {
out.config_file = Some(rest);
break;
}
i += 1;
out.config_file =
Some(args.get(i).cloned().ok_or_else(|| {
StrictParseError::MissingArgument("f".into())
})?);
break;
}
unk => return Err(StrictParseError::UnknownFlag(unk)),
}
}
}
} else {
out.paths.push(arg.clone());
}
i += 1;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grouped_short_flags() {
let p = parse(&["-rv".into()]).unwrap();
assert!(p.recursive && p.verbose);
}
#[test]
fn last_wins_sequence_flag() {
let p = parse(&["-s".into(), "default".into(), "-s".into(), "utf_8".into()]).unwrap();
assert_eq!(p.sequence.as_deref(), Some("utf_8"));
}
#[test]
fn glued_short_flag_with_arg() {
let p = parse(&["-sutf_8".into()]).unwrap();
assert_eq!(p.sequence.as_deref(), Some("utf_8"));
}
#[test]
fn unknown_short_flag_rejected() {
let err = parse(&["-Z".into()]).unwrap_err();
assert_eq!(err, StrictParseError::UnknownFlag('Z'));
}
#[test]
fn unknown_long_flag_rejected() {
let err = parse(&["--unknown".into()]).unwrap_err();
assert_eq!(err, StrictParseError::UnknownLongFlag("unknown".into()));
}
#[test]
fn completions_token_is_a_positional() {
let p = parse(&["completions".into(), "bash".into()]).unwrap();
assert_eq!(p.paths, vec!["completions", "bash"]);
}
#[test]
fn unknown_flag_message_format() {
let err = StrictParseError::UnknownFlag('Z');
assert_eq!(err.to_string(), "rusty-detox: invalid option -- 'Z'");
}
}