Skip to main content

tiny_args/
lib.rs

1/*!
2 A tiny command line argument parser with automatic help generation, and argument validation.
3
4- Inputs are categorized as `commands`, `options`, and `va args`.
5- Commands and va args have no prefixes. 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()`.
6- Options/flags are defined with the `-` or `--` prefixes.
7- These can hold values representing booleans, numbers and text values, (stored internally as bool, f64 and Strings).
8- Arguments with values are strictly defined with the equal sign `=` like: `--arg=value`.
9- Help sections such as description, usage, and examples can be redefined if needed using the
10   provided functions: `define_help_...()`.
11 - The help call is hard coded.
12
13
14 ## Example
15 ```
16use std::process::ExitCode;
17use tiny_args::*;
18
19fn main() -> ExitCode {
20    let mut args = TinyArgs::new();
21
22    // Optional help definitions:
23    args.define_help_program_name("demo");
24    args.define_help_description("A demo program for TinyArgs");
25    args.define_help_usage("[OPTIONS] [COMMAND] [ARGS]...");
26    args.define_help_example("--name=test some/path/  - Sets some values");
27
28    let list = args.define_command("list", "List vargs");
29    let version = args.define_command("version", "Display version");
30
31    let name = args.define_option_txt("name", "", "test", "A name of something");
32    let context = args.define_option_num("context", "c", 4, "Context lines");
33    let verbose = args.define_option_bool("verbose", "v", false, "Verbose mode");
34
35    if let Err(e) = args.parse_arguments() {
36        eprintln!("Error: {e}");
37        return ExitCode::FAILURE;
38    }
39
40    println!("name: {}", args.get_option(name));
41    println!("context: {}", args.get_option(context));
42    println!("verbose: {}", args.get_option(verbose));
43
44    if args.command() == version {
45        println!("Version: 1.2.3.4");
46    }
47
48    if args.command() == list {
49        for arg in args.get_va_args() {
50            println!("{arg}");
51        }
52    }
53
54    ExitCode::SUCCESS
55}
56 }
57 ```
58## Generated Help
59
60```none
61>demo_program --help
62
63A demo program for TinyArgs
64
65Help:
66
67  Usage: demo [OPTIONS] [COMMAND] [ARGS]...
68
69  Commands:
70
71      list                     List args
72      version                  Display version
73
74  Options:
75
76    -c, --context=<context>    Context lines [Default: 4]
77    -h, --help                 Display this help message
78        --name=<name>          A name of something [Default: test]
79    -v, --verbose              Verbose mode
80
81Examples:
82
83  demo --name=test some/path/  - Sets some values
84```
85*/
86
87use 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/// Possible argument values
118#[derive(Debug, Clone, PartialEq)]
119pub enum Value {
120    Bool(bool),
121    Num(f64),
122    Txt(String),
123}
124
125impl Value {
126    /// Parse str as bool Val
127    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    /// Parse str as num Val
133    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    /// Create a TinyArgs instance
204    #[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    /// Define the program name displayed in the help section
223    /// If not defined, the program name is automatically derived from the command line
224    pub fn define_help_program_name(&mut self, name: &str) {
225        self.program_name = name.to_owned();
226    }
227
228    /// Define the program description for the help section
229    pub fn define_help_description(&mut self, description: &str) {
230        self.description = description.into();
231    }
232
233    /// Define program usage for the help section
234    /// The program name gets automatically prefixed,
235    pub fn define_help_usage(&mut self, usage: &str) {
236        self.usage = usage.into();
237    }
238
239    /// Define examples in for the help section
240    /// You can this function multiple times to add more execution examples
241    /// The program name gets automatically prefixed
242    pub fn define_help_example(&mut self, examples: &str) {
243        self.examples.push(examples.to_string());
244    }
245
246    /// Define a command
247    #[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    /// Define a boolean option
256    #[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    /// Define a numerical option
273    #[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    /// Define a text option
295    #[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    /// Internal
317    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    /// Get the option's value from the stored handle
336    #[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    /// Get the active command handle
351    /// CmdHandle::NONE is returned if no command is set
352    /// Example:
353    ///  ```
354    ///      if args.command() == version {
355    ///          println!("Version: 1.2.3.4");
356    ///      }
357    ///  ```
358    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    /// This function MUST be run for the input arguments to be processed
369    /// Automatically handles the help printout if "help" or "h" is encountered
370    /// Call example:
371    /// ```
372    ///
373    ///    if let Err(e) = args.parse_arguments() {
374    ///        eprintln!("Error: {e}");
375    ///        return ExitCode::FAILURE;
376    ///    }
377    ///
378    /// ```
379    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    /// Parse arguments from a provided vector of Strings
385    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        // We derive the program name if none was defined by the user
393        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            // Trimming - or -- prefixes
403            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            // Parsing command or va_arg
409            if &trimmed_input == input {
410                // Argument was not prefixed with - or --
411                if let Some(cmd) = self.cmds.get_mut(&trimmed_input)
412                    && self.active_cmd.is_none()
413                {
414                    // No command was registered, and command is valid
415                    self.active_cmd = Some(cmd.clone());
416                    // TODO Do something about help?
417                    //
418                } else if self.active_cmd.is_some() || self.cmds.is_empty() {
419                    // Va args
420                    self.va_args.push(trimmed_input); // We add it to the va args bucket
421                } else {
422                    // Commands are defined, this is the first command input, but we don't recognise this specific one
423                    return Err(Error::UnknownCmd(trimmed_input));
424                }
425                continue; // We continue to next arg
426            }
427
428            let mut input_arg = trimmed_input;
429            let mut input_val = String::new();
430
431            // Try splitting arg=value into separate parts
432            //
433            // If value is not present, then the value string stays empty.
434            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            // We catch help option flags and display it immediately
447            if input_arg == "help" || input_arg == "h" {
448                self.print_help_and_exit(0);
449            }
450
451            // Find the argument against user registered ones
452            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                // Only boolean options/flags can be set without an explicit value
463                if input_val.is_empty() {
464                    if matches!(argument.value, Value::Bool(_)) {
465                        argument.value = Value::Bool(true)
466                    }
467                }
468                // Options/flags with explicit value assignment arg=val
469                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                // Argument not defined - unknown
488                return Err(Error::UnknownOpt(input_arg));
489            }
490        }
491
492        Ok(())
493    }
494
495    /// Internal - Acts as get, should not fail
496    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    /// Find if an argument was explicitly set by the user
503    pub fn was_option_set<T>(&self, arg_handle: OptHandle<T>) -> bool {
504        self.find_argument(arg_handle.name).was_set
505    }
506
507    /// Retrieve the rest of input va args
508    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    /// Get help as str
643    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    /// Print the program help
652    pub fn print_help(&mut self) {
653        println!("{}", self.get_help_text());
654    }
655
656    /// Print the program help and exit program with code
657    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}