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}