clappers/
lib.rs

1//! Clappers -  Command Line Argument Parsing Particularly Easy, Relatively Straightforward!
2//!
3//! `Clappers` aims to be the most user-friendly command line argument
4//! parser this side of the Milky Way. You configure a `Clappers`
5//! parser with the command line arguments you care about via
6//! chaining, with the last link in the chain being a call to
7//! `build()`. Command line argument values are then retrieved via
8//! getters on the `Clappers` parser.
9//!
10//! ## Example 1 - A Minimal Directory Listing
11//!
12//! ```
13//! use clappers::Clappers;
14//!
15//! fn main() {
16//!     let clappers = Clappers::new()
17//!         .set_flags(vec![
18//!             "h|help",
19//!             "l",
20//!             "R|recursive",
21//!         ])
22//!         .build();
23//!
24//!     if clappers.get_flag("help") {
25//!         println!("
26//!             usage: ls [arguments] [FILE1]...
27//!
28//!             Arguments:
29//!                 -h|--help        Print this help
30//!                 -l               Use a long listing format
31//!                 -R|--recursive   List subdirectories recursively
32//!         ");
33//!     }
34//!
35//!     if clappers.get_flag("l") {
36//!         // Show more details than usual
37//!     }
38//!
39//!     if clappers.get_flag("R") {
40//!         // Recurse into subdirectories
41//!     }
42//!
43//!     if clappers.get_flag("recursive") {
44//!         // This will also recurse
45//!     }
46//!
47//!     let filenames: Vec<String> = clappers.get_leftovers();
48//!
49//!     // ...
50//! }
51//! ```
52//!
53//! ## Example 2 - A Minimal Compiler
54//!
55//! ```
56//! use clappers::Clappers;
57//!
58//! fn main() {
59//!     let clappers = Clappers::new()
60//!         .set_flags(vec![
61//!             "h|help",
62//!             "v|verbose",
63//!         ])
64//!         .set_singles(vec![
65//!             "o|output",
66//!         ])
67//!         .set_multiples(vec![
68//!             "i|input",
69//!             "I",
70//!             "L",
71//!         ])
72//!         .build();
73//!
74//!     if clappers.get_flag("help") {
75//!         println!("
76//!             usage: compile [arguments]
77//!
78//!             Arguments:
79//!                 -h|--help                        Print this help
80//!                 -v|--verbose                     Enable verbose mode
81//!                 -I <dir1> ... <dirN>             Include directories
82//!                 -L <dir1> ... <dirN>             Library directories
83//!                 -i|--input <file1> ... <fileN>   Input filenames
84//!                 -o|--output filename             Output filename
85//!         ");
86//!     }
87//!
88//!     let output_filename = clappers.get_single("output");
89//!     let input_filenames: Vec<String> = clappers.get_multiple("input");
90//!
91//!     // ...
92//! }
93//! ```
94//!
95//! # Argument Types
96//!
97//! There are four types of arguments:
98//!
99//! 1. Flags
100//! 2. Single value
101//! 3. Multiple value
102//! 4. Leftovers
103//!
104//! ## 1. Flag Arguments
105//!
106//! Flag arguments are `true` if they were supplied on the command
107//! line, and `false` otherwise e.g:
108//!
109//!```ignore
110//! -h
111//! --help
112//! -v
113//! --verbose
114//!```
115//!
116//! *Note:* flag arguments do not take values
117//!
118//! ## 2. Single Value Arguments
119//!
120//! Single value arguments contain a single `String` value if they
121//! were supplied on the command line, and empty `String` otherwise
122//! e.g:
123//!
124//!```ignore
125//! -o filename.txt
126//! --output filename.txt
127//! -u Zelensky
128//! --username Zelensky
129//!```
130//!
131//! ## 3. Multiple Value Arguments
132//!
133//! Multiple value arguments contain at least a single `String` value
134//! if they were supplied on the command line, and empty `String`
135//! otherwise e.g:
136//!
137//!```ignore
138//! -i file1.txt
139//! --input file1.txt
140//! --host host1
141//!```
142//!
143//! They can also contain multiple values, by repetition on the
144//! command line e.g:
145//!
146//!```ignore
147//! -i file1.txt -i file2.txt ... -i fileN.txt
148//! --host host1 --host host2 ... --host hostN
149//!```
150//!
151//! The following format also works, reading from the first value
152//! until either the next argument is reached, or until the end of the
153//! entire command line arguments e.g:
154//!
155//!```ignore
156//! -i file1.txt file2.txt ... fileN.txt -n next_argument
157//! --host host1 host2 hostN
158//!```
159//!
160//! ## 4. Leftover Arguments
161//!
162//! Leftover argument values are values supplied on the command line
163//! that are not associated with any configuration. These includes:
164//!
165//! - any values when no other argument types have been supplied e.g:
166//!
167//!```ignore
168//! ls file1 file2... fileN
169//!```
170//!
171//! - any values after the double-dash argument e.g:
172//!
173//!```ignore
174//! ls -l -R  -- file1 file2... fileN`
175//!```
176//!
177//! - any value supplied to flags, because flags do not accept values
178//!
179//! - any remaining values supplied to singles value arguments,
180//! because these only take a one value
181//!
182//! # Caveats
183//!
184//! - Combining flags is currently unsupported i.e the following does
185//! not work:
186//!
187//!```ignore
188//! tar -zcf filename.tar.gz *
189//!```
190//!
191//! - Equals-Value is currently unsupported i.e the following does not
192//! work:
193//!
194//!```ignore
195//! tar -zc --file=filename.tar.gz
196//!```
197//!
198//! - Commands with their own separate `Clappers` parser is currently
199//! unsupported i.e the following does not work:
200//!
201//!```ignore
202//! apt-get -y install -f cargo
203//! apt-get update -f
204//!```
205//!
206//! - Command line argument values are always `String` types. This was
207//! by design, and no convenience functions are planned. To convert a
208//! `String` to something else, use `String`'s built-in `parse()`
209//! function instead:
210//!
211//!```
212//! use clappers::Clappers;
213//!
214//! fn main() {
215//!     let clappers = Clappers::new()
216//!         .set_singles(vec!["number"])
217//!         .build();
218//!
219//!     let number: i32 = clappers.get_single("number").parse().unwrap_or(0);
220//!
221//!     println!("Double {number} is {}", number * 2);
222//! }
223//!```
224//!
225
226use std::{
227    collections::{HashMap, HashSet},
228    env,
229};
230
231#[derive(Clone, Debug)]
232struct ConfigType {
233    name: HashSet<String>,
234    aliases: HashMap<String, String>,
235}
236
237impl ConfigType {
238    fn new() -> Self {
239        Self {
240            name: HashSet::new(),
241            aliases: HashMap::new(),
242        }
243    }
244
245    fn add_to_config(&mut self, arg_specs: Vec<&str>) {
246        for arg_spec in arg_specs {
247            let arguments: Vec<&str> = arg_spec.split('|').collect();
248
249            if arguments.is_empty() {
250                continue;
251            }
252
253            self.name.insert(arguments[0].to_string());
254
255            for argument in &arguments {
256                self.aliases
257                    .insert(argument.to_string(), arguments[0].to_string());
258            }
259        }
260    }
261}
262
263#[derive(Clone, Debug)]
264struct Config {
265    flags: ConfigType,
266    singles: ConfigType,
267    multiples: ConfigType,
268}
269
270#[derive(Clone, Debug)]
271struct Values {
272    flags: HashSet<String>,
273    singles: HashMap<String, String>,
274    multiples: HashMap<String, Vec<String>>,
275}
276
277#[derive(Clone, Debug)]
278pub struct Clappers {
279    config: Config,
280    values: Values,
281}
282
283impl Clappers {
284    /// Creates a `Clappers` parser
285    ///
286    /// # Parameters
287    ///
288    /// None.
289    ///
290    /// # Return value
291    ///
292    /// An empty `Clappers` parser, which is ready to be configured by
293    /// chaining:
294    ///
295    /// - `set_flags()`
296    /// - `set_singles()`
297    /// - `set_multiples()`
298    ///
299    /// Once configured, `build()` is chained last to build the actual
300    /// command line arguments parser
301    ///
302    /// # Example
303    ///
304    /// ```
305    /// use clappers::Clappers;
306    ///
307    /// fn main() {
308    ///     let clappers = Clappers::new()
309    ///         .set_flags(vec!["h|help", "v|verbose"])
310    ///         .set_singles(vec!["o|output", "u|username"])
311    ///         .set_multiples(vec!["i|input", "host"])
312    ///         .build();
313    ///
314    ///     // ...
315    /// }
316    /// ```
317    ///
318    pub fn new() -> Self {
319        Self {
320            config: Config {
321                flags: ConfigType::new(),
322                singles: ConfigType::new(),
323                multiples: ConfigType::new(),
324            },
325            values: Values {
326                flags: HashSet::new(),
327                singles: HashMap::new(),
328                multiples: HashMap::new(),
329            },
330        }
331    }
332
333    /// Add flag argument parsing to the `Clappers` config
334    ///
335    /// Flag arguments are `true` if they were supplied on the command
336    /// line, and `false` otherwise e.g:
337    ///
338    ///```ignore
339    /// -h
340    /// --help
341    /// -v
342    /// --verbose
343    ///```
344    ///
345    /// *Note:* flag arguments do not take values
346    ///
347    /// # Parameters
348    ///
349    /// `arg_specs` specifies which flag arguments on the command line
350    /// to care about.
351    ///
352    /// Each `arg_spec` contains "|" separated flag argument alias
353    /// names e.g:
354    ///
355    ///```ignore
356    /// clappers.set_flags(vec!["h|help", "v|verbose"]);
357    ///```
358    ///
359    /// # Return value
360    ///
361    /// The `Clappers` parser so that it can be chained
362    ///
363    /// # Example
364    ///
365    /// ```
366    /// use clappers::Clappers;
367    ///
368    /// fn main() {
369    ///     let clappers = Clappers::new()
370    ///         .set_flags(vec!["h|help", "v|verbose"])
371    ///         .set_singles(vec!["o|output", "u|username"])
372    ///         .set_multiples(vec!["i|input", "host"])
373    ///         .build();
374    ///
375    ///     // ...
376    /// }
377    /// ```
378    ///
379    pub fn set_flags(mut self, arg_specs: Vec<&str>) -> Self {
380        self.config.flags.add_to_config(arg_specs);
381        self
382    }
383
384    /// Add single value argument parsing to the `Clappers` config
385    ///
386    /// Single value arguments contain a single `String` value if they
387    /// were supplied on the command line, and empty `String`
388    /// otherwise e.g:
389    ///
390    ///```ignore
391    /// -o filename.txt
392    /// --output filename.txt
393    /// -u Zelensky
394    /// --username Zelensky
395    ///```
396    ///
397    /// # Parameters
398    ///
399    /// `arg_specs` specifies which single value arguments on the
400    /// command line to care about.
401    ///
402    /// Each `arg_spec` contains "|" separated single value argument
403    /// alias names e.g:
404    ///
405    ///```ignore
406    /// clappers.set_singles(vec!["o|output", "u|username"]);
407    ///```
408    ///
409    /// # Return value
410    ///
411    /// The `Clappers` parser so that it can be chained
412    ///
413    /// # Example
414    ///
415    /// ```
416    /// use clappers::Clappers;
417    ///
418    /// fn main() {
419    ///     let clappers = Clappers::new()
420    ///         .set_flags(vec!["h|help", "v|verbose"])
421    ///         .set_singles(vec!["o|output", "u|username"])
422    ///         .set_multiples(vec!["i|input", "host"])
423    ///         .build();
424    ///
425    ///     // ...
426    /// }
427    /// ```
428    ///
429    pub fn set_singles(mut self, arg_specs: Vec<&str>) -> Self {
430        self.config.singles.add_to_config(arg_specs);
431        self
432    }
433
434    /// Add multiple value argument parsing to the `Clappers` config
435    ///
436    /// Multiple value arguments contain at least a singly populated
437    /// `Vec<String>` value if they were supplied on the command line,
438    /// and empty `Vec<String>` otherwise e.g:
439    ///
440    ///```ignore
441    /// -i file1.txt
442    /// --input file1.txt
443    /// --host host1
444    ///```
445    ///
446    /// They can also contain multiple values, by repetition on the
447    /// command line e.g:
448    ///
449    ///```ignore
450    /// -i file1.txt -i file2.txt ... -i fileN.txt
451    /// --host host1 --host host2 ... --host hostN
452    ///```
453    ///
454    /// The following format also works, reading from the first value
455    /// until either the next argument is reached, or until the end of
456    /// the entire command line arguments e.g:
457    ///
458    ///```ignore
459    /// -i file1.txt file2.txt ... fileN.txt -n next_argument
460    /// --host host1 host2 hostN
461    ///```
462    ///
463    /// # Parameters
464    ///
465    /// `arg_specs` specifies which multiple value arguments on the
466    /// command line to care about.
467    ///
468    /// Each `arg_spec` contains "|" separated multiple value argument
469    /// alias names e.g:
470    ///
471    ///```ignore
472    /// clappers.set_multiples(vec!["i|input", "host"]);
473    ///```
474    ///
475    /// # Return value
476    ///
477    /// The `Clappers` parser so that it can be chained
478    ///
479    /// # Example
480    ///
481    /// ```
482    /// use clappers::Clappers;
483    ///
484    /// fn main() {
485    ///     let clappers = Clappers::new()
486    ///         .set_flags(vec!["h|help", "v|verbose"])
487    ///         .set_singles(vec!["o|output", "u|username"])
488    ///         .set_multiples(vec!["i|input", "host"])
489    ///         .build();
490    ///
491    ///     // ...
492    /// }
493    /// ```
494    ///
495    pub fn set_multiples(mut self, arg_specs: Vec<&str>) -> Self {
496        self.config.multiples.add_to_config(arg_specs);
497        self
498    }
499
500    /// Build the command line arguments parser with the current `Clappers` config
501    ///
502    /// # Parameters
503    ///
504    /// None
505    ///
506    /// # Return value
507    ///
508    /// The `Clappers` parser containing the parsed command line
509    /// arguments values, accessed with:
510    ///
511    /// - `get_flags()`
512    /// - `get_singles()`
513    /// - `get_multiples()`
514    /// - `get_leftovers()`
515    ///
516    /// # Example
517    ///
518    /// ```
519    /// use clappers::Clappers;
520    ///
521    /// fn main() {
522    ///     let clappers = Clappers::new()
523    ///         .set_flags(vec!["h|help", "v|verbose"])
524    ///         .set_singles(vec!["o|output", "u|username"])
525    ///         .set_multiples(vec!["i|input", "host"])
526    ///         .build();
527    ///
528    ///     if clappers.get_flag("help") {
529    ///         // Show help text
530    ///     }
531    ///
532    ///     // ...
533    /// }
534    /// ```
535    ///
536    pub fn build(mut self) -> Self {
537        // setup "leftovers" before parsing
538        self.config.multiples.name.insert("".to_string());
539        self.config
540            .multiples
541            .aliases
542            .insert("".to_string(), "".to_string());
543
544        let mut args = env::args().peekable();
545
546        // discard argv[0]
547        args.next();
548
549        while let Some(mut next) = args.next() {
550            if next.starts_with('-') {
551                next = next.split_off(1);
552
553                if next.starts_with('-') {
554                    next = next.split_off(1);
555                }
556
557                if let Some(name) = self.config.flags.aliases.get(&next) {
558                    self.values.flags.insert(name.to_string());
559                } else if let Some(name) = self.config.singles.aliases.get(&next) {
560                    if let Some(v) = args.peek() {
561                        if v.starts_with('-') {
562                            continue;
563                        } else {
564                            self.values
565                                .singles
566                                .insert(name.to_string(), args.next().unwrap());
567                        }
568                    }
569                } else if let Some(name) = self.config.multiples.aliases.get(&next) {
570                    if self.values.multiples.get_mut(name).is_none() {
571                        self.values.multiples.insert(name.clone(), vec![]);
572                    }
573
574                    while let Some(value) = args.peek() {
575                        if value.starts_with('-') {
576                            break;
577                        } else {
578                            self.values
579                                .multiples
580                                .get_mut(name)
581                                .unwrap()
582                                .push(args.next().unwrap());
583                        }
584                    }
585                }
586            } else {
587                if self.values.multiples.get_mut("").is_none() {
588                    self.values.multiples.insert("".to_string(), vec![]);
589                }
590
591                self.values.multiples.get_mut("").unwrap().push(next);
592            }
593        }
594
595        self
596    }
597
598    /// Check if the flag was supplied on the command line for the specified argument
599    ///
600    /// # Parameters
601    ///
602    /// `argument` is any alias of the specified argument
603    ///
604    /// # Return value
605    ///
606    /// `true` if the flag was supplied on the command line, and
607    /// `false` otherwise
608    ///
609    /// # Example
610    ///
611    /// ```
612    /// use clappers::Clappers;
613    ///
614    /// fn main() {
615    ///     let clappers = Clappers::new()
616    ///         .set_flags(vec!["h|help"])
617    ///         .build();
618    ///
619    ///     if clappers.get_flag("help") {
620    ///         // Show help text
621    ///     }
622    ///
623    ///     if clappers.get_flag("h") {
624    ///         // This will also show the help text
625    ///     }
626    ///
627    ///     // ...
628    /// }
629    /// ```
630    ///
631    pub fn get_flag(&self, argument: &str) -> bool {
632        self.config
633            .flags
634            .aliases
635            .get(argument)
636            .map_or(false, |f| self.values.flags.contains(f))
637    }
638
639    /// Get the single value supplied on the command line for the specified argument
640    ///
641    /// # Parameters
642    ///
643    /// `argument` is any alias of the specified argument
644    ///
645    /// # Return value
646    ///
647    /// The single `String` value if they were supplied on the command
648    /// line, and empty `String` otherwise
649    ///
650    /// # Example
651    ///
652    /// ```
653    /// use clappers::Clappers;
654    ///
655    /// fn main() {
656    ///     let clappers = Clappers::new()
657    ///         .set_singles(vec!["output"])
658    ///         .build();
659    ///
660    ///     println!("Output filename is {}", clappers.get_single("output"));
661    ///
662    ///     // ...
663    /// }
664    /// ```
665    ///
666    pub fn get_single(&self, argument: &str) -> String {
667        self.config
668            .singles
669            .aliases
670            .get(argument)
671            .map_or("".to_string(), |s| {
672                self.values
673                    .singles
674                    .get(s)
675                    .unwrap_or(&"".to_string())
676                    .to_string()
677            })
678    }
679
680    /// Get multiple values supplied on the command line for the specified argument
681    ///
682    /// # Parameters
683    ///
684    /// `argument` is any alias of the specified argument
685    ///
686    /// # Return value
687    ///
688    /// Multiple `String` values if they were supplied on the command
689    /// line, and empty `Vec<String>` otherwise
690    ///
691    /// # Example
692    ///
693    /// ```
694    /// use clappers::Clappers;
695    ///
696    /// fn main() {
697    ///     let clappers = Clappers::new()
698    ///         .set_multiples(vec!["input"])
699    ///         .build();
700    ///
701    ///     println!("Input filenames are {:#?}", clappers.get_multiple("input"));
702    ///
703    ///     // ...
704    /// }
705    /// ```
706    ///
707    pub fn get_multiple(&self, argument: &str) -> Vec<String> {
708        self.config
709            .multiples
710            .aliases
711            .get(argument)
712            .map_or(vec![], |m| {
713                self.values.multiples.get(m).unwrap_or(&vec![]).to_vec()
714            })
715    }
716
717    /// Get all values supplied on the command line that are not associated with any argument
718    ///
719    /// # Parameters
720    ///
721    /// None
722    ///
723    /// # Return value
724    ///
725    /// All `String` values supplied on the command line that are not
726    /// associated with any argument, and empty `Vec<String>`
727    /// otherwise
728    ///
729    /// # Example
730    ///
731    /// ```
732    /// use clappers::Clappers;
733    ///
734    /// fn main() {
735    ///     let clappers = Clappers::new()
736    ///         .build();
737    ///
738    ///     println!("`ls *` returned the following filenames: {:#?}", clappers.get_leftovers());
739    ///
740    ///     // ...
741    /// }
742    /// ```
743    ///
744    pub fn get_leftovers(&self) -> Vec<String> {
745        self.get_multiple("")
746    }
747}