use std::collections::HashMap;
#[derive(Debug, Default, Clone)]
pub struct Flags {
inner: HashMap<String, Option<String>>,
}
impl Flags {
pub fn nullish(&self, name: &str) -> Option<&str> {
self.inner.get(name).and_then(|value| value.as_deref())
}
pub fn truthy(&self, name: &str) -> Option<&str> {
self.nullish(name).filter(|value| !value.is_empty())
}
}
pub fn parse_flags(args: &[String]) -> Flags {
let mut flags: HashMap<String, Option<String>> = HashMap::new();
let mut index = 0;
while index < args.len() {
let arg = &args[index];
if !arg.starts_with("--") {
index += 1;
continue;
}
let body = &arg[2..];
let (key, inline) = match body.split_once('=') {
Some((key, rest)) => (key, Some(rest.split('=').next().unwrap_or("").to_string())),
None => (body, None),
};
let value = match &inline {
Some(value) => Some(value.clone()),
None => args.get(index + 1).cloned(),
};
flags.insert(to_camel_case(key), value);
if inline.as_deref().map_or(true, str::is_empty) {
index += 1;
}
index += 1;
}
Flags { inner: flags }
}
fn to_camel_case(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(character) = chars.next() {
if character == '-' {
if let Some(next) = chars.peek().copied().filter(char::is_ascii_lowercase) {
chars.next();
out.extend(next.to_uppercase());
continue;
}
}
out.push(character);
}
out
}
pub fn positional_args(args: &[String]) -> Vec<String> {
args.iter()
.enumerate()
.filter(|(index, arg)| {
if arg.starts_with("--") {
return false;
}
match index.checked_sub(1).and_then(|previous| args.get(previous)) {
Some(previous) => !previous.starts_with("--") || previous.contains('='),
None => true,
}
})
.map(|(_, arg)| arg.clone())
.collect()
}
pub fn positional_args_with_value_flags(args: &[String], value_flags: &[&str]) -> Vec<String> {
let mut positional = Vec::new();
let mut index = 0;
while index < args.len() {
let argument = &args[index];
if !argument.starts_with("--") {
positional.push(argument.clone());
index += 1;
continue;
}
let flag = argument.split('=').next().unwrap_or("");
if !argument.contains('=') && value_flags.contains(&flag) {
index += 1;
}
index += 1;
}
positional
}
#[cfg(test)]
mod tests {
use super::*;
fn args(raw: &[&str]) -> Vec<String> {
raw.iter().map(|value| (*value).to_string()).collect()
}
#[test]
fn a_second_equals_sign_is_dropped_the_way_js_split_drops_it() {
let flags = parse_flags(&args(&["--url=postgres://h/db?a=b"]));
assert_eq!(flags.nullish("url"), Some("postgres://h/db?a"));
}
#[test]
fn an_empty_inline_value_still_swallows_the_next_argument() {
let flags = parse_flags(&args(&["--command=", "--port", "3000"]));
assert_eq!(flags.nullish("command"), Some(""));
assert_eq!(flags.truthy("command"), None);
assert_eq!(flags.nullish("port"), None);
}
#[test]
fn a_trailing_flag_has_no_value_at_all() {
let flags = parse_flags(&args(&["--command"]));
assert_eq!(flags.nullish("command"), None);
}
#[test]
fn only_a_lowercase_letter_is_lifted_over_a_dash() {
assert_eq!(to_camel_case("compose-service"), "composeService");
assert_eq!(to_camel_case("a-b-c"), "aBC");
assert_eq!(to_camel_case("a--b"), "a-B");
assert_eq!(to_camel_case("no-Check"), "no-Check");
}
#[test]
fn the_git_reader_drops_a_positional_after_a_valueless_flag() {
assert_eq!(
positional_args(&args(&["--force", "name"])),
Vec::<String>::new()
);
assert_eq!(positional_args(&args(&["name", "--force"])), vec!["name"]);
assert_eq!(positional_args(&args(&["--as=x", "name"])), vec!["name"]);
}
#[test]
fn the_db_reader_keeps_a_positional_after_a_boolean_flag() {
let value_flags = ["--engine", "--url"];
assert_eq!(
positional_args_with_value_flags(&args(&["--replace", "name"]), &value_flags),
vec!["name"]
);
assert_eq!(
positional_args_with_value_flags(
&args(&["--engine", "postgres", "name"]),
&value_flags
),
vec!["name"]
);
}
}