use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct Args {
pub positionals: Vec<String>,
opts: HashMap<String, Vec<String>>,
}
fn canonical(k: &str) -> &str {
match k {
"alternativa" => "alternative",
"bloquea" => "blocks",
"cascada" => "cascade",
"forzar" => "force",
"luego" => "next",
"padre" => "parent",
"por" => "why",
"razon" => "reason",
"reabrir" => "reopen",
"rescatar" => "rescue",
"tipo" => "type",
"todo" => "all",
other => other,
}
}
impl Args {
pub fn parse<I: IntoIterator<Item = String>>(it: I) -> Args {
let v: Vec<String> = it.into_iter().collect();
let mut a = Args::default();
let mut i = 0;
while i < v.len() {
if let Some(k) = v[i].strip_prefix("--") {
let (k, inline) = match k.split_once('=') {
Some((k, val)) => (k, Some(val.to_string())),
None => (k, None),
};
let val = inline.or_else(|| {
v.get(i + 1).filter(|n| !n.starts_with("--")).map(|n| {
i += 1;
n.clone()
})
});
a.opts.entry(canonical(k).to_string()).or_default().extend(val);
} else {
a.positionals.push(v[i].clone());
}
i += 1;
}
a
}
pub fn has(&self, k: &str) -> bool {
self.opts.contains_key(k)
}
pub fn opt(&self, k: &str) -> Option<&str> {
self.opts.get(k).and_then(|v| v.last()).map(|s| s.as_str())
}
pub fn opt_or(&self, k: &str) -> String {
self.opt(k).unwrap_or_default().to_string()
}
pub fn list(&self, k: &str) -> Vec<String> {
self.opts.get(k).cloned().unwrap_or_default()
}
pub fn positional(&self, i: usize) -> Option<&str> {
self.positionals.get(i).map(|s| s.as_str())
}
pub fn unknown(&self, permitidas: &[&str]) -> Vec<&str> {
let mut v: Vec<&str> = self
.opts
.keys()
.map(|k| k.as_str())
.filter(|k| !permitidas.contains(k))
.collect();
v.sort_unstable();
v
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Args {
Args::parse(s.split_whitespace().map(String::from))
}
#[test]
fn positionals_and_options() {
let a = p("title --why reason --blocks --ref one --ref two");
assert_eq!(a.positional(0), Some("title"));
assert_eq!(a.opt("why"), Some("reason"));
assert!(a.has("blocks"));
assert_eq!(a.list("ref"), vec!["one", "two"]);
}
#[test]
fn a_flag_next_to_another_flag() {
let a = p("--blocks --why x");
assert!(a.has("blocks"));
assert_eq!(a.opt("blocks"), None);
assert_eq!(a.opt("why"), Some("x"));
}
#[test]
fn an_option_that_does_not_exist_does_not_pass_in_silence() {
let a = p("x --kind finding --type task");
assert_eq!(a.unknown(&["type", "why"]), vec!["kind"]);
assert!(a.unknown(&["type", "kind"]).is_empty());
}
#[test]
fn equals_sign() {
let a = p("--type=decision");
assert_eq!(a.opt("type"), Some("decision"));
}
#[test]
fn the_spanish_alias_resolves_to_the_english_name() {
let a = p("t --por reason --bloquea --padre 3 --forzar --luego x");
assert_eq!(a.opt("why"), Some("reason"));
assert!(a.has("blocks"));
assert_eq!(a.opt("parent"), Some("3"));
assert!(a.has("force"));
assert_eq!(a.opt("next"), Some("x"));
assert!(a.unknown(&["why", "blocks", "parent", "force", "next"]).is_empty());
assert_eq!(p("--inventada 1").unknown(&["why"]), vec!["inventada"]);
}
#[test]
fn no_alias_translated_itself_away() {
for es in [
"por", "tipo", "bloquea", "forzar", "luego", "padre", "cascada",
"rescatar", "reabrir", "todo", "razon", "alternativa",
] {
let en = canonical(es);
assert_ne!(en, es, "the alias for --{es} was lost");
assert_eq!(canonical(en), en, "--{en} is not canonical");
}
}
}