1use std::any::type_name;
89use std::collections::HashMap;
90use std::fmt::Display;
91use std::marker::PhantomData;
92use std::num::ParseFloatError;
93use std::str::ParseBoolError;
94
95#[derive(Clone, Debug)]
96pub enum Error {
97 ParseValue { value: String, arg: String },
98 UnknownOpt(String),
99 UnknownCmd(String),
100 Parse(String),
101}
102
103impl Display for Error {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Error::ParseValue { value, arg } => {
107 write!(f, "Cannot parse value: {} for argument: {}", value, arg)
108 }
109 Error::UnknownOpt(s) => write!(f, "Unknown option: {}", s),
110 Error::UnknownCmd(s) => write!(f, "Unknown command: {}", s),
111 Error::Parse(s) => f.write_str(s),
112 }
113 }
114}
115
116impl std::error::Error for Error {}
117
118#[derive(Debug, Clone, PartialEq)]
120pub enum Value {
121 Bool(bool),
122 Num(f64),
123 Txt(String),
124}
125
126impl Value {
127 pub fn parse_as_bool(input_val: &str) -> Result<Self, ParseBoolError> {
129 let b = input_val.parse::<bool>()?;
130 Ok(Value::Bool(b))
131 }
132
133 pub fn parse_as_num(input_val: &str) -> Result<Self, ParseFloatError> {
135 let num = input_val.parse::<f64>()?;
136 Ok(Value::Num(num))
137 }
138}
139
140pub trait FromValue: Sized {
141 fn from_value(v: &Value) -> Option<Self>;
142}
143
144impl FromValue for bool {
145 fn from_value(v: &Value) -> Option<Self> {
146 if let Value::Bool(b) = v {
147 Some(*b)
148 } else {
149 None
150 }
151 }
152}
153
154impl FromValue for f64 {
155 fn from_value(v: &Value) -> Option<Self> {
156 if let Value::Num(n) = v {
157 Some(*n)
158 } else {
159 None
160 }
161 }
162}
163
164impl FromValue for String {
165 fn from_value(v: &Value) -> Option<Self> {
166 if let Value::Txt(s) = v {
167 Some(s.clone())
168 } else {
169 None
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, Eq, PartialEq)]
175pub struct OptHandle<T> {
176 name: &'static str,
177 _p: PhantomData<T>,
178}
179
180#[derive(Debug, Clone, Copy, Eq, PartialEq)]
181pub struct CmdHandle {
182 name: &'static str,
183}
184
185impl CmdHandle {
186 const NONE: Self = CmdHandle { name: "" };
187}
188
189#[derive(Debug, Clone, PartialEq)]
190pub struct Argument {
191 pub name: &'static str,
192 pub short_name: Option<char>,
193 pub description: &'static str,
194 pub default: Value,
195 pub value: Value,
196 pub was_set: bool,
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct Command {
201 pub name: &'static str,
202 pub description: &'static str,
203}
204
205#[derive(Debug, Default, Clone, PartialEq)]
206pub struct TinyArgs {
207 pub program_name: String,
208 pub description: String,
209 pub help: String,
210 pub usage: String,
211 pub examples: Vec<String>,
212 pub cmds: HashMap<String, Command>,
213 pub opts: HashMap<String, Argument>,
214 pub va_args: Vec<String>,
215 pub active_cmd: Option<Command>,
216}
217
218impl TinyArgs {
219 #[must_use]
221 pub fn new() -> Self {
222 let mut res = Self {
223 program_name: String::new(),
224 description: String::new(),
225 help: String::new(),
226 usage: String::new(),
227 examples: vec![],
228 cmds: HashMap::new(),
229 opts: HashMap::new(),
230 va_args: vec![],
231 active_cmd: None,
232 };
233
234 let _ = res.define_option_bool("help", 'h', false, "Display this help message");
235 res
236 }
237
238 pub fn define_help_program_name(&mut self, name: &str) {
241 self.program_name = name.to_owned();
242 }
243
244 pub fn define_help_description(&mut self, description: &str) {
246 self.description = description.into();
247 }
248
249 pub fn define_help_usage(&mut self, usage: &str) {
252 self.usage = usage.into();
253 }
254
255 pub fn define_help_example(&mut self, examples: &str) {
259 self.examples.push(examples.to_string());
260 }
261
262 #[must_use]
264 pub fn define_command(&mut self, name: &'static str, description: &'static str) -> CmdHandle {
265 let arg = Command { name, description };
266 self.cmds.insert(name.to_owned(), arg);
267
268 CmdHandle { name }
269 }
270
271 #[must_use]
273 pub fn define_option_bool(
274 &mut self,
275 name: &'static str,
276 short_name: impl Into<Option<char>>,
277 default_value: bool,
278 description: &'static str,
279 ) -> OptHandle<bool> {
280 self.define_argument(name, short_name, Value::Bool(default_value), description);
281
282 OptHandle {
283 name,
284 _p: PhantomData::<bool>,
285 }
286 }
287
288 #[must_use]
290 pub fn define_option_num(
291 &mut self,
292 name: &'static str,
293 short_name: impl Into<Option<char>>,
294 default_value: impl Into<f64>,
295 description: &'static str,
296 ) -> OptHandle<f64> {
297 self.define_argument(
298 name,
299 short_name,
300 Value::Num(default_value.into()),
301 description,
302 );
303
304 OptHandle {
305 name,
306 _p: PhantomData::<f64>,
307 }
308 }
309
310 #[must_use]
312 pub fn define_option_txt(
313 &mut self,
314 name: &'static str,
315 short_name: impl Into<Option<char>>,
316 default_value: &str,
317 description: &'static str,
318 ) -> OptHandle<String> {
319 self.define_argument(
320 name,
321 short_name,
322 Value::Txt(default_value.into()),
323 description,
324 );
325
326 OptHandle {
327 name,
328 _p: PhantomData::<String>,
329 }
330 }
331
332 fn define_argument(
334 &mut self,
335 name: &'static str,
336 short_name: impl Into<Option<char>>,
337 default_value: Value,
338 description: &'static str,
339 ) {
340 let arg = Argument {
341 name,
342 short_name: short_name.into(),
343 description,
344 value: default_value.clone(),
345 default: default_value,
346 was_set: false,
347 };
348 self.opts.insert(name.to_owned(), arg);
349 }
350
351 #[must_use]
353 pub fn get_option<T: FromValue>(&self, opt_handle: OptHandle<T>) -> T {
354 let val = &self.find_argument(opt_handle.name).value;
355
356 T::from_value(val).unwrap_or_else(|| {
357 panic!(
358 "type mismatch for argument {} when converting from {:?} to {}",
359 opt_handle.name,
360 val,
361 type_name::<T>()
362 )
363 })
364 }
365
366 pub fn command(&self) -> CmdHandle {
375 let name = self.active_cmd.as_ref().map_or_else(|| "", |c| c.name);
376
377 if name.is_empty() {
378 return CmdHandle::NONE;
379 }
380
381 CmdHandle { name }
382 }
383
384 pub fn parse_arguments(&mut self) -> Result<(), Error> {
396 let args = std::env::args().collect();
397 self.parse_arguments_from_vec(args)
398 }
399
400 pub fn parse_arguments_from_vec(&mut self, args: Vec<String>) -> Result<(), Error> {
402 let mut args_iter = args.iter();
403
404 let input_name = args_iter.next().ok_or_else(|| {
405 Error::Parse("Failed parsing first argument (executable path)".to_owned())
406 })?;
407
408 if self.program_name.is_empty() {
410 let split: Vec<&str> = input_name.split(|c| "\\/".contains(c)).collect();
411
412 self.program_name = split
413 .last()
414 .map_or("program_name".to_owned(), |s| s.to_string())
415 }
416
417 for input in args_iter {
419 let mut trimmed_input = input.to_owned();
420 let mut prefix_dash_count = 0;
421
422 for _ in 0..2 {
424 if let Some(trimmed) = trimmed_input.strip_prefix('-') {
425 prefix_dash_count += 1;
426 trimmed_input = trimmed.to_owned();
427 } else {
428 break;
429 }
430 }
431
432 if trimmed_input.is_empty() {
433 return Err(Error::Parse("Invalid argument prefixed by '-'".to_owned()));
434 }
435
436 if prefix_dash_count == 0 {
439 if let Some(cmd) = self.cmds.get_mut(&trimmed_input)
441 && self.active_cmd.is_none()
442 {
443 self.active_cmd = Some(cmd.clone());
445 } else if self.active_cmd.is_some() || self.cmds.is_empty() {
447 self.va_args.push(trimmed_input); } else {
450 return Err(Error::UnknownCmd(trimmed_input));
452 }
453 continue; }
455
456 let mut input_arg = trimmed_input;
457 let mut input_val = String::new();
458
459 if let Some((left, right)) = input_arg.split_once('=') {
463 if left.is_empty() {
464 return Err(Error::Parse(format!("Argument missing before ={}", right)));
465 }
466
467 if right.is_empty() {
468 return Err(Error::Parse(format!("Value missing after {}=", left)));
469 }
470 input_val = right.to_owned();
471 input_arg = left.to_owned();
472 }
473
474 if prefix_dash_count == 1 && input_arg.chars().count() > 1 && !input_val.is_empty() {
476 return Err(Error::Parse(format!(
477 "Grouped options cannot have assigned values: '-{input_arg}={input_val}'"
478 )));
479 }
480
481 if input_arg == "help" || input_arg == "h" {
483 self.print_help_and_exit(0);
484 }
485
486 if prefix_dash_count == 1 && input_arg.chars().count() > 1 {
489 for short_name in input_arg.chars() {
491 if short_name == 'h' {
493 self.print_help_and_exit(0);
494 }
495
496 let found_arg = self.opts.iter_mut().find_map(|(_, a)| {
497 if Some(short_name) == a.short_name {
498 Some(a)
499 } else {
500 None
501 }
502 });
503
504 if let Some(argument) = found_arg {
506 argument.was_set = true;
507 if matches!(argument.value, Value::Bool(_)) {
509 argument.value = Value::Bool(true)
510 } else {
511 return Err(Error::Parse(format!(
512 "Only boolean type options can be part of grouped options: '-{input_arg}', option: '{short_name}', '{}'",
513 argument.name
514 )));
515 }
516 } else {
517 return Err(Error::UnknownOpt(short_name.into()));
518 }
519 }
520
521 continue;
522 }
523
524 let found_arg = self.opts.iter_mut().find_map(|(_, a)| {
526 if (prefix_dash_count == 2 && input_arg == a.name)
527 || (prefix_dash_count == 1
528 && input_arg == a.short_name.unwrap_or(' ').to_string())
529 {
530 Some(a)
531 } else {
532 None
533 }
534 });
535
536 if let Some(argument) = found_arg {
537 argument.was_set = true;
538 if input_val.is_empty() {
540 if matches!(argument.value, Value::Bool(_)) {
541 argument.value = Value::Bool(true)
542 } else {
543 return Err(Error::Parse(format!(
544 "No value specified for option: '{}'",
545 argument.name
546 )));
547 }
548 }
549 else {
551 argument.value = match argument.value {
552 Value::Txt(_) => Value::Txt(input_val),
553 Value::Num(_) => {
554 Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
555 value: input_val,
556 arg: input_arg,
557 })?
558 }
559 Value::Bool(_) => {
560 Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
561 value: input_val,
562 arg: input_arg,
563 })?
564 }
565 }
566 }
567 } else {
568 return Err(Error::UnknownOpt(input_arg));
570 }
571 }
572
573 Ok(())
574 }
575
576 fn find_argument(&self, name: &str) -> &Argument {
578 self.opts
579 .get(name)
580 .unwrap_or_else(|| panic!("Could not find argument: {name}"))
581 }
582
583 pub fn was_option_set<T>(&self, arg_handle: OptHandle<T>) -> bool {
585 self.find_argument(arg_handle.name).was_set
586 }
587
588 pub fn get_va_args(&self) -> std::slice::Iter<'_, String> {
590 self.va_args.iter()
591 }
592
593 fn generate_help(&mut self) {
594 if self.usage.is_empty() {
595 self.usage = {
596 let mut options = "";
597 let mut commands = "";
598
599 if !self.opts.is_empty() {
600 options = "[OPTIONS] "
601 };
602
603 if !self.cmds.is_empty() {
604 commands = "[COMMANDS] "
605 };
606
607 format!("{}{}[ARGS]...", options, commands)
608 }
609 }
610
611 let examples = {
612 let mut res = String::new();
613
614 if !self.examples.is_empty() {
615 res = "\nExamples:\n\n".to_owned() + &res;
616 self.examples.iter().for_each(|s| {
617 res.push_str(&format!(" {program} {s}\n", program = self.program_name))
618 });
619 }
620
621 res
622 };
623
624 self.help = format!(
625 "
626{description}
627
628Help:
629
630 Usage: {program} {usage}
631{commands} {arguments} {examples}
632",
633 description = self.description,
634 program = self.program_name,
635 usage = self.usage,
636 commands = if !self.cmds.is_empty() {
637 "\n Commands:\n\n".to_string() + &self.generate_cmds_help_list()
638 } else {
639 "".to_string()
640 },
641 arguments = if !self.opts.is_empty() {
642 "\n Options:\n\n".to_string() + &self.generate_args_help_list()
643 } else {
644 "".to_string()
645 },
646 );
647 }
648
649 fn generate_args_help_list(&self) -> String {
650 let mut args_help = String::new();
651
652 let mut keys: Vec<&String> = self.opts.keys().collect();
653 keys.sort();
654
655 for arg in keys.iter().map(|&k| self.opts.get(k).unwrap()) {
656 let name = "--".to_owned() + arg.name;
657
658 let short_name = {
659 if let Some(short_name) = arg.short_name {
660 "-".to_owned() + &short_name.to_string() + ", "
661 } else {
662 "".to_string()
663 }
664 };
665
666 let mut default = match &arg.default {
667 Value::Bool(true) => "true".to_string(),
668 Value::Txt(s) => {
669 if s.is_empty() {
670 "".to_string()
671 } else {
672 s.clone()
673 }
674 }
675 Value::Num(n) => n.to_string(),
676 _ => "".to_string(),
677 };
678
679 let value = {
680 match arg.default {
681 Value::Bool(_) => "".to_string(),
682 _ => format!("=<{}>", arg.name),
683 }
684 };
685
686 if !default.is_empty() {
687 default = format!("[Default: {}]", default);
688 }
689
690 let line = &format!(
691 "{space:2}{short_name:>6}{name_and_val:23}{desc} {default}\n",
692 space = "",
693 name_and_val = name + &value,
694 desc = arg.description
695 );
696
697 args_help.push_str(line);
698 }
699
700 args_help
701 }
702
703 fn generate_cmds_help_list(&self) -> String {
704 let mut cmds_help = String::new();
705
706 let mut keys: Vec<&String> = self.cmds.keys().collect();
707 keys.sort();
708
709 for cmd in keys.iter().map(|&k| self.cmds.get(k).unwrap()) {
710 let line = &format!(
711 "{space:6}{name:25}{desc}\n",
712 space = "",
713 name = cmd.name,
714 desc = cmd.description
715 );
716
717 cmds_help.push_str(line);
718 }
719
720 cmds_help
721 }
722
723 pub fn get_help_text(&mut self) -> &str {
725 if self.help.is_empty() {
726 self.generate_help();
727 }
728
729 &self.help
730 }
731
732 pub fn print_help(&mut self) {
734 println!("{}", self.get_help_text());
735 }
736
737 pub fn print_help_and_exit(&mut self, exit_code: i32) {
739 println!("{}", self.get_help_text());
740 std::process::exit(exit_code);
741 }
742}