simple_cli_parser/
lib.rs

1//! A crate for parsing commands and arguemnts passed to the console.
2//! 
3//! This can parse commands with various arguments.
4//! It currently only supports option arguments (or args beginning with a "-"),
5//! but an update for regular params and argument rules (like ordering) will be coming soon.
6//! 
7//! ## Getting Started
8//! To start, create a new [`Parser`] struct and add a couple of [`Arg`]s to it using the
9//! [`Parser::add_arg()`] or [`Parser::add_args()`] methods.
10//! Then, call the [`Parser::parse()`] method with [`std::env::Args`] passed in.
11//! 
12//! ```
13//! use cli_parser::{ Parser, Arg };
14//! use std::env;
15//! 
16//! fn main() {
17//!     let parser = Parser::new();
18//!     let my_arg = Arg::new().flag("help").short('h');
19//!     parser.add_arg(my_arg);
20//!     
21//!     let mut args = env::args();
22//! 
23//!     // Don't include the first argument
24//!     args.next();
25//!     
26//!     let hashmap = parser.parse(&mut args).unwrap();
27//! 
28//!     if hashmap.contains_key("help") {
29//!         println!("Help argument called!");
30//!     }
31//! }
32//! ```
33
34use std::cell::RefCell;
35use std::collections::HashMap;
36use std::error::Error;
37use std::fmt::Display;
38
39#[derive(Debug)]
40struct InvalidCommandError {
41    reason: InvalidCommandReasons
42}
43
44impl InvalidCommandError {
45    pub fn new(reason: InvalidCommandReasons) -> InvalidCommandError {
46        InvalidCommandError { reason }
47    }
48}
49
50impl Display for InvalidCommandError {
51    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
52        match &self.reason {
53            InvalidCommandReasons::Unexpected(s) => {
54                write!(f, "Invalid command, unexpected token '{}'", s)
55            },
56            InvalidCommandReasons::Duplicate(s) => {
57                write!(f, "Invalid command, duplicate token '{}'", s)
58            },
59            InvalidCommandReasons::Missing => {
60                write!(f, "Invalid command, missing argument")
61            }
62        }
63    }
64}
65
66impl Error for InvalidCommandError {
67    
68}
69
70#[derive(Debug)]
71enum InvalidCommandReasons {
72    Unexpected(String),
73    Missing,
74    Duplicate(String),
75}
76
77#[derive(Clone, Debug)]
78enum ArgTypes {
79    Param(bool),
80    Input,
81    Short(char),
82    None
83}
84
85/// Represents a single argument which can be passed to a [`Parser`].
86/// 
87/// # Example
88/// ```
89/// let arg = Arg::new().param("num");
90/// let parser = Parser::new();
91/// let mut args = std::env::args();
92/// args.next();
93/// 
94/// parser.add_arg(arg);
95/// let output = parser.parse(&mut args).unwrap();
96/// ```
97#[derive(Clone, Debug)]
98pub struct Arg {
99    name: String,
100    arg_type: ArgTypes,
101    expecting: bool,
102}
103
104impl Arg {
105    /// Create a new arg object, note you must call further methods on this for it to be useful.
106    /// 
107    /// # Example
108    /// ```
109    /// let arg = Arg::new();
110    /// ```
111    pub fn new() -> Arg {
112        
113        Arg { name: String::new(), arg_type: ArgTypes::None, expecting: false} 
114    }
115
116    /// A parameter argument, or one that does not expect any argument to come before it.
117    /// Note that the order that these are added to the parser matters.
118    /// 
119    /// # Example
120    /// ```
121    /// let arg = Arg::new().param("p1");
122    /// ```
123    /// `arg` is a required argument and the [`Parser::parse()`] will return an error if it is not present.
124    pub fn param(self, name: &str) -> Arg {
125        Arg { name: String::from(name), arg_type: ArgTypes::Param(false), expecting: false }
126    }
127
128    /// An optional argument that expects a value to follow directly after it.
129    /// 
130    /// # Example
131    /// ```
132    /// let arg = Arg::new().input("inp");
133    /// ```
134    /// Upon parsing, if `--inp` is one of the arguments called, `arg` will be in the output with whatever string comes next in the arguments.
135    pub fn input(self, name: &str) -> Arg {
136        Arg { name: String::from(name), arg_type: ArgTypes::Input, expecting: true }
137    }
138
139    /// A flag argument, or one that toggles a setting without expecting another token afterwards.
140    /// 
141    /// # Example
142    /// ```
143    /// let arg = Arg::new().flag("optional");
144    /// ```
145    /// Upon parsing, if `--optional` is one of the arguments called, `arg` will be in the output with the value `true`.
146    pub fn flag(self, name: &str) -> Arg {
147        Arg { name: String::from(name), arg_type: self.arg_type, expecting: false }
148    }
149
150    /// Sets a short option for the argument, allowing it to be called with a char rather than a string.
151    /// 
152    /// # Example
153    /// ```
154    /// let arg = Arg::new().flag("help").short('h');
155    /// ```
156    /// The `arg` variable can be called by `--help` or by `-h`.
157    pub fn short(self, ch: char) -> Arg {
158        Arg { name: self.name, arg_type: ArgTypes::Short(ch), expecting: self.expecting }
159    }
160
161    fn set_used(&mut self, used: bool) {
162        self.arg_type = ArgTypes::Param(used);
163    }
164}
165
166/// A struct that parses the command line for certain [`Arg`]s.
167/// 
168/// # Example
169/// ```
170/// let parser = Parser::new();
171/// let arg = Arg::new().param("num");
172/// let mut args = std::env::args();
173/// args.next();
174/// 
175/// parser.add_arg(arg);
176/// let output = parser.parse(&mut args).unwrap();
177/// ```
178pub struct Parser {
179    args: RefCell<Vec<Arg>>,
180}
181
182impl Parser {
183
184    /// Creates a new Parser struct.
185    /// 
186    /// # Example
187    /// ```
188    /// let parser = Parser::new();
189    /// ```
190    pub fn new() -> Parser {
191        Parser { args: RefCell::new(vec![]) }
192    }
193
194    /// Adds an argument to the parser.
195    /// 
196    /// # Example
197    /// ```
198    /// let arg = Arg::new().param("num");
199    /// let parser = Parser::new();
200    /// 
201    /// parser.add_arg(arg);
202    /// ```
203    pub fn add_arg(&self, arg: Arg) {
204        self.args.borrow_mut().push(arg);
205    }
206
207    /// Adds a vector of arguments to the parser.
208    /// 
209    /// # Example
210    /// ```
211    /// let mut args = vec![
212    ///     Arg::new().param("num1"),
213    ///     Arg::new().param("num2"),
214    /// ];
215    /// let parser = Parser::new();
216    /// 
217    /// parser.add_args(args);
218    /// ```
219    pub fn add_args(&self, mut args: Vec<Arg>) {
220        self.args.borrow_mut().append(&mut args);
221    }
222
223    /// Returns the arguments associated with this parser as a vector.
224    /// 
225    /// # Example
226    /// ```
227    /// let args = parser.args();
228    /// ```
229    pub fn args(&self) -> Vec<Arg> {
230        self.args.borrow().clone()
231    }
232
233    /// Returns the number of arguments associated with this parser.
234    /// 
235    /// # Example
236    /// ```
237    /// let n = parser.len();
238    /// ```
239    pub fn len(&self) -> usize {
240        self.args.borrow().len()
241    }
242
243    fn get_err(&self, reason: InvalidCommandReasons) -> Result<HashMap<String, Option<String>>, Box<dyn Error>> {
244        return Err(Box::new(InvalidCommandError::new(reason)))
245    }
246
247    /// Parses through the remaining arguments and returns a hashmap of arguments passed and their relevant values.
248    /// 
249    /// # Example
250    /// ```
251    /// let parser = Parser::new();
252    /// let mut args = vec![
253    ///     Arg::new().param("p1"),
254    ///     Arg::new().param("p2"),
255    ///     Arg::new().flag("help").short('h');
256    /// ];
257    /// parser.add_args(args);
258    /// 
259    /// let mut input_args = std::env::args();
260    /// input_args.next();
261    /// 
262    /// let hashmap = parser.parse(input_args).unwrap();
263    /// println!("p1: {}, p2: {}", hashmap.get("p1"), hashmap.get("p2"));
264    /// if hashmap.contains_key("help") {
265    ///     println!("Help requested!");
266    /// }
267    /// ```
268    pub fn parse(&self, args: &mut impl Iterator<Item = String>) -> Result<HashMap<String, Option<String>>, Box<dyn Error>> {
269        let mut hashmap: HashMap<String, Option<String>> = HashMap::new();
270        let mut prev_arg: Option<Box<Arg>> = None;
271        let mut args = args.peekable();
272        let mut parser_args = self.args.clone().take();
273
274        while let Some(c_arg) = args.next() {
275            if c_arg.starts_with("-") {
276                // Return error if calling a new argument without providing a follow up argument to the previous one
277                if prev_arg.is_some() {
278                    return self.get_err(InvalidCommandReasons::Unexpected(c_arg));
279                }
280
281                if c_arg.starts_with("--") {
282                    // Full arg
283                    let mut found = false;
284                    for arg in &parser_args {
285                        if c_arg.ends_with(&arg.name) && c_arg.len() == arg.name.len() + 2 {
286                            found = true;
287                            if arg.expecting {
288                                prev_arg = Some(Box::new(arg.clone()));
289                            } else {
290                                match hashmap.insert(arg.name.clone(), None) {
291                                    Some(_) => return self.get_err(InvalidCommandReasons::Duplicate(c_arg)),
292                                    None => {},
293                                };
294                                prev_arg = None;
295                            }
296                        }
297                    }
298                    if !found {
299                        return self.get_err(InvalidCommandReasons::Unexpected(c_arg));
300                    }
301                } else {
302                    // Short arg
303                    let mut found = false;
304                    for arg in &parser_args {
305                        if let ArgTypes::Short(c) = arg.arg_type {
306                            if c_arg.ends_with(c) && c_arg.len() == 2 {
307                                found = true;
308                                if arg.expecting {
309                                    prev_arg = Some(Box::new(arg.clone()));
310                                } else {
311                                    match hashmap.insert(arg.name.clone(), None) {
312                                        Some(_) => return self.get_err(InvalidCommandReasons::Duplicate(c_arg)),
313                                        None => {},
314                                    };
315                                    prev_arg = None;
316                                }
317                            }
318                        }
319                    }
320                    if !found {
321                        return self.get_err(InvalidCommandReasons::Unexpected(c_arg));
322                    }
323                }
324            } else {
325                // non-argument token
326                if prev_arg.is_none() {
327                    // params
328                    let mut found = false;
329                    for arg in &mut parser_args {
330                        if let ArgTypes::Param(used) = arg.arg_type {
331                            if used {
332                                continue;
333                            }
334                            
335                            match hashmap.insert(arg.name.clone(), Some(c_arg.clone())) {
336                                Some (_) => return self.get_err(InvalidCommandReasons::Duplicate(c_arg)),
337                                None => {}
338                            };
339                            arg.set_used(true);
340                            prev_arg = None;
341                            found = true;
342                            break;
343                        }
344                    }
345
346                    if !found {
347                        return self.get_err(InvalidCommandReasons::Unexpected(c_arg));
348                    }
349                } else {
350                    match hashmap.insert(prev_arg.unwrap().name, Some(c_arg.clone())) {
351                        Some (_) => return self.get_err(InvalidCommandReasons::Duplicate(c_arg)),
352                        None => {}
353                    }
354                    prev_arg = None;
355                }
356            };
357
358            if args.peek().is_none() && prev_arg.is_some() {
359                return self.get_err(InvalidCommandReasons::Missing);
360            }
361        }
362
363        for arg in parser_args {
364            if let ArgTypes::Param(false) = arg.arg_type {
365                return self.get_err(InvalidCommandReasons::Missing);
366            }
367        }
368
369        Ok(hashmap)
370    }
371}
372
373
374
375// Tests
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn create_arg() {
382        let default = Arg::new().input("default");
383        assert_eq!(default.name, "default");
384        assert_eq!(default.expecting, true);
385        
386        let short = Arg::new().input("short").short('s');
387        assert_eq!(short.name, "short");
388        if let ArgTypes::Short(c) = short.arg_type {
389            assert_eq!(c, 's');
390        } else {
391            assert!(false);
392        }
393        assert_eq!(short.expecting, true);
394
395        let flag = Arg::new().flag("flag").short( 'f');
396        assert_eq!(flag.expecting, false);
397        if let ArgTypes::Short(c) = flag.arg_type {
398            assert_eq!(c, 'f');
399        } else {
400            assert!(false);
401        }
402        assert_eq!(flag.name, "flag");
403
404        let mut param = Arg::new().param("param");
405        assert_eq!(param.expecting, false);
406        if let ArgTypes::Param(used) = param.arg_type {
407            assert!(!used);
408        } else {
409            assert!(false);
410        }
411        assert_eq!(param.name, "param");
412
413        param.set_used(true);
414        if let ArgTypes::Param(used) = param.arg_type {
415            assert!(used);
416        } else {
417            assert!(false);
418        }
419    }
420
421    #[test]
422    fn append_args() {
423        let default = Arg::new().input("default");
424        let short = Arg::new().input("short").short('s');
425        let flag = Arg::new().flag("flag").short( 'f');
426
427        let parser = Parser::new();
428        parser.add_arg(default);
429        assert_eq!(parser.len(), 1);
430        parser.add_args(vec![short, flag]);
431        assert_eq!(parser.len(), 3);
432        assert_eq!(parser.args()[2].name, "flag");
433    }
434
435    #[test]
436    fn test_parse_err() {
437        let default = Arg::new().input("default");
438        let short = Arg::new().input("short").short('s');
439        let flag = Arg::new().flag("flag").short( 'f');
440        let param1 = Arg::new().param("p1");
441        let param2 = Arg::new().param("p2");
442
443        let parser = Parser::new();
444        parser.add_args(vec![default, short, flag, param1, param2]);
445
446        // Tests err on duplicate value
447        let mut cmd = "--short short_inp -s s_inp p1 p2"
448            .split_whitespace()
449            .map(|s| { String::from(s) });
450
451        let res = parser.parse(&mut cmd);
452        assert!(res.is_err());
453        
454        // Tests err on invalid command order
455        let mut cmd = "--default -f p1 p2"
456            .split_whitespace()
457            .map(|s| { String::from(s) });
458
459        let res = parser.parse(&mut cmd);
460        assert!(res.is_err());
461
462        // Tests err on unexpected token
463        let mut cmd = "p1 p2 -f dsjfhkjuh"
464        .split_whitespace()
465        .map(|s| { String::from(s) });
466
467        let res = parser.parse(&mut cmd);
468        assert!(res.is_err());
469
470        // Tests err on missing token
471        let mut cmd = "p1 p2 --default"
472            .split_whitespace()
473            .map(|s| { String::from(s) });
474
475        let res = parser.parse(&mut cmd);
476        assert!(res.is_err());
477
478        // Tests err on missing param
479        let mut cmd = "p1 -f"
480            .split_whitespace()
481            .map(|s| { String::from(s) });
482
483        let res = parser.parse(&mut cmd);
484        assert!(res.is_err());
485
486        // Tests err on unrecognized arg
487        let mut cmd = "p1 p2 --doesntexist"
488            .split_whitespace()
489            .map(|s| { String::from(s) });
490
491        let res = parser.parse(&mut cmd);
492        assert!(res.is_err());
493
494    }
495
496    #[test]
497    pub fn test_parse_success() {
498        let default = Arg::new().input("default");
499        let short = Arg::new().input("short").short('s');
500        let flag = Arg::new().flag("flag").short( 'f');
501        let param1 = Arg::new().param("file");
502        let param2 = Arg::new().param("path");
503
504        let parser = Parser::new();
505        parser.add_args(vec![default, short, flag, param1, param2]);
506
507        let mut cmd = "--default def_arg filename -s s_arg -f pathname"
508            .split_whitespace()
509            .map(|s| { String::from(s) });
510
511        let res = parser.parse(&mut cmd);
512        assert!(res.is_ok());
513
514        let res = res.unwrap();
515        assert_eq!(res.len(), 5);
516        assert!(res.contains_key("default"));
517        assert_eq!(res.get("default").unwrap(), &Some(String::from("def_arg")));
518        assert!(res.contains_key("short"));
519        assert_eq!(res.get("short").unwrap(), &Some(String::from("s_arg")));
520        assert!(res.contains_key("flag"));
521        assert_eq!(res.get("flag").unwrap(), &None);
522        assert!(res.contains_key("file"));
523        assert_eq!(res.get("file").unwrap(), &Some(String::from("filename")));
524        assert!(res.contains_key("path"));
525        assert_eq!(res.get("path").unwrap(), &Some(String::from("pathname")));
526    }
527}