1use std::any::type_name;
76use std::collections::HashMap;
77use std::fmt::Display;
78use std::marker::PhantomData;
79use std::num::ParseFloatError;
80use std::str::ParseBoolError;
81
82#[derive(Clone, Debug)]
83pub enum Error {
84 ParseValue { value: String, arg: String },
85 UnknownArg(String),
86 Parse(String),
87}
88
89impl Display for Error {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Error::ParseValue { value, arg } => {
93 write!(f, "Cannot parse value: {} for argument: {}", value, arg)
94 }
95 Error::UnknownArg(s) => write!(f, "Unknown argument: {}", s),
96 Error::Parse(s) => f.write_str(s),
97 }
98 }
99}
100
101impl std::error::Error for Error {}
102
103#[derive(Debug, Clone, PartialEq)]
105pub enum Value {
106 Bool(bool),
107 Txt(String),
108 Num(f64),
109}
110
111impl Value {
112 pub fn parse_as_num(input_val: &str) -> Result<Self, ParseFloatError> {
114 let num = input_val.parse::<f64>()?;
115 Ok(Value::Num(num))
116 }
117
118 pub fn parse_as_bool(input_val: &str) -> Result<Self, ParseBoolError> {
120 let b = input_val.parse::<bool>()?;
121 Ok(Value::Bool(b))
122 }
123}
124
125pub trait FromValue: Sized {
126 fn from_value(v: &Value) -> Option<Self>;
127}
128
129impl FromValue for bool {
130 fn from_value(v: &Value) -> Option<Self> {
131 if let Value::Bool(b) = v {
132 Some(*b)
133 } else {
134 None
135 }
136 }
137}
138
139impl FromValue for f64 {
140 fn from_value(v: &Value) -> Option<Self> {
141 if let Value::Num(n) = v {
142 Some(*n)
143 } else {
144 None
145 }
146 }
147}
148
149impl FromValue for String {
150 fn from_value(v: &Value) -> Option<Self> {
151 if let Value::Txt(s) = v {
152 Some(s.clone())
153 } else {
154 None
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq)]
160pub struct Argument {
161 pub name: &'static str,
162 pub short_name: &'static str,
163 pub description: &'static str,
164 pub default: Value,
165 pub value: Value,
166 pub was_set: bool,
167}
168
169#[derive(Debug, Default, Clone, PartialEq)]
170pub struct TinyArgs {
171 pub program_name: String,
172 pub description: String,
173 pub help: String,
174 pub usage: String,
175 pub examples: Vec<String>,
176 pub args: HashMap<String, Argument>,
177 pub vargs: Vec<String>,
178}
179
180impl TinyArgs {
181 #[must_use]
183 pub fn new() -> Self {
184 let mut ta = Self {
185 program_name: String::new(),
186 description: String::new(),
187 help: String::new(),
188 usage: String::new(),
189 examples: vec![],
190 args: HashMap::new(),
191 vargs: vec![],
192 };
193
194 ta.usage = "[OPTIONS] [VARGS]...".to_owned();
195
196 let _ = ta.define_arg_bool("help", "h", false, "Display this help message");
197 ta
198 }
199
200 pub fn define_help_program_name(&mut self, name: &str) {
203 self.program_name = name.to_owned();
204 }
205
206 pub fn define_help_description(&mut self, description: &str) {
208 self.description = description.into();
209 }
210
211 pub fn define_help_usage(&mut self, usage: &str) {
214 self.usage = usage.into();
215 }
216
217 pub fn define_help_example(&mut self, examples: &str) {
221 self.examples.push(examples.to_string());
222 }
223
224 #[must_use]
226 pub fn define_arg_bool(
227 &mut self,
228 name: &'static str,
229 short_name: &'static str,
230 default_value: bool,
231 description: &'static str,
232 ) -> ArgHandle<bool> {
233 self.define_arg(name, short_name, Value::Bool(default_value), description);
234
235 ArgHandle {
236 name,
237 _p: PhantomData::<bool>,
238 }
239 }
240
241 #[must_use]
243 pub fn define_arg_num(
244 &mut self,
245 name: &'static str,
246 short_name: &'static str,
247 default_value: impl Into<f64>,
248 description: &'static str,
249 ) -> ArgHandle<f64> {
250 self.define_arg(
251 name,
252 short_name,
253 Value::Num(default_value.into()),
254 description,
255 );
256
257 ArgHandle {
258 name,
259 _p: PhantomData::<f64>,
260 }
261 }
262
263 #[must_use]
265 pub fn define_arg_txt(
266 &mut self,
267 name: &'static str,
268 short_name: &'static str,
269 default_value: &str,
270 description: &'static str,
271 ) -> ArgHandle<String> {
272 self.define_arg(
273 name,
274 short_name,
275 Value::Txt(default_value.into()),
276 description,
277 );
278
279 ArgHandle {
280 name,
281 _p: PhantomData::<String>,
282 }
283 }
284
285 fn define_arg(
286 &mut self,
287 name: &'static str,
288 short_name: &'static str,
289 default_value: Value,
290 description: &'static str,
291 ) {
292 let arg = Argument {
293 name,
294 short_name,
295 description,
296 value: default_value.clone(),
297 default: default_value,
298 was_set: false,
299 };
300 self.args.insert(name.to_owned(), arg);
301 }
302
303 #[must_use]
306 pub fn get<T: FromValue>(&self, arg_handle: ArgHandle<T>) -> T {
307 let val = self.get_val(arg_handle.name);
308
309 T::from_value(val).unwrap_or_else(|| {
310 panic!(
311 "type mismatch for argument {} when converting from {:?} to {}",
312 arg_handle.name,
313 val,
314 type_name::<T>()
315 )
316 })
317 }
318
319 pub fn parse_arguments(&mut self) -> Result<(), Error> {
331 let mut vargs: Vec<String> = vec![];
332 let mut args_iter = std::env::args().peekable();
333
334 let input_name = args_iter.next().ok_or_else(|| {
335 Error::Parse("Failed parsing first argument (executable path)".to_owned())
336 })?;
337
338 if self.program_name.is_empty() {
339 let split: Vec<&str> = input_name.split(|c| c == '\\' || c == '/').collect();
340
341 self.program_name = split
342 .last()
343 .map_or("program_name".to_owned(), |s| s.to_string())
344 }
345
346 for input in args_iter {
347 let trimmed_input = input.trim_start_matches('-').to_owned();
349 if trimmed_input.is_empty() {
350 return Err(Error::Parse("Invalid argument starting with -".to_owned()));
351 }
352
353 if trimmed_input == input {
356 vargs.push(input);
357 continue; }
359
360 let mut input_arg = trimmed_input;
361 let mut input_val = String::new();
362
363 if let Some((left, right)) = input_arg.split_once('=') {
367 if left.is_empty() {
368 return Err(Error::Parse(format!("Argument missing before ={}", right)));
369 }
370
371 if right.is_empty() {
372 return Err(Error::Parse(format!("Value missing after {}=", left)));
373 }
374 input_val = right.to_owned();
375 input_arg = left.to_owned();
376 }
377
378 if input_arg == "help" || input_arg == "h" {
380 self.print_help_and_exit(0);
381 }
382
383 let found_arg = self.args.iter_mut().find_map(|(_, a)| {
385 if input_arg == a.name || input_arg == a.short_name {
386 Some(a)
387 } else {
388 None
389 }
390 });
391
392 if let Some(argument) = found_arg {
393 argument.was_set = true;
394
395 if input_val.is_empty() {
397 if matches!(argument.value, Value::Bool(_)) {
398 argument.value = Value::Bool(true)
399 }
400 }
401 else {
403 argument.value = match argument.value {
404 Value::Txt(_) => Value::Txt(input_val),
405 Value::Num(_) => {
406 Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
407 value: input_val,
408 arg: input_arg,
409 })?
410 }
411 Value::Bool(_) => {
412 Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
413 value: input_val,
414 arg: input_arg,
415 })?
416 }
417 }
418 }
419 } else {
420 return Err(Error::UnknownArg(input_arg));
421 }
422 }
423
424 self.vargs = vargs;
425 Ok(())
426 }
427
428 fn get_arg(&self, name: &str) -> &Argument {
429 self.args
430 .get(name)
431 .unwrap_or_else(|| panic!("Could not find argument: {name}"))
432 }
433
434 fn get_val(&self, name: &str) -> &Value {
435 &self.get_arg(name).value
436 }
437
438 pub fn was_set<T>(&self, arg_handle: ArgHandle<T>) -> bool {
440 self.get_arg(arg_handle.name).was_set
441 }
442
443 pub fn get_vargs(&self) -> std::slice::Iter<'_, String> {
445 self.vargs.iter()
446 }
447
448 fn generate_help(&mut self) {
449 let examples = {
450 let mut res = String::new();
451 self.examples.iter().for_each(|s| {
452 res.push_str(&format!(" {program} {s}\n", program = self.program_name))
453 });
454
455 if !res.is_empty() {
456 res = "Examples:\n\n".to_owned() + &res;
457 }
458
459 res
460 };
461
462 self.help = format!(
463 "
464{description}
465
466Help:
467
468 Usage: {program} {usage}
469
470 Options:
471
472{arguments}
473
474{examples}",
475 description = self.description,
476 program = self.program_name,
477 usage = self.usage,
478 arguments = self.generate_args_help_list(),
479 );
480 }
481
482 fn generate_args_help_list(&self) -> String {
483 let mut args_help = String::new();
484
485 let mut keys: Vec<&String> = self.args.keys().collect();
486 keys.sort();
487
488 for arg in keys.iter().map(|&k| self.args.get(k).unwrap()) {
489 let name = "--".to_owned() + arg.name;
490
491 let short_name = {
492 if !arg.short_name.is_empty() {
493 "-".to_owned() + arg.short_name + ", "
494 } else {
495 "".to_string()
496 }
497 };
498
499 let mut default = match &arg.default {
500 Value::Bool(true) => "true".to_string(),
501 Value::Txt(s) => {
502 if s.is_empty() {
503 "".to_string()
504 } else {
505 s.clone()
506 }
507 }
508 Value::Num(n) => n.to_string(),
509 _ => "".to_string(),
510 };
511
512 let value = {
513 match arg.default {
514 Value::Bool(_) => "".to_string(),
515 _ => format!("=<{}>", arg.name),
516 }
517 };
518
519 if !default.is_empty() {
520 default = format!("[Default: {}]", default);
521 }
522
523 let line = &format!(
524 "{space:2}{short_name:>6}{name_and_val:25}{desc} {default}\n",
525 space = "",
526 name_and_val = name + &value,
527 desc = arg.description
528 );
529
530 args_help.push_str(line);
531 }
532
533 args_help
534 }
535
536 pub fn get_help_txt(&mut self) -> &str {
538 if self.help.is_empty() {
539 self.generate_help();
540 }
541
542 &self.help
543 }
544
545 pub fn print_help(&mut self) {
547 println!("{}", self.get_help_txt());
548 }
549
550 pub fn print_help_and_exit(&mut self, exit_code: i32) {
552 println!("{}", self.get_help_txt());
553 std::process::exit(exit_code);
554 }
555}
556
557#[derive(Debug, Clone, Copy, PartialEq)]
558pub struct ArgHandle<T> {
559 pub name: &'static str,
560 _p: PhantomData<T>,
561}