Skip to main content

tiny_args/
lib.rs

1/*!
2A tiny command line argument parser with automatic help generation, and argument validation.
3
4- Inputs are categorized as `commands`, `options`, and `va args`.
5- Options are defined with the `-` or `--` prefixes.
6- These can hold values representing booleans, numbers and text values, (stored internally as bool, f64 and Strings).
7- Boolean short name options can be set as groups. E.g.: `-abc`
8- Arguments with values are strictly defined by using the equal sign `=`, i.e. `--arg=value`.
9- Commands and va args have no dash prefix. First argument of such kind is stored as the command, and the rest into a va_args "bucket", which can be retrived with `get_va_args()`.
10- Help sections such as description, usage, and examples can be redefined if needed using the
11  provided functions: `define_help_...()`.
12- The help call (-h --help) is hard coded during argument parsing.
13
14
15 ## Example
16```
17use std::process::ExitCode;
18use tiny_args::*;
19
20fn main() -> ExitCode {
21    let mut args = TinyArgs::new();
22
23    // Optional help definitions:
24    args.define_help_program_name("demo");
25    args.define_help_description("A demo program for TinyArgs");
26    args.define_help_usage("[OPTIONS] [COMMAND] [ARGS]...");
27    args.define_help_example("--name=test some/path/  - Sets some values");
28
29    let list = args.define_command("list", "List vargs");
30    let version = args.define_command("version", "Display version");
31
32    let name = args.define_option_txt("name", None, "test", "A name of something");
33    let context = args.define_option_num("context", 'c', 4, "Context lines");
34    let verbose = args.define_option_bool("verbose", 'v', false, "Verbose mode");
35
36    if let Err(e) = args.parse_arguments() {
37        eprintln!("Error: {e}");
38        return ExitCode::FAILURE;
39    }
40
41    println!("name: {}", args.get_option(name));
42    println!("context: {}", args.get_option(context));
43    println!("verbose: {}", args.get_option(verbose));
44
45    if args.command() == version {
46        println!("Version: 1.2.3.4");
47    }
48
49    if args.command() == list {
50        for arg in args.get_va_args() {
51            println!("{arg}");
52        }
53    }
54
55    ExitCode::SUCCESS
56  }
57 ```
58
59## Generated Help
60
61```none
62>demo_program --help
63
64A demo program for TinyArgs
65
66Help:
67
68  Usage: demo [OPTIONS] [COMMAND] [ARGS]...
69
70  Commands:
71
72      list                     List args
73      version                  Display version
74
75  Options:
76
77    -c, --context=<context>    Context lines [Default: 4]
78    -h, --help                 Display this help message
79        --name=<name>          A name of something [Default: test]
80    -v, --verbose              Verbose mode
81
82Examples:
83
84  demo --name=test some/path/  - Sets some values
85```
86*/
87
88use 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/// Possible argument values
119#[derive(Debug, Clone, PartialEq)]
120pub enum Value {
121    Bool(bool),
122    Num(f64),
123    Txt(String),
124}
125
126impl Value {
127    /// Parse str as bool Val
128    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    /// Parse str as num Val
134    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    /// Create a TinyArgs instance
220    #[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    /// Define the program name displayed in the help section
239    /// If not defined, the program name is automatically derived from the command line
240    pub fn define_help_program_name(&mut self, name: &str) {
241        self.program_name = name.to_owned();
242    }
243
244    /// Define the program description for the help section
245    pub fn define_help_description(&mut self, description: &str) {
246        self.description = description.into();
247    }
248
249    /// Define program usage for the help section
250    /// The program name gets automatically prefixed,
251    pub fn define_help_usage(&mut self, usage: &str) {
252        self.usage = usage.into();
253    }
254
255    /// Define examples in for the help section
256    /// You can this function multiple times to add more execution examples
257    /// The program name gets automatically prefixed
258    pub fn define_help_example(&mut self, examples: &str) {
259        self.examples.push(examples.to_string());
260    }
261
262    /// Define a command
263    #[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    /// Define a boolean option
272    #[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    /// Define a numerical option
289    #[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    /// Define a text option
311    #[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    /// Internal
333    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    /// Get the option's value from the stored handle
352    #[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    /// Get the active command handle
367    /// CmdHandle::NONE is returned if no command is set
368    /// Example:
369    ///  ```
370    ///      if args.command() == version {
371    ///          println!("Version: 1.2.3.4");
372    ///      }
373    ///  ```
374    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    /// This function MUST be run for the input arguments to be processed
385    /// Automatically handles the help printout if "help" or "h" is encountered
386    /// Call example:
387    /// ```
388    ///
389    ///    if let Err(e) = args.parse_arguments() {
390    ///        eprintln!("Error: {e}");
391    ///        return ExitCode::FAILURE;
392    ///    }
393    ///
394    /// ```
395    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    /// Parse arguments from a provided vector of Strings
401    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        // We derive the program name if none was defined by the user
409        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        // Iter though the arguments
418        for input in args_iter {
419            let mut trimmed_input = input.to_owned();
420            let mut prefix_dash_count = 0;
421
422            // Trimming - or -- prefixes and counting the dashes
423            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            // Parsing command or va_arg
437            // No - or -- prefix
438            if prefix_dash_count == 0 {
439                // Argument was not prefixed with - or --
440                if let Some(cmd) = self.cmds.get_mut(&trimmed_input)
441                    && self.active_cmd.is_none()
442                {
443                    // No command was registered, and command is valid
444                    self.active_cmd = Some(cmd.clone());
445                    //
446                } else if self.active_cmd.is_some() || self.cmds.is_empty() {
447                    // Va args
448                    self.va_args.push(trimmed_input); // We add it to the va args bucket
449                } else {
450                    // Commands are defined, this is the first command input, but we don't recognise this specific one
451                    return Err(Error::UnknownCmd(trimmed_input));
452                }
453                continue; // We continue to next arg
454            }
455
456            let mut input_arg = trimmed_input;
457            let mut input_val = String::new();
458
459            // Try splitting arg=value into separate parts
460            //
461            // If value is not present, then the value string stays empty
462            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            // We don't allow grouped short options with value assignments: e.g. -abc=10
475            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            // We catch help option flags and display it immediately
482            if input_arg == "help" || input_arg == "h" {
483                self.print_help_and_exit(0);
484            }
485
486            // Grouped short option: -abc
487            // We know that value is empty since we validated above
488            if prefix_dash_count == 1 && input_arg.chars().count() > 1 {
489                // We iterate though all characters part of the short name arg combo
490                for short_name in input_arg.chars() {
491                    // Auto help print
492                    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                    // Verify if arg is defined
505                    if let Some(argument) = found_arg {
506                        argument.was_set = true;
507                        // Only boolean options can be part of groups
508                        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            // Find the argument against user registered ones
525            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                // Only boolean options/flags can be set without an explicit value
539                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                // Options with explicit value assignment arg=val
550                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                // Argument not defined - unknown
569                return Err(Error::UnknownOpt(input_arg));
570            }
571        }
572
573        Ok(())
574    }
575
576    /// Internal - Acts as get, should not fail
577    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    /// Find if an argument was explicitly set by the user
584    pub fn was_option_set<T>(&self, arg_handle: OptHandle<T>) -> bool {
585        self.find_argument(arg_handle.name).was_set
586    }
587
588    /// Retrieve the rest of input va args
589    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    /// Get help as str
724    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    /// Print the program help
733    pub fn print_help(&mut self) {
734        println!("{}", self.get_help_text());
735    }
736
737    /// Print the program help and exit program with code
738    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}