use std::collections::HashMap;
pub enum OptionPolicy {
Exact(usize),
AtLeast(usize),
AtMost(usize),
Finalize(),
FinalizeIgnore(),
}
pub struct OptionSpec {
abrev: char,
name: &'static str,
desc: &'static str,
required: bool,
policy: OptionPolicy,
}
impl OptionSpec {
pub fn new(
abrev: char,
name: &'static str,
desc: &'static str,
required: bool,
policy: OptionPolicy,
) -> Self {
Self {
abrev,
name,
desc,
required,
policy,
}
}
fn enforce(&self, values: Vec<String>) -> Result<Vec<String>, String> {
match self.policy {
OptionPolicy::Exact(n) => {
if values.len() != n {
return Err(format!(
"{} values supplied for option '{}', expected exactly {}",
values.len(),
self.name,
n,
));
};
Ok(values)
}
OptionPolicy::AtLeast(n) => {
if values.len() < n {
return Err(format!(
"{} values supplied for option '{}', expected at least {}",
values.len(),
self.name,
n,
));
};
Ok(values)
}
OptionPolicy::AtMost(n) => {
if values.len() > n {
return Err(format!(
"{} values supplied for option '{}', expected at most {}",
values.len(),
self.name,
n,
));
};
Ok(values)
}
OptionPolicy::Finalize() => Ok(values),
OptionPolicy::FinalizeIgnore() => Ok(values),
}
}
}
impl std::fmt::Display for OptionSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
" -{}, --{} {}\n {}\n\n",
self.abrev,
self.name,
if self.required { "[required]" } else { "" },
indent(self.desc, 8, ' '),
)
}
}
fn indent(string: &str, depth: usize, indent_ch: char) -> String {
let mut result = String::with_capacity(string.len());
let mut buf = String::with_capacity(256);
let mut indent: [u8; 4] = [b'\0'; 4];
let indent = indent_ch.encode_utf8(&mut indent).repeat(depth);
for ch in string.chars() {
buf.push(ch);
if ch == '\n' {
buf.push_str(&indent);
}
if buf.len() > buf.capacity() - 2 {
result.push_str(&buf);
buf.clear();
}
}
result.push_str(&buf);
buf.clear();
result
}
pub struct Config {
command: String,
parsed: HashMap<&'static str, Vec<String>>,
}
impl Config {
pub fn new_env(specs: &[OptionSpec]) -> Result<Config, String> {
Config::parse(std::env::args(), specs)
}
pub fn new(args: &[&str], specs: &[OptionSpec]) -> Result<Config, String> {
Config::parse(args.iter().map(|arg| arg.to_string()), specs)
}
pub fn generate_usage(
specs: &[OptionSpec],
list_required: bool,
list_unrequired: bool,
) -> String {
let mut required_string = String::new();
let mut unrequired_string = String::new();
if list_required {
required_string = specs
.iter()
.filter(|spec| spec.required)
.map(|spec| spec.to_string())
.collect();
}
if list_unrequired {
unrequired_string = specs
.iter()
.filter(|spec| !spec.required)
.map(|spec| spec.to_string())
.collect();
}
return required_string + &unrequired_string;
}
fn parse<'a>(
mut args: impl Iterator<Item = String>,
specs: &[OptionSpec],
) -> Result<Config, String> {
let command = args.next().unwrap_or_else(|| String::new());
let name_map: HashMap<&str, &OptionSpec> =
specs.iter().map(|spec| (spec.name, spec)).collect();
let abrev_map: HashMap<char, &OptionSpec> =
specs.iter().map(|spec| (spec.abrev, spec)).collect();
let mut parsed: HashMap<&'static str, Vec<String>> = HashMap::new();
let mut current_spec: &OptionSpec = match name_map.get("(unnamed)") {
Some(v) => v,
None => return Err("No specification for unnamed arguments found".to_string()),
};
let mut values = Vec::new();
let mut in_finalize = false;
for arg in args {
if !in_finalize && arg.starts_with("-") {
match current_spec.policy {
OptionPolicy::Finalize() | OptionPolicy::FinalizeIgnore() => {
in_finalize = true;
values.push(arg);
continue;
}
_ => (),
}
values = current_spec.enforce(values)?;
Self::insert_non_duplicate(&mut parsed, current_spec, values)?;
values = Vec::new();
if arg.starts_with("--") {
current_spec = match name_map.get(&arg[2..]) {
Some(spec) => {
if let Some(_) = parsed.get(spec.name) {
return Err(format!("Duplicate option '{}'", spec.name));
}
spec
}
None => return Err(format!("Invalid option {}", arg)),
};
}
else {
let options: Vec<_> = arg.chars().skip(1).collect();
for (index, option) in options.iter().enumerate() {
let spec = match abrev_map.get(&option) {
Some(spec) => spec,
None => return Err(format!("Invalid abbreviated option '{}'", option)),
};
if index == options.len() - 1 {
current_spec = spec;
break;
}
if let Some(_) = parsed.get(spec.name) {
return Err(format!("Duplicate option '{}'", spec.name));
}
Self::insert_non_duplicate(&mut parsed, spec, vec![])?;
}
}
continue;
}
values.push(arg);
}
values = current_spec.enforce(values)?;
Self::insert_non_duplicate(&mut parsed, current_spec, values)?;
if let OptionPolicy::FinalizeIgnore() = current_spec.policy {
} else {
for required in specs.iter().filter(|spec| spec.required) {
if let None = parsed.get(required.name) {
return Err(format!("Missing required option '{}'", required.name));
}
}
}
Ok(Config { command, parsed })
}
fn insert_non_duplicate(
map: &mut HashMap<&str, Vec<String>>,
spec: &OptionSpec,
values: Vec<String>,
) -> Result<(), String> {
match spec.policy {
OptionPolicy::Exact(0) => (),
_ => {
if let Some(_) = map.get(spec.name) {
return Err(format!("Duplicate option '{}'", spec.name));
}
}
}
map.insert(spec.name, values);
Ok(())
}
pub fn command(&self) -> &String {
&self.command
}
pub fn option(&self, name: &str) -> Option<&[String]> {
match self.parsed.get(name) {
Some(values) => Some(values),
None => None,
}
}
}