#![deny(missing_docs)]
use std::fmt::Display;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bag {
program_name: String,
entries: Vec<Entry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
enum Entry {
#[default]
Empty,
LongSwitch(String, Option<String>),
ShortSwitch(String, Option<String>),
Operand(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
MissingProgramName,
InvalidSwitch(String),
}
pub fn parse(args: Vec<String>) -> Result<Bag, ParseError> {
let mut saw_end_of_options = false;
let mut entries: Vec<Entry> = Vec::new();
let mut iter = args.into_iter();
let Some(program_name) = iter.next() else {
return Err(ParseError::MissingProgramName);
};
for arg in iter {
if saw_end_of_options || arg == "-" {
entries.push(Entry::Operand(arg));
continue;
}
if arg == "--" {
entries.push(Entry::Operand(arg));
saw_end_of_options = true;
continue;
}
if let Some(value) = arg.strip_prefix("--") {
if let Some((name, value)) = value.split_once('=') {
if name.is_empty() {
return Err(ParseError::InvalidSwitch(arg));
}
entries.push(Entry::LongSwitch(
String::from(name),
Some(String::from(value)),
));
} else {
entries.push(Entry::LongSwitch(String::from(value), None));
}
continue;
}
if let Some(value) = arg.strip_prefix("-") {
if let Some((name, value)) = value.split_once('=') {
if name.is_empty() {
return Err(ParseError::InvalidSwitch(arg));
}
entries.push(Entry::ShortSwitch(
String::from(name),
Some(String::from(value)),
));
} else {
entries.push(Entry::ShortSwitch(String::from(value), None));
}
continue;
}
entries.push(Entry::Operand(arg));
}
Ok(Bag {
program_name,
entries,
})
}
impl Bag {
pub fn is_empty(&self) -> bool {
self.entries.iter().all(|e| *e == Entry::Empty)
}
pub fn shift_flag(&mut self, name: &str) -> bool {
for entry in self.entries.iter_mut() {
if let Entry::LongSwitch(n, None) | Entry::ShortSwitch(n, None) = entry {
if n != name {
continue;
}
*entry = Entry::Empty;
return true;
};
}
false
}
pub fn shift_operand_with_value(&mut self, expected_value: &str) -> bool {
for entry in self.entries.iter_mut() {
if let Entry::Operand(val) = entry {
if val != expected_value {
continue;
}
*entry = Entry::Empty;
return true;
}
}
false
}
pub fn shift_operand(&mut self) -> Option<String> {
for entry in self.entries.iter_mut() {
if let Entry::Operand(val) = entry {
let value = std::mem::take(val);
*entry = Entry::Empty;
return Some(value);
}
}
None
}
pub fn shift_option(&mut self, name: &str) -> Option<String> {
for i in 0..self.entries.len() {
match &mut self.entries[i] {
Entry::LongSwitch(n, Some(val)) | Entry::ShortSwitch(n, Some(val)) => {
if n != name {
continue;
}
let value = std::mem::take(val);
self.entries[i] = Entry::Empty;
return Some(value);
}
Entry::LongSwitch(n, None) | Entry::ShortSwitch(n, None) => {
if n != name {
continue;
}
let Some(Entry::Operand(val)) = self.entries.get_mut(i + 1) else {
continue;
};
let value = std::mem::take(val);
self.entries[i] = Entry::Empty;
self.entries[i + 1] = Entry::Empty;
return Some(value);
}
_ => continue,
}
}
None
}
pub fn program_name(&self) -> &str {
self.program_name.as_str()
}
pub fn shift_remaining(&mut self) -> Vec<String> {
let mut result = Vec::new();
for entry in self.entries.iter_mut() {
if *entry == Entry::Empty {
continue;
}
result.push(entry.to_string());
*entry = Entry::Empty;
}
result
}
}
impl Display for Entry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Entry::Empty => Ok(()),
Entry::LongSwitch(name, Some(value)) => write!(f, "--{name}={value}"),
Entry::ShortSwitch(name, Some(value)) => write!(f, "-{name}={value}"),
Entry::LongSwitch(name, None) => write!(f, "--{name}"),
Entry::ShortSwitch(name, None) => write!(f, "-{name}"),
Entry::Operand(value) => write!(f, "{value}"),
}
}
}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingProgramName => write!(
f,
"empty list of command line arguments; must have at least one element that corresponds to the executable name"
),
Self::InvalidSwitch(invalid) => write!(f, "option without a name: `{invalid}`"),
}
}
}
impl std::error::Error for ParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn args<const N: usize>(arr: [&str; N]) -> Vec<String> {
arr.iter().map(|s| s.to_string()).collect()
}
#[test]
fn missing_exe_name() {
let result = parse(args([]));
assert_eq!(result, Err(ParseError::MissingProgramName));
}
#[test]
fn malformed_option() {
let result = parse(args(["program", "--=value"]));
assert_eq!(
result,
Err(ParseError::InvalidSwitch(String::from("--=value")))
);
let result = parse(args(["program", "-=value"]));
assert_eq!(
result,
Err(ParseError::InvalidSwitch(String::from("-=value")))
);
}
#[test]
fn shift_eql_separated_options() {
let mut bag = parse(args(["program", "--opt1=val1", "-opt2=val2"])).unwrap();
assert!(!bag.is_empty());
assert_eq!(bag.shift_option("opt1").as_deref(), Some("val1"));
assert_eq!(bag.shift_option("opt2").as_deref(), Some("val2"));
assert_eq!(bag.shift_option("opt2"), None);
assert!(bag.is_empty());
}
#[test]
fn shift_space_separated_options() {
let mut bag = parse(args(["program", "--opt1", "val1", "-opt2", "val2"])).unwrap();
assert_eq!(bag.shift_option("opt1").as_deref(), Some("val1"));
assert_eq!(bag.shift_option("opt2").as_deref(), Some("val2"));
assert_eq!(bag.shift_option("opt2"), None);
assert!(bag.is_empty());
}
#[test]
fn shift_option_does_not_remove_flags() {
let mut bag = parse(args(["program", "--switch", "--switch", "value"])).unwrap();
assert_eq!(bag.shift_option("switch").as_deref(), Some("value"));
assert!(bag.shift_flag("switch"));
assert!(bag.is_empty());
}
#[test]
fn shift_option_returns_multiple_values() {
let mut bag = parse(args(["program", "--opt", "value", "--opt=value2"])).unwrap();
assert_eq!(bag.shift_option("opt").as_deref(), Some("value"));
assert_eq!(bag.shift_option("opt").as_deref(), Some("value2"));
assert_eq!(bag.shift_option("opt"), None);
}
#[test]
fn shifting_the_same_flag_multiple_times() {
let mut bag = parse(args(["prgoram", "--flag", "--flag"])).unwrap();
assert!(bag.shift_flag("flag"));
assert!(bag.shift_flag("flag"));
assert!(!bag.shift_flag("flag"));
assert!(bag.is_empty());
}
#[test]
fn shift_operand() {
let mut bag = parse(args(["program", "-", "a", "b", "--", "c", "--", "-foo"])).unwrap();
assert!(!bag.is_empty());
assert_eq!(bag.shift_operand().as_deref(), Some("-"));
assert_eq!(bag.shift_operand().as_deref(), Some("a"));
assert_eq!(bag.shift_operand().as_deref(), Some("b"));
assert_eq!(bag.shift_operand().as_deref(), Some("--"));
assert_eq!(bag.shift_operand().as_deref(), Some("c"));
assert_eq!(bag.shift_operand().as_deref(), Some("--"));
assert_eq!(bag.shift_operand().as_deref(), Some("-foo"));
assert_eq!(bag.shift_operand(), None);
assert!(bag.is_empty());
}
#[test]
fn resolving_operand_ambiguity() {
let mut bag = parse(args(["program", "--flag", "operand"])).unwrap();
assert_eq!(bag.shift_operand().as_deref(), Some("operand"));
assert_eq!(bag.shift_option("flag"), None);
assert!(bag.shift_flag("flag"));
assert!(bag.is_empty());
let mut bag = parse(args(["program", "--flag", "operand"])).unwrap();
assert_eq!(bag.shift_option("flag").as_deref(), Some("operand"));
assert_eq!(bag.shift_operand(), None);
assert!(bag.is_empty());
}
#[test]
fn shifting_remaining_arguments() {
let mut bag = parse(args(["program", "-", "a", "b", "--", "c"])).unwrap();
assert_eq!(bag.shift_operand().as_deref(), Some("-"));
assert_eq!(
bag.shift_remaining(),
vec![
String::from("a"),
String::from("b"),
String::from("--"),
String::from("c")
]
);
assert!(bag.is_empty());
}
#[test]
fn shift_operand_with_value() {
let mut bag = parse(args(["program", "a", "b"])).unwrap();
assert!(bag.shift_operand_with_value("b"));
assert!(bag.shift_operand_with_value("a"));
assert!(bag.is_empty());
}
}
#[cfg(doctest)]
#[doc = include_str!("README.md")]
struct ReadmeDocTest;