use std::any::type_name;
use std::collections::HashMap;
use std::fmt::Display;
use std::marker::PhantomData;
use std::num::ParseFloatError;
use std::str::ParseBoolError;
#[derive(Clone, Debug)]
pub enum Error {
ParseValue { value: String, arg: String },
UnknownOpt(String),
UnknownCmd(String),
Parse(String),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::ParseValue { value, arg } => {
write!(f, "Cannot parse value: {} for argument: {}", value, arg)
}
Error::UnknownOpt(s) => write!(f, "Unknown option: {}", s),
Error::UnknownCmd(s) => write!(f, "Unknown command: {}", s),
Error::Parse(s) => f.write_str(s),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Bool(bool),
Num(f64),
Txt(String),
}
impl Value {
pub fn parse_as_bool(input_val: &str) -> Result<Self, ParseBoolError> {
let b = input_val.parse::<bool>()?;
Ok(Value::Bool(b))
}
pub fn parse_as_num(input_val: &str) -> Result<Self, ParseFloatError> {
let num = input_val.parse::<f64>()?;
Ok(Value::Num(num))
}
}
pub trait FromValue: Sized {
fn from_value(v: &Value) -> Option<Self>;
}
impl FromValue for bool {
fn from_value(v: &Value) -> Option<Self> {
if let Value::Bool(b) = v {
Some(*b)
} else {
None
}
}
}
impl FromValue for f64 {
fn from_value(v: &Value) -> Option<Self> {
if let Value::Num(n) = v {
Some(*n)
} else {
None
}
}
}
impl FromValue for String {
fn from_value(v: &Value) -> Option<Self> {
if let Value::Txt(s) = v {
Some(s.clone())
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Argument {
pub name: &'static str,
pub short_name: &'static str,
pub description: &'static str,
pub default: Value,
pub value: Value,
pub was_set: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
pub name: &'static str,
pub description: &'static str,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct TinyArgs {
pub program_name: String,
pub description: String,
pub help: String,
pub usage: String,
pub examples: Vec<String>,
pub cmds: HashMap<String, Command>,
pub opts: HashMap<String, Argument>,
pub va_args: Vec<String>,
pub active_cmd: Option<Command>,
}
impl TinyArgs {
#[must_use]
pub fn new() -> Self {
let mut res = Self {
program_name: String::new(),
description: String::new(),
help: String::new(),
usage: String::new(),
examples: vec![],
cmds: HashMap::new(),
opts: HashMap::new(),
va_args: vec![],
active_cmd: None,
};
let _ = res.define_option_bool("help", "h", false, "Display this help message");
res
}
pub fn define_help_program_name(&mut self, name: &str) {
self.program_name = name.to_owned();
}
pub fn define_help_description(&mut self, description: &str) {
self.description = description.into();
}
pub fn define_help_usage(&mut self, usage: &str) {
self.usage = usage.into();
}
pub fn define_help_example(&mut self, examples: &str) {
self.examples.push(examples.to_string());
}
#[must_use]
pub fn define_command(&mut self, name: &'static str, description: &'static str) -> CmdHandle {
let arg = Command { name, description };
self.cmds.insert(name.to_owned(), arg);
CmdHandle { name }
}
#[must_use]
pub fn define_option_bool(
&mut self,
name: &'static str,
short_name: &'static str,
default_value: bool,
description: &'static str,
) -> OptHandle<bool> {
self.define_argument(name, short_name, Value::Bool(default_value), description);
OptHandle {
name,
_p: PhantomData::<bool>,
}
}
#[must_use]
pub fn define_option_num(
&mut self,
name: &'static str,
short_name: &'static str,
default_value: impl Into<f64>,
description: &'static str,
) -> OptHandle<f64> {
self.define_argument(
name,
short_name,
Value::Num(default_value.into()),
description,
);
OptHandle {
name,
_p: PhantomData::<f64>,
}
}
#[must_use]
pub fn define_option_txt(
&mut self,
name: &'static str,
short_name: &'static str,
default_value: &str,
description: &'static str,
) -> OptHandle<String> {
self.define_argument(
name,
short_name,
Value::Txt(default_value.into()),
description,
);
OptHandle {
name,
_p: PhantomData::<String>,
}
}
fn define_argument(
&mut self,
name: &'static str,
short_name: &'static str,
default_value: Value,
description: &'static str,
) {
let arg = Argument {
name,
short_name,
description,
value: default_value.clone(),
default: default_value,
was_set: false,
};
self.opts.insert(name.to_owned(), arg);
}
#[must_use]
pub fn get_option<T: FromValue>(&self, opt_handle: OptHandle<T>) -> T {
let val = &self.find_argument(opt_handle.name).value;
T::from_value(&val).unwrap_or_else(|| {
panic!(
"type mismatch for argument {} when converting from {:?} to {}",
opt_handle.name,
val,
type_name::<T>()
)
})
}
pub fn command(&self) -> CmdHandle {
let name = self.active_cmd.as_ref().map_or_else(|| "", |c| c.name);
if name.is_empty() {
return CmdHandle::NONE;
}
CmdHandle { name }
}
pub fn parse_arguments(&mut self) -> Result<(), Error> {
let args = std::env::args().collect();
self.parse_arguments_from_vec(args)
}
pub fn parse_arguments_from_vec(&mut self, args: Vec<String>) -> Result<(), Error> {
let mut args_iter = args.iter();
let input_name = args_iter.next().ok_or_else(|| {
Error::Parse("Failed parsing first argument (executable path)".to_owned())
})?;
if self.program_name.is_empty() {
let split: Vec<&str> = input_name.split(|c| c == '\\' || c == '/').collect();
self.program_name = split
.last()
.map_or("program_name".to_owned(), |s| s.to_string())
}
for input in args_iter {
let trimmed_input = input.trim_start_matches('-').to_owned();
if trimmed_input.is_empty() {
return Err(Error::Parse("Invalid argument starting with -".to_owned()));
}
if &trimmed_input == input {
if let Some(cmd) = self.cmds.get_mut(&trimmed_input)
&& self.active_cmd.is_none()
{
self.active_cmd = Some(cmd.clone());
} else if self.active_cmd.is_some() || self.cmds.is_empty() {
self.va_args.push(trimmed_input); } else {
return Err(Error::UnknownCmd(trimmed_input));
}
continue; }
let mut input_arg = trimmed_input;
let mut input_val = String::new();
if let Some((left, right)) = input_arg.split_once('=') {
if left.is_empty() {
return Err(Error::Parse(format!("Argument missing before ={}", right)));
}
if right.is_empty() {
return Err(Error::Parse(format!("Value missing after {}=", left)));
}
input_val = right.to_owned();
input_arg = left.to_owned();
}
if input_arg == "help" || input_arg == "h" {
self.print_help_and_exit(0);
}
let found_arg = self.opts.iter_mut().find_map(|(_, a)| {
if input_arg == a.name || input_arg == a.short_name {
Some(a)
} else {
None
}
});
if let Some(argument) = found_arg {
argument.was_set = true;
if input_val.is_empty() {
if matches!(argument.value, Value::Bool(_)) {
argument.value = Value::Bool(true)
}
}
else {
argument.value = match argument.value {
Value::Txt(_) => Value::Txt(input_val),
Value::Num(_) => {
Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
value: input_val,
arg: input_arg,
})?
}
Value::Bool(_) => {
Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
value: input_val,
arg: input_arg,
})?
}
}
}
} else {
return Err(Error::UnknownOpt(input_arg));
}
}
Ok(())
}
fn find_argument(&self, name: &str) -> &Argument {
self.opts
.get(name)
.unwrap_or_else(|| panic!("Could not find argument: {name}"))
}
pub fn was_option_set<T>(&self, arg_handle: OptHandle<T>) -> bool {
self.find_argument(arg_handle.name).was_set
}
pub fn get_va_args(&self) -> std::slice::Iter<'_, String> {
self.va_args.iter()
}
fn generate_help(&mut self) {
if self.usage.is_empty() {
self.usage = {
let mut options = "";
let mut commands = "";
if !self.opts.is_empty() {
options = "[OPTIONS] "
};
if !self.cmds.is_empty() {
commands = "[COMMANDS] "
};
format!("{}{}[ARGS]...", options, commands)
}
}
let examples = {
let mut res = String::new();
if !self.examples.is_empty() {
res = "\nExamples:\n\n".to_owned() + &res;
self.examples.iter().for_each(|s| {
res.push_str(&format!(" {program} {s}\n", program = self.program_name))
});
}
res
};
self.help = format!(
"
{description}
Help:
Usage: {program} {usage}
{commands} {arguments} {examples}
",
description = self.description,
program = self.program_name,
usage = self.usage,
commands = if !self.cmds.is_empty() {
"\n Commands:\n\n".to_string() + &self.generate_cmds_help_list()
} else {
"".to_string()
},
arguments = if !self.opts.is_empty() {
"\n Options:\n\n".to_string() + &self.generate_args_help_list()
} else {
"".to_string()
},
);
}
fn generate_args_help_list(&self) -> String {
let mut args_help = String::new();
let mut keys: Vec<&String> = self.opts.keys().collect();
keys.sort();
for arg in keys.iter().map(|&k| self.opts.get(k).unwrap()) {
let name = "--".to_owned() + arg.name;
let short_name = {
if !arg.short_name.is_empty() {
"-".to_owned() + arg.short_name + ", "
} else {
"".to_string()
}
};
let mut default = match &arg.default {
Value::Bool(true) => "true".to_string(),
Value::Txt(s) => {
if s.is_empty() {
"".to_string()
} else {
s.clone()
}
}
Value::Num(n) => n.to_string(),
_ => "".to_string(),
};
let value = {
match arg.default {
Value::Bool(_) => "".to_string(),
_ => format!("=<{}>", arg.name),
}
};
if !default.is_empty() {
default = format!("[Default: {}]", default);
}
let line = &format!(
"{space:2}{short_name:>6}{name_and_val:23}{desc} {default}\n",
space = "",
name_and_val = name + &value,
desc = arg.description
);
args_help.push_str(line);
}
args_help
}
fn generate_cmds_help_list(&self) -> String {
let mut cmds_help = String::new();
let mut keys: Vec<&String> = self.cmds.keys().collect();
keys.sort();
for cmd in keys.iter().map(|&k| self.cmds.get(k).unwrap()) {
let line = &format!(
"{space:6}{name:25}{desc}\n",
space = "",
name = cmd.name,
desc = cmd.description
);
cmds_help.push_str(line);
}
cmds_help
}
pub fn get_help_text(&mut self) -> &str {
if self.help.is_empty() {
self.generate_help();
}
&self.help
}
pub fn print_help(&mut self) {
println!("{}", self.get_help_text());
}
pub fn print_help_and_exit(&mut self, exit_code: i32) {
println!("{}", self.get_help_text());
std::process::exit(exit_code);
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct OptHandle<T> {
name: &'static str,
_p: PhantomData<T>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct CmdHandle {
name: &'static str,
}
impl CmdHandle {
const NONE: Self = CmdHandle { name: "" };
}