1use std::any::type_name;
88use std::collections::HashMap;
89use std::fmt::Display;
90use std::marker::PhantomData;
91use std::num::ParseFloatError;
92use std::str::ParseBoolError;
93
94#[derive(Clone, Debug)]
95pub enum Error {
96 ParseValue { value: String, arg: String },
97 UnknownOpt(String),
98 UnknownCmd(String),
99 Parse(String),
100}
101
102impl Display for Error {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 Error::ParseValue { value, arg } => {
106 write!(f, "Cannot parse value: {} for argument: {}", value, arg)
107 }
108 Error::UnknownOpt(s) => write!(f, "Unknown option: {}", s),
109 Error::UnknownCmd(s) => write!(f, "Unknown command: {}", s),
110 Error::Parse(s) => f.write_str(s),
111 }
112 }
113}
114
115impl std::error::Error for Error {}
116
117#[derive(Debug, Clone, PartialEq)]
119pub enum Value {
120 Bool(bool),
121 Num(f64),
122 Txt(String),
123}
124
125impl Value {
126 pub fn parse_as_bool(input_val: &str) -> Result<Self, ParseBoolError> {
128 let b = input_val.parse::<bool>()?;
129 Ok(Value::Bool(b))
130 }
131
132 pub fn parse_as_num(input_val: &str) -> Result<Self, ParseFloatError> {
134 let num = input_val.parse::<f64>()?;
135 Ok(Value::Num(num))
136 }
137}
138
139pub trait FromValue: Sized {
140 fn from_value(v: &Value) -> Option<Self>;
141}
142
143impl FromValue for bool {
144 fn from_value(v: &Value) -> Option<Self> {
145 if let Value::Bool(b) = v {
146 Some(*b)
147 } else {
148 None
149 }
150 }
151}
152
153impl FromValue for f64 {
154 fn from_value(v: &Value) -> Option<Self> {
155 if let Value::Num(n) = v {
156 Some(*n)
157 } else {
158 None
159 }
160 }
161}
162
163impl FromValue for String {
164 fn from_value(v: &Value) -> Option<Self> {
165 if let Value::Txt(s) = v {
166 Some(s.clone())
167 } else {
168 None
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq)]
174pub struct Argument {
175 pub name: &'static str,
176 pub short_name: &'static str,
177 pub description: &'static str,
178 pub default: Value,
179 pub value: Value,
180 pub was_set: bool,
181}
182
183#[derive(Debug, Clone, PartialEq)]
184pub struct Command {
185 pub name: &'static str,
186 pub description: &'static str,
187}
188
189#[derive(Debug, Default, Clone, PartialEq)]
190pub struct TinyArgs {
191 pub program_name: String,
192 pub description: String,
193 pub help: String,
194 pub usage: String,
195 pub examples: Vec<String>,
196 pub cmds: HashMap<String, Command>,
197 pub opts: HashMap<String, Argument>,
198 pub va_args: Vec<String>,
199 pub active_cmd: Option<Command>,
200}
201
202impl TinyArgs {
203 #[must_use]
205 pub fn new() -> Self {
206 let mut res = Self {
207 program_name: String::new(),
208 description: String::new(),
209 help: String::new(),
210 usage: String::new(),
211 examples: vec![],
212 cmds: HashMap::new(),
213 opts: HashMap::new(),
214 va_args: vec![],
215 active_cmd: None,
216 };
217
218 let _ = res.define_option_bool("help", "h", false, "Display this help message");
219 res
220 }
221
222 pub fn define_help_program_name(&mut self, name: &str) {
225 self.program_name = name.to_owned();
226 }
227
228 pub fn define_help_description(&mut self, description: &str) {
230 self.description = description.into();
231 }
232
233 pub fn define_help_usage(&mut self, usage: &str) {
236 self.usage = usage.into();
237 }
238
239 pub fn define_help_example(&mut self, examples: &str) {
243 self.examples.push(examples.to_string());
244 }
245
246 #[must_use]
248 pub fn define_command(&mut self, name: &'static str, description: &'static str) -> CmdHandle {
249 let arg = Command { name, description };
250 self.cmds.insert(name.to_owned(), arg);
251
252 CmdHandle { name }
253 }
254
255 #[must_use]
257 pub fn define_option_bool(
258 &mut self,
259 name: &'static str,
260 short_name: &'static str,
261 default_value: bool,
262 description: &'static str,
263 ) -> OptHandle<bool> {
264 self.define_argument(name, short_name, Value::Bool(default_value), description);
265
266 OptHandle {
267 name,
268 _p: PhantomData::<bool>,
269 }
270 }
271
272 #[must_use]
274 pub fn define_option_num(
275 &mut self,
276 name: &'static str,
277 short_name: &'static str,
278 default_value: impl Into<f64>,
279 description: &'static str,
280 ) -> OptHandle<f64> {
281 self.define_argument(
282 name,
283 short_name,
284 Value::Num(default_value.into()),
285 description,
286 );
287
288 OptHandle {
289 name,
290 _p: PhantomData::<f64>,
291 }
292 }
293
294 #[must_use]
296 pub fn define_option_txt(
297 &mut self,
298 name: &'static str,
299 short_name: &'static str,
300 default_value: &str,
301 description: &'static str,
302 ) -> OptHandle<String> {
303 self.define_argument(
304 name,
305 short_name,
306 Value::Txt(default_value.into()),
307 description,
308 );
309
310 OptHandle {
311 name,
312 _p: PhantomData::<String>,
313 }
314 }
315
316 fn define_argument(
318 &mut self,
319 name: &'static str,
320 short_name: &'static str,
321 default_value: Value,
322 description: &'static str,
323 ) {
324 let arg = Argument {
325 name,
326 short_name,
327 description,
328 value: default_value.clone(),
329 default: default_value,
330 was_set: false,
331 };
332 self.opts.insert(name.to_owned(), arg);
333 }
334
335 #[must_use]
337 pub fn get_option<T: FromValue>(&self, opt_handle: OptHandle<T>) -> T {
338 let val = &self.find_argument(opt_handle.name).value;
339
340 T::from_value(&val).unwrap_or_else(|| {
341 panic!(
342 "type mismatch for argument {} when converting from {:?} to {}",
343 opt_handle.name,
344 val,
345 type_name::<T>()
346 )
347 })
348 }
349
350 pub fn command(&self) -> CmdHandle {
359 let name = self.active_cmd.as_ref().map_or_else(|| "", |c| c.name);
360
361 if name.is_empty() {
362 return CmdHandle::NONE;
363 }
364
365 CmdHandle { name }
366 }
367
368 pub fn parse_arguments(&mut self) -> Result<(), Error> {
380 let args = std::env::args().collect();
381 self.parse_arguments_from_vec(args)
382 }
383
384 pub fn parse_arguments_from_vec(&mut self, args: Vec<String>) -> Result<(), Error> {
386 let mut args_iter = args.iter();
387
388 let input_name = args_iter.next().ok_or_else(|| {
389 Error::Parse("Failed parsing first argument (executable path)".to_owned())
390 })?;
391
392 if self.program_name.is_empty() {
394 let split: Vec<&str> = input_name.split(|c| c == '\\' || c == '/').collect();
395
396 self.program_name = split
397 .last()
398 .map_or("program_name".to_owned(), |s| s.to_string())
399 }
400
401 for input in args_iter {
402 let trimmed_input = input.trim_start_matches('-').to_owned();
404 if trimmed_input.is_empty() {
405 return Err(Error::Parse("Invalid argument starting with -".to_owned()));
406 }
407
408 if &trimmed_input == input {
410 if let Some(cmd) = self.cmds.get_mut(&trimmed_input)
412 && self.active_cmd.is_none()
413 {
414 self.active_cmd = Some(cmd.clone());
416 } else if self.active_cmd.is_some() || self.cmds.is_empty() {
419 self.va_args.push(trimmed_input); } else {
422 return Err(Error::UnknownCmd(trimmed_input));
424 }
425 continue; }
427
428 let mut input_arg = trimmed_input;
429 let mut input_val = String::new();
430
431 if let Some((left, right)) = input_arg.split_once('=') {
435 if left.is_empty() {
436 return Err(Error::Parse(format!("Argument missing before ={}", right)));
437 }
438
439 if right.is_empty() {
440 return Err(Error::Parse(format!("Value missing after {}=", left)));
441 }
442 input_val = right.to_owned();
443 input_arg = left.to_owned();
444 }
445
446 if input_arg == "help" || input_arg == "h" {
448 self.print_help_and_exit(0);
449 }
450
451 let found_arg = self.opts.iter_mut().find_map(|(_, a)| {
453 if input_arg == a.name || input_arg == a.short_name {
454 Some(a)
455 } else {
456 None
457 }
458 });
459
460 if let Some(argument) = found_arg {
461 argument.was_set = true;
462 if input_val.is_empty() {
464 if matches!(argument.value, Value::Bool(_)) {
465 argument.value = Value::Bool(true)
466 }
467 }
468 else {
470 argument.value = match argument.value {
471 Value::Txt(_) => Value::Txt(input_val),
472 Value::Num(_) => {
473 Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
474 value: input_val,
475 arg: input_arg,
476 })?
477 }
478 Value::Bool(_) => {
479 Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
480 value: input_val,
481 arg: input_arg,
482 })?
483 }
484 }
485 }
486 } else {
487 return Err(Error::UnknownOpt(input_arg));
489 }
490 }
491
492 Ok(())
493 }
494
495 fn find_argument(&self, name: &str) -> &Argument {
497 self.opts
498 .get(name)
499 .unwrap_or_else(|| panic!("Could not find argument: {name}"))
500 }
501
502 pub fn was_option_set<T>(&self, arg_handle: OptHandle<T>) -> bool {
504 self.find_argument(arg_handle.name).was_set
505 }
506
507 pub fn get_va_args(&self) -> std::slice::Iter<'_, String> {
509 self.va_args.iter()
510 }
511
512 fn generate_help(&mut self) {
513 if self.usage.is_empty() {
514 self.usage = {
515 let mut options = "";
516 let mut commands = "";
517
518 if !self.opts.is_empty() {
519 options = "[OPTIONS] "
520 };
521
522 if !self.cmds.is_empty() {
523 commands = "[COMMANDS] "
524 };
525
526 format!("{}{}[ARGS]...", options, commands)
527 }
528 }
529
530 let examples = {
531 let mut res = String::new();
532
533 if !self.examples.is_empty() {
534 res = "\nExamples:\n\n".to_owned() + &res;
535 self.examples.iter().for_each(|s| {
536 res.push_str(&format!(" {program} {s}\n", program = self.program_name))
537 });
538 }
539
540 res
541 };
542
543 self.help = format!(
544 "
545{description}
546
547Help:
548
549 Usage: {program} {usage}
550{commands} {arguments} {examples}
551",
552 description = self.description,
553 program = self.program_name,
554 usage = self.usage,
555 commands = if !self.cmds.is_empty() {
556 "\n Commands:\n\n".to_string() + &self.generate_cmds_help_list()
557 } else {
558 "".to_string()
559 },
560 arguments = if !self.opts.is_empty() {
561 "\n Options:\n\n".to_string() + &self.generate_args_help_list()
562 } else {
563 "".to_string()
564 },
565 );
566 }
567
568 fn generate_args_help_list(&self) -> String {
569 let mut args_help = String::new();
570
571 let mut keys: Vec<&String> = self.opts.keys().collect();
572 keys.sort();
573
574 for arg in keys.iter().map(|&k| self.opts.get(k).unwrap()) {
575 let name = "--".to_owned() + arg.name;
576
577 let short_name = {
578 if !arg.short_name.is_empty() {
579 "-".to_owned() + arg.short_name + ", "
580 } else {
581 "".to_string()
582 }
583 };
584
585 let mut default = match &arg.default {
586 Value::Bool(true) => "true".to_string(),
587 Value::Txt(s) => {
588 if s.is_empty() {
589 "".to_string()
590 } else {
591 s.clone()
592 }
593 }
594 Value::Num(n) => n.to_string(),
595 _ => "".to_string(),
596 };
597
598 let value = {
599 match arg.default {
600 Value::Bool(_) => "".to_string(),
601 _ => format!("=<{}>", arg.name),
602 }
603 };
604
605 if !default.is_empty() {
606 default = format!("[Default: {}]", default);
607 }
608
609 let line = &format!(
610 "{space:2}{short_name:>6}{name_and_val:23}{desc} {default}\n",
611 space = "",
612 name_and_val = name + &value,
613 desc = arg.description
614 );
615
616 args_help.push_str(line);
617 }
618
619 args_help
620 }
621
622 fn generate_cmds_help_list(&self) -> String {
623 let mut cmds_help = String::new();
624
625 let mut keys: Vec<&String> = self.cmds.keys().collect();
626 keys.sort();
627
628 for cmd in keys.iter().map(|&k| self.cmds.get(k).unwrap()) {
629 let line = &format!(
630 "{space:6}{name:25}{desc}\n",
631 space = "",
632 name = cmd.name,
633 desc = cmd.description
634 );
635
636 cmds_help.push_str(line);
637 }
638
639 cmds_help
640 }
641
642 pub fn get_help_text(&mut self) -> &str {
644 if self.help.is_empty() {
645 self.generate_help();
646 }
647
648 &self.help
649 }
650
651 pub fn print_help(&mut self) {
653 println!("{}", self.get_help_text());
654 }
655
656 pub fn print_help_and_exit(&mut self, exit_code: i32) {
658 println!("{}", self.get_help_text());
659 std::process::exit(exit_code);
660 }
661}
662
663#[derive(Debug, Clone, Copy, Eq, PartialEq)]
664pub struct OptHandle<T> {
665 name: &'static str,
666 _p: PhantomData<T>,
667}
668
669#[derive(Debug, Clone, Copy, Eq, PartialEq)]
670pub struct CmdHandle {
671 name: &'static str,
672}
673
674impl CmdHandle {
675 const NONE: Self = CmdHandle { name: "" };
676}