pub(crate) struct ArgParser<'a> {
args: &'a [String],
pos: usize,
}
impl<'a> ArgParser<'a> {
pub fn new(args: &'a [String]) -> Self {
Self { args, pos: 0 }
}
pub fn is_done(&self) -> bool {
self.pos >= self.args.len()
}
pub fn current(&self) -> Option<&'a str> {
self.args.get(self.pos).map(|s| s.as_str())
}
pub fn rest(&self) -> &'a [String] {
if self.pos < self.args.len() {
&self.args[self.pos..]
} else {
&[]
}
}
pub fn advance(&mut self) {
self.pos += 1;
}
pub fn flag(&mut self, name: &str) -> bool {
if self.current() == Some(name) {
self.advance();
true
} else {
false
}
}
pub fn flag_any(&mut self, names: &[&str]) -> bool {
if self.current().is_some_and(|cur| names.contains(&cur)) {
self.advance();
return true;
}
false
}
pub fn flag_value(
&mut self,
name: &str,
cmd: &str,
) -> std::result::Result<Option<&'a str>, String> {
let arg = match self.args.get(self.pos) {
Some(a) => a.as_str(),
None => return Ok(None),
};
if arg == name {
self.pos += 1;
match self.args.get(self.pos) {
Some(val) => {
self.pos += 1;
Ok(Some(val.as_str()))
}
None => Err(format!("{cmd}: {name} requires an argument")),
}
} else if let Some(rest) = arg.strip_prefix(name) {
if !rest.is_empty() {
self.pos += 1;
Ok(Some(rest))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
pub fn flag_value_any(
&mut self,
names: &[&str],
cmd: &str,
) -> std::result::Result<Option<&'a str>, String> {
let arg = match self.args.get(self.pos) {
Some(a) => a.as_str(),
None => return Ok(None),
};
for (i, &name) in names.iter().enumerate() {
if arg == name {
self.pos += 1;
return match self.args.get(self.pos) {
Some(val) => {
self.pos += 1;
Ok(Some(val.as_str()))
}
None => Err(format!("{cmd}: {name} requires an argument")),
};
}
if i == 0
&& let Some(rest) = arg.strip_prefix(name).filter(|r| !r.is_empty())
{
self.pos += 1;
return Ok(Some(rest));
}
}
Ok(None)
}
pub fn flag_value_opt(&mut self, name: &str) -> Option<&'a str> {
let arg = match self.args.get(self.pos) {
Some(a) => a.as_str(),
None => return None,
};
if arg == name {
self.pos += 1;
if let Some(val) = self.args.get(self.pos) {
self.pos += 1;
Some(val.as_str())
} else {
None
}
} else if let Some(rest) = arg.strip_prefix(name) {
if !rest.is_empty() {
self.pos += 1;
Some(rest)
} else {
None
}
} else {
None
}
}
pub fn positional(&mut self) -> Option<&'a str> {
let val = self.args.get(self.pos).map(|s| s.as_str())?;
self.pos += 1;
Some(val)
}
pub fn is_flag(&self) -> bool {
self.args
.get(self.pos)
.map(|s| s.starts_with('-') && s.len() > 1)
.unwrap_or(false)
}
pub fn bool_flags(&mut self, allowed: &str) -> Vec<char> {
if let Some(arg) = self.current()
&& arg.starts_with('-')
&& !arg.starts_with("--")
&& arg.len() > 1
{
let chars: Vec<char> = arg[1..].chars().collect();
if chars.iter().all(|c| allowed.contains(*c)) {
self.advance();
return chars;
}
}
Vec::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn args(strs: &[&str]) -> Vec<String> {
strs.iter().map(|s| s.to_string()).collect()
}
#[test]
fn test_flag() {
let a = args(&["-v", "file"]);
let mut p = ArgParser::new(&a);
assert!(p.flag("-v"));
assert!(!p.flag("-v"));
assert_eq!(p.current(), Some("file"));
}
#[test]
fn test_flag_value_separate() {
let a = args(&["-n", "10", "file"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.flag_value("-n", "cmd").unwrap(), Some("10"));
assert_eq!(p.current(), Some("file"));
}
#[test]
fn test_flag_value_attached() {
let a = args(&["-n10", "file"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.flag_value("-n", "cmd").unwrap(), Some("10"));
assert_eq!(p.current(), Some("file"));
}
#[test]
fn test_flag_value_missing() {
let a = args(&["-n"]);
let mut p = ArgParser::new(&a);
assert!(p.flag_value("-n", "cmd").is_err());
}
#[test]
fn test_flag_value_no_match() {
let a = args(&["-v"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.flag_value("-n", "cmd").unwrap(), None);
assert_eq!(p.current(), Some("-v"));
}
#[test]
fn test_flag_any() {
let a = args(&["--verbose"]);
let mut p = ArgParser::new(&a);
assert!(p.flag_any(&["-v", "--verbose"]));
assert!(p.is_done());
}
#[test]
fn test_flag_value_any() {
let a = args(&["--output", "file.txt"]);
let mut p = ArgParser::new(&a);
assert_eq!(
p.flag_value_any(&["-o", "--output"], "cmd").unwrap(),
Some("file.txt")
);
}
#[test]
fn test_flag_value_opt_no_value() {
let a = args(&["-n"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.flag_value_opt("-n"), None);
}
#[test]
fn test_flag_value_opt_separate() {
let a = args(&["-n", "10", "file"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.flag_value_opt("-n"), Some("10"));
assert_eq!(p.current(), Some("file"));
}
#[test]
fn test_flag_value_opt_attached() {
let a = args(&["-n10", "file"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.flag_value_opt("-n"), Some("10"));
assert_eq!(p.current(), Some("file"));
}
#[test]
fn test_flag_value_any_attached() {
let a = args(&["-ofile.txt"]);
let mut p = ArgParser::new(&a);
assert_eq!(
p.flag_value_any(&["-o", "--output"], "cmd").unwrap(),
Some("file.txt")
);
assert!(p.is_done());
}
#[test]
fn test_flag_value_any_missing() {
let a = args(&["--output"]);
let mut p = ArgParser::new(&a);
assert!(p.flag_value_any(&["-o", "--output"], "cmd").is_err());
}
#[test]
fn test_current() {
let a = args(&["hello"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.current(), Some("hello"));
p.advance();
assert_eq!(p.current(), None);
}
#[test]
fn test_positional() {
let a = args(&["file1", "file2"]);
let mut p = ArgParser::new(&a);
assert_eq!(p.positional(), Some("file1"));
assert_eq!(p.positional(), Some("file2"));
assert!(p.is_done());
}
#[test]
fn test_rest() {
let a = args(&["-v", "cmd", "arg1", "arg2"]);
let mut p = ArgParser::new(&a);
p.advance(); p.advance(); assert_eq!(p.rest().len(), 2);
}
#[test]
fn test_bool_flags() {
let a = args(&["-rnuf", "file"]);
let mut p = ArgParser::new(&a);
let flags = p.bool_flags("rnufsz");
assert_eq!(flags, vec!['r', 'n', 'u', 'f']);
assert_eq!(p.current(), Some("file"));
}
#[test]
fn test_bool_flags_no_match_unknown_char() {
let a = args(&["-rxn", "file"]);
let mut p = ArgParser::new(&a);
let flags = p.bool_flags("rn"); assert!(flags.is_empty());
assert_eq!(p.current(), Some("-rxn")); }
#[test]
fn test_bool_flags_long_flag_ignored() {
let a = args(&["--verbose"]);
let mut p = ArgParser::new(&a);
let flags = p.bool_flags("verbose");
assert!(flags.is_empty());
}
#[test]
fn test_bool_flags_single_dash_ignored() {
let a = args(&["-"]);
let mut p = ArgParser::new(&a);
let flags = p.bool_flags("abc");
assert!(flags.is_empty());
assert_eq!(p.current(), Some("-")); }
#[test]
fn test_bool_flags_single_char() {
let a = args(&["-v"]);
let mut p = ArgParser::new(&a);
let flags = p.bool_flags("v");
assert_eq!(flags, vec!['v']);
assert!(p.is_done());
}
#[test]
fn test_is_flag() {
let a = args(&["-v", "-", "file", "--long"]);
let mut p = ArgParser::new(&a);
assert!(p.is_flag()); p.advance();
assert!(!p.is_flag()); p.advance();
assert!(!p.is_flag()); p.advance();
assert!(p.is_flag()); }
}