use std::collections::VecDeque;
use std::ffi::OsString;
use std::ops::RangeInclusive;
pub const YEARS: RangeInclusive<i32> = 2000..=2100;
#[derive(Debug)]
pub struct Args {
program: &'static str,
rest: VecDeque<String>,
typed: Vec<String>,
}
impl Args {
pub fn from_env(program: &'static str) -> Self {
Self::new(program, std::env::args_os().skip(1))
}
pub fn new(program: &'static str, args: impl Iterator<Item = OsString>) -> Self {
let rest = args
.map(|arg| arg.to_string_lossy().into_owned())
.flat_map(|arg| match arg.split_once('=') {
Some((key, value)) if key.starts_with("--") => {
vec![key.to_string(), value.to_string()]
}
_ => vec![arg],
})
.collect();
Self {
program,
rest,
typed: Vec::new(),
}
}
pub fn next_arg(&mut self) -> Option<String> {
self.rest.pop_front()
}
pub fn peek_value(&self) -> bool {
self.rest.front().is_some_and(|next| !next.starts_with('-'))
}
pub fn value(&mut self, flag: &str) -> String {
self.remember(flag);
self.rest
.pop_front()
.unwrap_or_else(|| self.fail(&format!("{flag} needs a value")))
}
pub fn number(&mut self, flag: &str) -> i64 {
let raw = self.value(flag);
raw.parse()
.unwrap_or_else(|_| self.fail(&format!("{flag} needs a number, not {raw:?}")))
}
pub fn year(&mut self, flag: &str) -> i32 {
let raw = self.value(flag);
raw.parse::<i32>()
.ok()
.filter(|year| YEARS.contains(year))
.unwrap_or_else(|| {
self.fail(&format!(
"{flag} wants a year between {} and {}, not {raw:?}",
YEARS.start(),
YEARS.end()
))
})
}
pub fn remember(&mut self, flag: &str) {
let name = flag.trim_start_matches('-').to_string();
if !self.typed.contains(&name) {
self.typed.push(name);
}
}
pub fn was_typed(&self, flag: &str) -> bool {
self.typed
.iter()
.any(|seen| seen == flag.trim_start_matches('-'))
}
pub fn fail(&self, message: &str) -> ! {
eprintln!("{}: {message}", self.program);
std::process::exit(2)
}
}
#[cfg(test)]
mod tests {
use super::Args;
fn args(items: &[&str]) -> Args {
Args::new("test", items.iter().map(|item| (*item).into()))
}
#[test]
fn long_options_split_on_equals_and_nothing_else_does() {
let mut parsed = args(&["--year=2027", "-y", "2026", "some=path"]);
assert_eq!(parsed.next_arg().as_deref(), Some("--year"));
assert_eq!(parsed.value("--year"), "2027");
assert_eq!(parsed.next_arg().as_deref(), Some("-y"));
assert_eq!(parsed.value("-y"), "2026");
assert_eq!(parsed.next_arg().as_deref(), Some("some=path"));
}
#[test]
fn an_optional_value_is_told_from_the_next_flag() {
let mut parsed = args(&["--track", "octocat", "--year", "2027"]);
parsed.next_arg();
assert!(parsed.peek_value(), "a login follows");
assert_eq!(parsed.value("--track"), "octocat");
parsed.next_arg();
assert!(parsed.peek_value(), "a year follows");
let mut bare = args(&["--track", "--year", "2027"]);
bare.next_arg();
assert!(!bare.peek_value(), "a flag is not a value");
}
#[test]
fn typed_flags_are_remembered_so_defaults_know_to_stay_out() {
let mut parsed = args(&["--year", "2027"]);
parsed.next_arg();
parsed.value("--year");
assert!(parsed.was_typed("--year"));
assert!(parsed.was_typed("year"), "with or without the dashes");
assert!(!parsed.was_typed("--top"));
}
}