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 starting argument (executable path)".to_owned())
336 })?;
337
338 if self.program_name.is_empty() {
339 self.program_name = input_name
340 }
341
342 for input in args_iter {
343 let trimmed_input = input.trim_start_matches('-').to_owned();
345 if trimmed_input.is_empty() {
346 return Err(Error::Parse("Invalid argument starting with -".to_owned()));
347 }
348
349 if trimmed_input == input {
352 vargs.push(input);
353 continue; }
355
356 let mut input_arg = trimmed_input;
357 let mut input_val = String::new();
358
359 if let Some((left, right)) = input_arg.split_once('=') {
363 if left.is_empty() {
364 return Err(Error::Parse(format!("Argument missing before ={}", right)));
365 }
366
367 if right.is_empty() {
368 return Err(Error::Parse(format!("Value missing after {}=", left)));
369 }
370 input_val = right.to_owned();
371 input_arg = left.to_owned();
372 }
373
374 if input_arg == "help" || input_arg == "h" {
376 self.print_help_and_exit(0);
377 }
378
379 let found_arg = self.args.iter_mut().find_map(|(_, a)| {
381 if input_arg == a.name || input_arg == a.short_name {
382 Some(a)
383 } else {
384 None
385 }
386 });
387
388 if let Some(argument) = found_arg {
389 argument.was_set = true;
390
391 if input_val.is_empty() {
393 if matches!(argument.value, Value::Bool(_)) {
394 argument.value = Value::Bool(true)
395 }
396 }
397 else {
399 argument.value = match argument.value {
400 Value::Txt(_) => Value::Txt(input_val),
401 Value::Num(_) => {
402 Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
403 value: input_val,
404 arg: input_arg,
405 })?
406 }
407 Value::Bool(_) => {
408 Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
409 value: input_val,
410 arg: input_arg,
411 })?
412 }
413 }
414 }
415 } else {
416 return Err(Error::UnknownArg(input_arg));
417 }
418 }
419
420 self.vargs = vargs;
421 Ok(())
422 }
423
424 fn get_arg(&self, name: &str) -> &Argument {
425 self.args
426 .get(name)
427 .unwrap_or_else(|| panic!("Could not find argument: {name}"))
428 }
429
430 fn get_val(&self, name: &str) -> &Value {
431 &self.get_arg(name).value
432 }
433
434 pub fn was_set<T>(&self, arg_handle: ArgHandle<T>) -> bool {
436 self.get_arg(arg_handle.name).was_set
437 }
438
439 pub fn get_vargs(&self) -> std::slice::Iter<'_, String> {
441 self.vargs.iter()
442 }
443
444 fn generate_help(&mut self) {
445 let examples = {
446 let mut res = String::new();
447 self.examples.iter().for_each(|s| {
448 res.push_str(&format!(" {program} {s}\n", program = self.program_name))
449 });
450
451 if !res.is_empty() {
452 res = "Examples:\n\n".to_owned() + &res;
453 }
454
455 res
456 };
457
458 self.help = format!(
459 " in the given order
460{description}
461
462Help:
463
464 Usage: {program} {usage}
465
466 Options:
467
468{arguments}
469
470{examples}",
471 description = self.description,
472 program = self.program_name,
473 usage = self.usage,
474 arguments = self.generate_args_help_list(),
475 );
476 }
477
478 fn generate_args_help_list(&self) -> String {
479 let mut args_help = String::new();
480
481 let mut keys: Vec<&String> = self.args.keys().collect();
482 keys.sort();
483
484 for arg in keys.iter().map(|&k| self.args.get(k).unwrap()) {
485 let name = "--".to_owned() + arg.name;
486
487 let short_name = {
488 if !arg.short_name.is_empty() {
489 "-".to_owned() + arg.short_name + ", "
490 } else {
491 "".to_string()
492 }
493 };
494
495 let mut default = match &arg.default {
496 Value::Bool(true) => "true".to_string(),
497 Value::Txt(s) => {
498 if s.is_empty() {
499 "".to_string()
500 } else {
501 s.clone()
502 }
503 }
504 Value::Num(n) => n.to_string(),
505 _ => "".to_string(),
506 };
507
508 let value = {
509 match arg.default {
510 Value::Bool(_) => "".to_string(),
511 _ => format!("=<{}>", arg.name),
512 }
513 };
514
515 if !default.is_empty() {
516 default = format!("[Default: {}]", default);
517 }
518
519 let line = &format!(
520 "{space:2}{short_name:>6}{name_and_val:25}{desc} {default}\n",
521 space = "",
522 name_and_val = name + &value,
523 desc = arg.description
524 );
525
526 args_help.push_str(line);
527 }
528
529 args_help
530 }
531
532 pub fn get_help_txt(&mut self) -> &str {
534 if self.help.is_empty() {
535 self.generate_help();
536 }
537
538 &self.help
539 }
540
541 pub fn print_help(&mut self) {
543 println!("{}", self.get_help_txt());
544 }
545
546 pub fn print_help_and_exit(&mut self, exit_code: i32) {
548 println!("{}", self.get_help_txt());
549 std::process::exit(exit_code);
550 }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq)]
554pub struct ArgHandle<T> {
555 pub name: &'static str,
556 _p: PhantomData<T>,
557}