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 starting argument (executable path)".to_owned())
336        })?;
337
338        if self.program_name.is_empty() {
339            self.program_name = input_name
340        }
341
342        for input in args_iter {
343            // Trimming - prefixes
344            let trimmed_input = input.trim_start_matches('-').to_owned();
345            if trimmed_input.is_empty() {
346                return Err(Error::Parse("Invalid argument starting with -".to_owned()));
347            }
348
349            // Argument was not prefixed with - or --
350            // We add it to the VARGS bucket
351            if trimmed_input == input {
352                vargs.push(input);
353                continue; // We continue to next arg
354            }
355
356            let mut input_arg = trimmed_input;
357            let mut input_val = String::new();
358
359            // Try Split arg=value
360            //
361            // If value is not present, value string stays empty
362            if let Some((left, right)) = input_arg.split_once('=') {
363                if left.is_empty() {
364                    return Err(Error::Parse(format!("Argument missing before ={}", right)));
365                }
366
367                if right.is_empty() {
368                    return Err(Error::Parse(format!("Value missing after {}=", left)));
369                }
370                input_val = right.to_owned();
371                input_arg = left.to_owned();
372            }
373
374            // Help
375            if input_arg == "help" || input_arg == "h" {
376                self.print_help_and_exit(0);
377            }
378
379            // Find Arg
380            let found_arg = self.args.iter_mut().find_map(|(_, a)| {
381                if input_arg == a.name || input_arg == a.short_name {
382                    Some(a)
383                } else {
384                    None
385                }
386            });
387
388            if let Some(argument) = found_arg {
389                argument.was_set = true;
390
391                // Boolean arguments/flags can be set without an explicit value
392                if input_val.is_empty() {
393                    if matches!(argument.value, Value::Bool(_)) {
394                        argument.value = Value::Bool(true)
395                    }
396                }
397                // Argument with explicit value assignment arg=val
398                else {
399                    argument.value = match argument.value {
400                        Value::Txt(_) => Value::Txt(input_val),
401                        Value::Num(_) => {
402                            Value::parse_as_num(&input_val).map_err(|_| Error::ParseValue {
403                                value: input_val,
404                                arg: input_arg,
405                            })?
406                        }
407                        Value::Bool(_) => {
408                            Value::parse_as_bool(&input_val).map_err(|_| Error::ParseValue {
409                                value: input_val,
410                                arg: input_arg,
411                            })?
412                        }
413                    }
414                }
415            } else {
416                return Err(Error::UnknownArg(input_arg));
417            }
418        }
419
420        self.vargs = vargs;
421        Ok(())
422    }
423
424    fn get_arg(&self, name: &str) -> &Argument {
425        self.args
426            .get(name)
427            .unwrap_or_else(|| panic!("Could not find argument: {name}"))
428    }
429
430    fn get_val(&self, name: &str) -> &Value {
431        &self.get_arg(name).value
432    }
433
434    /// Find if an argument was explicitly set by the user
435    pub fn was_set<T>(&self, arg_handle: ArgHandle<T>) -> bool {
436        self.get_arg(arg_handle.name).was_set
437    }
438
439    /// Retrieve the rest of input vargs
440    pub fn get_vargs(&self) -> std::slice::Iter<'_, String> {
441        self.vargs.iter()
442    }
443
444    fn generate_help(&mut self) {
445        let examples = {
446            let mut res = String::new();
447            self.examples.iter().for_each(|s| {
448                res.push_str(&format!("  {program} {s}\n", program = self.program_name))
449            });
450
451            if !res.is_empty() {
452                res = "Examples:\n\n".to_owned() + &res;
453            }
454
455            res
456        };
457
458        self.help = format!(
459            " in the given order
460{description}
461
462Help:
463
464  Usage: {program} {usage}
465
466  Options:
467
468{arguments}
469
470{examples}",
471            description = self.description,
472            program = self.program_name,
473            usage = self.usage,
474            arguments = self.generate_args_help_list(),
475        );
476    }
477
478    fn generate_args_help_list(&self) -> String {
479        let mut args_help = String::new();
480
481        let mut keys: Vec<&String> = self.args.keys().collect();
482        keys.sort();
483
484        for arg in keys.iter().map(|&k| self.args.get(k).unwrap()) {
485            let name = "--".to_owned() + arg.name;
486
487            let short_name = {
488                if !arg.short_name.is_empty() {
489                    "-".to_owned() + arg.short_name + ", "
490                } else {
491                    "".to_string()
492                }
493            };
494
495            let mut default = match &arg.default {
496                Value::Bool(true) => "true".to_string(),
497                Value::Txt(s) => {
498                    if s.is_empty() {
499                        "".to_string()
500                    } else {
501                        s.clone()
502                    }
503                }
504                Value::Num(n) => n.to_string(),
505                _ => "".to_string(),
506            };
507
508            let value = {
509                match arg.default {
510                    Value::Bool(_) => "".to_string(),
511                    _ => format!("=<{}>", arg.name),
512                }
513            };
514
515            if !default.is_empty() {
516                default = format!("[Default: {}]", default);
517            }
518
519            let line = &format!(
520                "{space:2}{short_name:>6}{name_and_val:25}{desc} {default}\n",
521                space = "",
522                name_and_val = name + &value,
523                desc = arg.description
524            );
525
526            args_help.push_str(line);
527        }
528
529        args_help
530    }
531
532    /// Get help as str
533    pub fn get_help_txt(&mut self) -> &str {
534        if self.help.is_empty() {
535            self.generate_help();
536        }
537
538        &self.help
539    }
540
541    /// Print the program help
542    pub fn print_help(&mut self) {
543        println!("{}", self.get_help_txt());
544    }
545
546    /// Print the program help and exit program with code
547    pub fn print_help_and_exit(&mut self, exit_code: i32) {
548        println!("{}", self.get_help_txt());
549        std::process::exit(exit_code);
550    }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq)]
554pub struct ArgHandle<T> {
555    pub name: &'static str,
556    _p: PhantomData<T>,
557}