#![deny(missing_docs)]
use std::collections::BTreeMap;
use std::ffi::OsString;
#[derive(Default, Debug)]
pub struct Minimist(pub BTreeMap<String, Vec<OsString>>);
impl std::ops::Deref for Minimist {
type Target = BTreeMap<String, Vec<OsString>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Minimist {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Minimist {
pub const POS: &'static str = "-";
pub const PASS: &'static str = "--";
pub fn parse<S, I>(arg_list: I) -> Self
where
S: Into<OsString>,
I: IntoIterator<Item = S>,
{
let mut map: BTreeMap<String, Vec<OsString>> = BTreeMap::new();
let mut last_flag = None;
let mut iter = arg_list.into_iter();
for tok in Tok::parse(&mut iter) {
match tok {
Tok::Key(k) => {
last_flag = Some(k.clone());
map.entry(k).or_default();
}
Tok::Val(v) => {
if let Some(f) = last_flag.take() {
map.entry(f).or_default().push(v);
} else {
map.entry(Self::POS.into()).or_default().push(v);
}
}
}
}
for v in iter {
map.entry(Self::PASS.into()).or_default().push(v.into());
}
Self(map)
}
pub fn as_flag(&self, arg: &str) -> bool {
self.as_list(arg).is_some()
}
pub fn as_one(&self, arg: &str) -> Option<&OsString> {
self.as_list(arg).and_then(|mut l| l.next())
}
pub fn as_one_path(&self, arg: &str) -> Option<&std::path::Path> {
self.as_one(arg).map(|p| p.as_os_str().as_ref())
}
pub fn as_list(
&self,
arg: &str,
) -> Option<impl Iterator<Item = &OsString>> {
self.get(arg).map(|l| l.iter())
}
pub fn as_list_path(
&self,
arg: &str,
) -> Option<impl Iterator<Item = &std::path::Path>> {
self.as_list(arg).map(|i| i.map(|p| p.as_os_str().as_ref()))
}
pub fn to_one_str(&self, arg: &str) -> Option<std::borrow::Cow<'_, str>> {
self.as_one(arg).map(|s| s.to_string_lossy())
}
pub fn to_list_str(
&self,
arg: &str,
) -> Option<impl Iterator<Item = std::borrow::Cow<'_, str>>> {
self.as_list(arg).map(|i| i.map(|s| s.to_string_lossy()))
}
pub fn set_default(
&mut self,
arg: impl std::fmt::Display,
val: impl Into<OsString>,
) {
let arg = self.0.entry(arg.to_string()).or_default();
if arg.is_empty() {
arg.push(val.into());
}
}
pub fn set_default_env(
&mut self,
arg: impl std::fmt::Display,
env: impl AsRef<std::ffi::OsStr>,
) {
if let Some(env) = std::env::var_os(env) {
self.set_default(arg, env);
}
}
}
enum Tok {
Key(String),
Val(OsString),
}
impl Tok {
fn parse<S, I>(arg_iter: &mut I) -> Vec<Tok>
where
S: Into<OsString>,
I: Iterator<Item = S>,
{
let mut out = Vec::new();
for arg in arg_iter {
let arg = arg.into();
let as_str = arg.to_string_lossy();
if as_str == "--" {
return out;
}
if as_str.len() < 2 || !as_str.starts_with('-') {
if !arg.is_empty() {
out.push(Tok::Val(arg));
}
continue;
}
if as_str.starts_with("--") {
out.push(Tok::Key(as_str.trim_start_matches("--").to_string()));
continue;
}
for c in as_str.trim_start_matches('-').chars() {
out.push(Tok::Key(c.to_string()));
}
}
out
}
}
#[cfg(test)]
mod test {
use super::*;
const FIX: &[(&str, &[&str], &str)] = &[
(
"single dashes",
&["-", "-ab", "-", "--c", "-", "-", "--", "-"],
r#"Minimist({"-": ["-", "-"], "--": ["-"], "a": [], "b": ["-"], "c": ["-"]})"#,
),
(
"sane repeats",
&["-s", "1", "-s", "-s", "-s", "2", "-s"],
r#"Minimist({"s": ["1", "2"]})"#,
),
(
"newlines ( not sure this is a good thing :/ )",
&["--a\nb", "a\nb"],
r#"Minimist({"a\nb": ["a\nb"]})"#,
),
(
"whitespace",
&["-a", " ", "-b", "\t"],
r#"Minimist({"a": [" "], "b": ["\t"]})"#,
),
];
#[test]
fn fixtures() {
for (name, input, output) in FIX.iter() {
let result = format!("{:?}", Minimist::parse(input.iter()));
assert_eq!(*output, result, "fixture({name})");
}
}
}