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 - Internally, the arguments are stored inside a Value enum containing three simple types:
5   bool, String, and f64 representing booleans, numbers and text.
6 - Arguments without the `-` or `--` prefixes are stored inside a Vec "bucket" of VARGS in the given order. This list can be accessed using the `get_vargs()` function.
7 - Arguments with values are set like: `--arg=value`.
8 - Help sections such as description, usage, and examples can be redefined if needed using the
9   provided functions: `define_help_...()`.
10 - The help call is hard coded.
11
12
13 ## Example
14 ```
15 use tiny_args::*;
16 use std::process::ExitCode;
17
18 fn main() -> ExitCode {
19     let mut args = TinyArgs::new();
20
21     // Optional definitions:
22     args.define_help_program_name("demo_program");
23     args.define_help_description("A demo for TinyArgs");
24     args.define_help_usage("[OPTION] [PATHS]...");
25     args.define_help_example("--name=test some/path/  - Sets some values");
26
27     let name = args.define_arg_txt("name", "", "test", "A name of something");
28     let times = args.define_arg_num("times", "t", 22, "How many times");
29     let version = args.define_arg_bool("version", "v", false, "Display version");
30
31     if let Err(e) = args.parse_arguments() {
32         eprintln!("Error: {e}");
33         return ExitCode::FAILURE;
34     }
35
36     if args.get(version) {
37         println!("Version: 1.2.3.4");
38     }
39
40     println!("name: {}", args.get(name));
41     println!("times: {}", args.get(times));
42
43     println!("Paths:");
44     for arg in args.get_vargs() {
45         println!("{arg}");
46     }
47
48     ExitCode::SUCCESS
49 }
50 ```
51## Generated Help
52
53```none
54>demo_program --help
55
56Help:
57
58  Usage: demo_program [OPTION] [PATHS]...
59
60  Options:
61
62    -h, --help                   Display this help message
63        --name=<name>            A name of something [Default: test]
64    -t, --times=<times>          How many times [Default: 22]
65    -v, --version                Display version number
66
67
68Examples:
69
70  demo_program --name=test some/path/  - Sets some values
71```
72
73*/
74
75use 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/// Possible argument values
104#[derive(Debug, Clone, PartialEq)]
105pub enum Value {
106    Bool(bool),
107    Txt(String),
108    Num(f64),
109}
110
111impl Value {
112    /// Parse str as num Val
113    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    /// Parse str as bool Val
119    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    /// Create a TinyArgs instance
182    #[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    /// Define the program name displayed in the help section
201    /// If not defined, the program name is derived automatically from the command line
202    pub fn define_help_program_name(&mut self, name: &str) {
203        self.program_name = name.to_owned();
204    }
205
206    /// Define the program description for the help section
207    pub fn define_help_description(&mut self, description: &str) {
208        self.description = description.into();
209    }
210
211    /// Define program usage for the help section
212    /// The program name gets automatically prefixed,
213    pub fn define_help_usage(&mut self, usage: &str) {
214        self.usage = usage.into();
215    }
216
217    /// Define examples in for the help section
218    /// You can this function multiple times to add more execution examples
219    /// The program name gets automatically prefixed
220    pub fn define_help_example(&mut self, examples: &str) {
221        self.examples.push(examples.to_string());
222    }
223
224    /// Define a boolean argument
225    #[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    /// Define a numerical argument
242    #[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    /// Define a textual argument
264    #[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    /// Gets the argument value from the stored handle
304    /// T is known at compile time from the handle
305    #[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    /// This function MUST be run for the input arguments to be processed
320    /// Automatically handles the help printout if "help" or "h" is encountered
321    /// Call example:
322    /// ```
323    ///
324    ///    if let Err(e) = ta.parse_arguments() {
325    ///        eprintln!("Error: {e}");
326    ///        return ExitCode::FAILURE;
327    ///    }
328    ///
329    /// ```
330    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 first argument (executable path)".to_owned())
336        })?;
337
338        if self.program_name.is_empty() {
339            let split: Vec<&str> = input_name.split(|c| c == '\\' || c == '/').collect();
340
341            self.program_name = split
342                .last()
343                .map_or("program_name".to_owned(), |s| s.to_string())
344        }
345
346        for input in args_iter {
347            // Trimming - prefixes
348            let trimmed_input = input.trim_start_matches('-').to_owned();
349            if trimmed_input.is_empty() {
350                return Err(Error::Parse("Invalid argument starting with -".to_owned()));
351            }
352
353            // Argument was not prefixed with - or --
354            // We add it to the VARGS bucket
355            if trimmed_input == input {
356                vargs.push(input);
357                continue; // We continue to next arg
358            }
359
360            let mut input_arg = trimmed_input;
361            let mut input_val = String::new();
362
363            // Try Split arg=value
364            //
365            // If value is not present, value string stays empty
366            if let Some((left, right)) = input_arg.split_once('=') {
367                if left.is_empty() {
368                    return Err(Error::Parse(format!("Argument missing before ={}", right)));
369                }
370
371                if right.is_empty() {
372                    return Err(Error::Parse(format!("Value missing after {}=", left)));
373                }
374                input_val = right.to_owned();
375                input_arg = left.to_owned();
376            }
377
378            // Help
379            if input_arg == "help" || input_arg == "h" {
380                self.print_help_and_exit(0);
381            }
382
383            // Find Arg
384            let found_arg = self.args.iter_mut().find_map(|(_, a)| {
385                if input_arg == a.name || input_arg == a.short_name {
386                    Some(a)
387                } else {
388                    None
389                }
390            });
391
392            if let Some(argument) = found_arg {
393                argument.was_set = true;
394
395                // Boolean arguments/flags can be set without an explicit value
396                if input_val.is_empty() {
397                    if matches!(argument.value, Value::Bool(_)) {
398                        argument.value = Value::Bool(true)
399                    }
400                }
401                // Argument with explicit value assignment arg=val
402                else {
403                    argument.value = match argument.value {
404                        Value::Txt(_) => Value::Txt(input_val),
405                        Value::Num(_) => {
406                            Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
407                                value: input_val,
408                                arg: input_arg,
409                            })?
410                        }
411                        Value::Bool(_) => {
412                            Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
413                                value: input_val,
414                                arg: input_arg,
415                            })?
416                        }
417                    }
418                }
419            } else {
420                return Err(Error::UnknownArg(input_arg));
421            }
422        }
423
424        self.vargs = vargs;
425        Ok(())
426    }
427
428    fn get_arg(&self, name: &str) -> &Argument {
429        self.args
430            .get(name)
431            .unwrap_or_else(|| panic!("Could not find argument: {name}"))
432    }
433
434    fn get_val(&self, name: &str) -> &Value {
435        &self.get_arg(name).value
436    }
437
438    /// Find if an argument was explicitly set by the user
439    pub fn was_set<T>(&self, arg_handle: ArgHandle<T>) -> bool {
440        self.get_arg(arg_handle.name).was_set
441    }
442
443    /// Retrieve the rest of input vargs
444    pub fn get_vargs(&self) -> std::slice::Iter<'_, String> {
445        self.vargs.iter()
446    }
447
448    fn generate_help(&mut self) {
449        let examples = {
450            let mut res = String::new();
451            self.examples.iter().for_each(|s| {
452                res.push_str(&format!("  {program} {s}\n", program = self.program_name))
453            });
454
455            if !res.is_empty() {
456                res = "Examples:\n\n".to_owned() + &res;
457            }
458
459            res
460        };
461
462        self.help = format!(
463            " 
464{description}
465
466Help:
467
468  Usage: {program} {usage}
469
470  Options:
471
472{arguments}
473
474{examples}",
475            description = self.description,
476            program = self.program_name,
477            usage = self.usage,
478            arguments = self.generate_args_help_list(),
479        );
480    }
481
482    fn generate_args_help_list(&self) -> String {
483        let mut args_help = String::new();
484
485        let mut keys: Vec<&String> = self.args.keys().collect();
486        keys.sort();
487
488        for arg in keys.iter().map(|&k| self.args.get(k).unwrap()) {
489            let name = "--".to_owned() + arg.name;
490
491            let short_name = {
492                if !arg.short_name.is_empty() {
493                    "-".to_owned() + arg.short_name + ", "
494                } else {
495                    "".to_string()
496                }
497            };
498
499            let mut default = match &arg.default {
500                Value::Bool(true) => "true".to_string(),
501                Value::Txt(s) => {
502                    if s.is_empty() {
503                        "".to_string()
504                    } else {
505                        s.clone()
506                    }
507                }
508                Value::Num(n) => n.to_string(),
509                _ => "".to_string(),
510            };
511
512            let value = {
513                match arg.default {
514                    Value::Bool(_) => "".to_string(),
515                    _ => format!("=<{}>", arg.name),
516                }
517            };
518
519            if !default.is_empty() {
520                default = format!("[Default: {}]", default);
521            }
522
523            let line = &format!(
524                "{space:2}{short_name:>6}{name_and_val:25}{desc} {default}\n",
525                space = "",
526                name_and_val = name + &value,
527                desc = arg.description
528            );
529
530            args_help.push_str(line);
531        }
532
533        args_help
534    }
535
536    /// Get help as str
537    pub fn get_help_txt(&mut self) -> &str {
538        if self.help.is_empty() {
539            self.generate_help();
540        }
541
542        &self.help
543    }
544
545    /// Print the program help
546    pub fn print_help(&mut self) {
547        println!("{}", self.get_help_txt());
548    }
549
550    /// Print the program help and exit program with code
551    pub fn print_help_and_exit(&mut self, exit_code: i32) {
552        println!("{}", self.get_help_txt());
553        std::process::exit(exit_code);
554    }
555}
556
557#[derive(Debug, Clone, Copy, PartialEq)]
558pub struct ArgHandle<T> {
559    pub name: &'static str,
560    _p: PhantomData<T>,
561}