use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
pub struct ArgSpec {
args: HashMap<String, ArgType>,
}
type ArgIter = std::iter::Peekable<std::iter::Skip<std::env::Args>>;
impl ArgSpec {
pub fn build() -> ArgSpecBuilder {
ArgSpecBuilder {
args: HashMap::new(),
err: None,
}
}
pub fn has_arg(&self, name: impl Into<String>, ty: ArgType) -> bool {
if let Some(_t) = self.args.get(&name.into()) {
matches!(ty, _t)
} else {
false
}
}
pub fn parse(&self) -> Result<Args> {
let mut bools: HashMap<String, bool> = HashMap::new();
let mut ints: HashMap<String, i64> = HashMap::new();
let mut uints: HashMap<String, u64> = HashMap::new();
let mut floats: HashMap<String, f64> = HashMap::new();
let mut strs: HashMap<String, String> = HashMap::new();
let mut bool_arrays: HashMap<String, Box<[bool]>> = HashMap::new();
let mut int_arrays: HashMap<String, Box<[i64]>> = HashMap::new();
let mut uint_arrays: HashMap<String, Box<[u64]>> = HashMap::new();
let mut float_arrays: HashMap<String, Box<[f64]>> = HashMap::new();
let mut str_arrays: HashMap<String, Box<[String]>> = HashMap::new();
let mut free_args: Vec<String> = Vec::new();
let mut args = std::env::args().skip(1).peekable();
while let Some(arg) = args.next() {
let mut chars = arg.chars().peekable();
if chars.peek() == Some(&'-') {
chars.next();
if chars.peek() == Some(&'-') {
chars.next();
}
let arg_name: String = chars.collect();
let arg_type = *self
.args
.get(&arg_name)
.ok_or(Error::UnknownArgument(arg_name.clone()))?;
match arg_type {
ArgType::Boolean => parse_bool(arg_name, &mut args, &mut bools)?,
ArgType::Integer => parse_arg(arg_name, arg_type, &mut args, &mut ints)?,
ArgType::UInteger => parse_arg(arg_name, arg_type, &mut args, &mut uints)?,
ArgType::Float => parse_arg(arg_name, arg_type, &mut args, &mut floats)?,
ArgType::String => parse_arg(arg_name, arg_type, &mut args, &mut strs)?,
ArgType::BooleanArray(n) => {
parse_array_arg(arg_name, arg_type, n, &mut args, &mut bool_arrays)?
}
ArgType::IntegerArray(n) => {
parse_array_arg(arg_name, arg_type, n, &mut args, &mut int_arrays)?
}
ArgType::UIntegerArray(n) => {
parse_array_arg(arg_name, arg_type, n, &mut args, &mut uint_arrays)?
}
ArgType::FloatArray(n) => {
parse_array_arg(arg_name, arg_type, n, &mut args, &mut float_arrays)?
}
ArgType::StringArray(n) => {
parse_array_arg(arg_name, arg_type, n, &mut args, &mut str_arrays)?
}
}
} else {
free_args.push(chars.collect());
}
}
Ok(Args::new(
bools,
ints,
uints,
floats,
strs,
bool_arrays,
int_arrays,
uint_arrays,
float_arrays,
str_arrays,
free_args,
))
}
}
trait HashMapExt<V> {
fn insert_arg(&mut self, name: String, v: V) -> Result<()>;
}
impl<V> HashMapExt<V> for HashMap<String, V> {
fn insert_arg(&mut self, name: String, v: V) -> Result<()> {
if self.contains_key(&name) {
Err(Error::RepeatedArgument(name))
} else {
self.insert(name, v);
Ok(())
}
}
}
trait StringExt {
fn is_valid_arg_name(&self) -> bool;
}
impl StringExt for String {
fn is_valid_arg_name(&self) -> bool {
!(self.starts_with('-')
|| self.contains(|c: char| c.is_whitespace())
|| self.contains(|c: char| c.is_control()))
}
}
fn parse_arg<T: std::str::FromStr>(
arg_name: String,
arg_type: ArgType,
args: &mut ArgIter,
dict: &mut HashMap<String, T>,
) -> Result<()> {
let arg_str = args
.next()
.ok_or(Error::MissingParameter(arg_name.clone(), arg_type))?;
dict.insert_arg(
arg_name.clone(),
arg_str.parse::<T>().or(Err(Error::InvalidParameter(
arg_name.clone(),
arg_type,
arg_str,
)))?,
)?;
Ok(())
}
fn parse_bool(
arg_name: String,
args: &mut ArgIter,
bools: &mut HashMap<String, bool>,
) -> Result<()> {
let value = if args.peek() == Some(&"false".to_string()) {
args.next();
false
} else if args.peek() == Some(&"true".to_string()) {
args.next();
true
} else {
true
};
bools.insert_arg(arg_name, value)
}
fn parse_array_arg<T: std::str::FromStr>(
arg_name: String,
arg_type: ArgType,
array_size: usize,
args: &mut ArgIter,
dict: &mut HashMap<String, Box<[T]>>,
) -> Result<()> {
let mut params: Vec<T> = Vec::with_capacity(array_size);
for _ in 0..array_size {
let arg_str = args
.next()
.ok_or(Error::MissingParameter(arg_name.clone(), arg_type))?;
params.push(arg_str.parse::<T>().or(Err(Error::InvalidParameter(
arg_name.clone(),
arg_type,
arg_str,
)))?);
}
dict.insert_arg(arg_name, params.into_boxed_slice())
}
impl std::fmt::Debug for ArgSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map().entries(self.args.iter()).finish()
}
}
#[derive(Copy, Clone, Debug)]
pub enum ArgType {
Boolean,
BooleanArray(usize),
Integer,
IntegerArray(usize),
UInteger,
UIntegerArray(usize),
Float,
FloatArray(usize),
String,
StringArray(usize),
}
pub struct ArgSpecBuilder {
args: HashMap<String, ArgType>,
err: Option<Error>,
}
impl ArgSpecBuilder {
pub fn done(self) -> Result<ArgSpec> {
if let Some(err) = self.err {
Err(err)
} else {
Ok(ArgSpec { args: self.args })
}
}
pub fn parse(self) -> Result<Args> {
self.done()?.parse()
}
pub fn arg(mut self, name: impl Into<String>, ty: ArgType) -> ArgSpecBuilder {
let name_str = name.into();
if self.err.is_none() && !self.args.contains_key(&name_str) {
if name_str.is_valid_arg_name() {
self.args.insert(name_str, ty);
} else {
self.err = Some(Error::InvalidArgumentName(name_str));
}
} else {
self.err = Some(Error::RedeclaredArgument(name_str));
}
self
}
pub fn boolean(self, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::Boolean)
}
pub fn integer(self, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::Integer)
}
pub fn uinteger(self, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::UInteger)
}
pub fn float(self, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::Float)
}
pub fn string(self, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::String)
}
pub fn boolean_array(self, size: usize, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::BooleanArray(size))
}
pub fn integer_array(self, size: usize, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::IntegerArray(size))
}
pub fn uinteger_array(self, size: usize, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::UIntegerArray(size))
}
pub fn float_array(self, size: usize, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::FloatArray(size))
}
pub fn string_array(self, size: usize, name: impl Into<String>) -> ArgSpecBuilder {
self.arg(name, ArgType::StringArray(size))
}
}
#[macro_export]
macro_rules! arg_spec {
($($arg_name:ident : $arg_type:tt),*$(,)?) => {{
let b = $crate::ArgSpec::build();
$(let b = arg_spec!(b, $arg_name, $arg_type);)*
b.done().unwrap()
}};
($builder:expr, $arg_name:ident, bool) => {
$builder.boolean(stringify!($arg_name))
};
($builder:expr, $arg_name:ident, i64) => {
$builder.integer(stringify!($arg_name))
};
($builder:expr, $arg_name:ident, u64) => {
$builder.uinteger(stringify!($arg_name))
};
($builder:expr, $arg_name:ident, f64) => {
$builder.float(stringify!($arg_name))
};
($builder:expr, $arg_name:ident, String) => {
$builder.string(stringify!($arg_name))
};
($builder:expr, $arg_name:ident, [bool; $array_size:literal]) => {
$builder.boolean_array($array_size, stringify!($arg_name))
};
($builder:expr, $arg_name:ident, [i64; $array_size:literal]) => {
$builder.integer_array($array_size, stringify!($arg_name))
};
($builder:expr, $arg_name:ident, [u64; $array_size:literal]) => {
$builder.uinteger_array($array_size, stringify!($arg_name))
};
($builder:expr, $arg_name:ident, [f64; $array_size:literal]) => {
$builder.float_array($array_size, stringify!($arg_name))
};
($builder:expr, $arg_name:ident, [String; $array_size:literal]) => {
$builder.string_array($array_size, stringify!($arg_name))
};
($builder:expr, $arg_name:ident, $arg_type:tt) => {
compile_error!(concat!("`", stringify!($arg_name), "` cannot be of type `", stringify!($arg_type), "` because `ArgSpec` doesn't support it"));
};
}
#[derive(Debug)]
pub struct Args {
bools: HashMap<String, bool>,
ints: HashMap<String, i64>,
uints: HashMap<String, u64>,
floats: HashMap<String, f64>,
strs: HashMap<String, String>,
bool_arrays: HashMap<String, Box<[bool]>>,
int_arrays: HashMap<String, Box<[i64]>>,
uint_arrays: HashMap<String, Box<[u64]>>,
float_arrays: HashMap<String, Box<[f64]>>,
str_arrays: HashMap<String, Box<[String]>>,
free_args: Vec<String>,
}
impl Args {
pub(crate) fn new(
bools: HashMap<String, bool>,
ints: HashMap<String, i64>,
uints: HashMap<String, u64>,
floats: HashMap<String, f64>,
strs: HashMap<String, String>,
bool_arrays: HashMap<String, Box<[bool]>>,
int_arrays: HashMap<String, Box<[i64]>>,
uint_arrays: HashMap<String, Box<[u64]>>,
float_arrays: HashMap<String, Box<[f64]>>,
str_arrays: HashMap<String, Box<[String]>>,
free_args: Vec<String>,
) -> Self {
Args {
bools,
ints,
uints,
floats,
strs,
bool_arrays,
int_arrays,
uint_arrays,
float_arrays,
str_arrays,
free_args,
}
}
pub fn is_set(&self, name: impl Into<String>) -> bool {
let n = name.into();
self
.bools
.keys()
.chain(self.ints.keys())
.chain(self.uints.keys())
.chain(self.strs.keys())
.chain(self.bool_arrays.keys())
.chain(self.int_arrays.keys())
.chain(self.uint_arrays.keys())
.chain(self.str_arrays.keys())
.find(|&k| *k == n)
.is_some()
}
pub fn boolean(&self, name: impl Into<String>) -> Option<&bool> {
self.bools.get(&name.into())
}
pub fn integer(&self, name: impl Into<String>) -> Option<&i64> {
self.ints.get(&name.into())
}
pub fn uinteger(&self, name: impl Into<String>) -> Option<&u64> {
self.uints.get(&name.into())
}
pub fn float(&self, name: impl Into<String>) -> Option<&f64> {
self.floats.get(&name.into())
}
pub fn string(&self, name: impl Into<String>) -> Option<&String> {
self.strs.get(&name.into())
}
pub fn boolean_array(&self, name: impl Into<String>) -> Option<&[bool]> {
Some(self.bool_arrays.get(&name.into())?.as_ref())
}
pub fn integer_array(&self, name: impl Into<String>) -> Option<&[i64]> {
Some(self.int_arrays.get(&name.into())?.as_ref())
}
pub fn uinteger_array(&self, name: impl Into<String>) -> Option<&[u64]> {
Some(self.uint_arrays.get(&name.into())?.as_ref())
}
pub fn float_array(&self, name: impl Into<String>) -> Option<&[f64]> {
Some(self.float_arrays.get(&name.into())?.as_ref())
}
pub fn string_array(&self, name: impl Into<String>) -> Option<&[String]> {
Some(self.str_arrays.get(&name.into())?.as_ref())
}
pub fn free_args(&self) -> &Vec<String> {
&self.free_args
}
}
pub enum Error {
UnknownArgument(String),
MissingParameter(String, ArgType),
InvalidParameter(String, ArgType, String),
RedeclaredArgument(String),
RepeatedArgument(String),
InvalidArgumentName(String),
}
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::UnknownArgument(name) => write!(f, "Unknown argument {}.", name),
Error::MissingParameter(name, ty) => write!(
f,
"Missing {:?} parameter value for argument '{}'.",
ty, name
),
Error::InvalidParameter(name, ty, given) => write!(
f,
"Expected {:?} value for argument '{}' but found '{}'.",
ty, name, given
),
Error::RedeclaredArgument(name) => {
write!(f, "Argument '{}' has already been declared.", name)
}
Error::RepeatedArgument(name) => {
write!(f, "Argument '{}' has already been assigned a value.", name)
}
Error::InvalidArgumentName(name) => write!(
f,
"'{}' is not a valid argument name. Argument names must not begin with a dash or contain any whitespace or control characters.",
name),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_builder() -> Result<()> {
let spec = arg_spec! {
b: bool,
n: i64,
u: u64,
name: String,
};
assert!(spec.has_arg("b", ArgType::Boolean));
assert!(spec.has_arg("n", ArgType::Integer));
assert!(spec.has_arg("u", ArgType::UInteger));
assert!(spec.has_arg("name", ArgType::String));
assert!(!spec.has_arg("none", ArgType::Boolean));
Ok(())
}
#[test]
fn parse() -> Result<()> {
let args = arg_spec! {
b: bool,
n: i64,
u: u64,
name: String,
}
.parse()?;
assert_eq!(args.free_args().len(), 0);
Ok(())
}
}