Skip to main content

getopt2/
getopt2.rs

1/*
2 * Copyright (c) Radim Kolar 2013, 2018, 2023, 2025.
3 * SPDX-License-Identifier: MIT
4 *
5 * getopt2 library is licensed under MIT license:
6 *   https://spdx.org/licenses/MIT.html
7*/
8
9//!  # getopt2 command line parser
10//!
11//!  ### Command line argument parser with GNU parsing rules and double dash '--' support.
12//!
13//!  `getopt2::`[`new`] parses the command line elements and isolates arguments from options.
14//!  It returns a [`getopt`] structure where you can query options and use isolated arguments.
15//!
16//!  An element that starts with '-' (and is not exactly "-" or "--") is an option element.
17//!  The characters of this element (aside from the initial '-') are option characters.
18//!  A double dash `--` can be used to indicate the end of options; any arguments following
19//!  it are treated as positional arguments.
20//!
21//!  #### Example
22//!  ```rust
23//!  use std::env::args;
24//!  use getopt2::hideBin;
25//!  let rc = getopt2::new(hideBin(args()), "ab:c");
26//!  if let Ok(g) = rc {
27//!     // command line options parsed sucessfully
28//!     if let Some(str) = g.options.get(&'b') {
29//!        // handle b argument
30//!        println!("option -b have {} argument", str);
31//!     };
32//!  };
33//!  ```
34//!  #### Reference
35//!
36//!  1. [POSIX getopt](https://pubs.opengroup.org/onlinepubs/9799919799/functions/getopt.html) function.
37//!  1. [GNU libc getopt](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) function.
38//!
39//!  [`new`]: ./fn.new.html
40//!  [`getopt`]: ./struct.getopt.html
41
42#![forbid(unsafe_code)]
43#![forbid(missing_docs)]
44#![allow(non_camel_case_types)]
45#![allow(unused_parens)]
46#![allow(non_snake_case)]
47#![allow(unused_doc_comments)]
48#![deny(rustdoc::bare_urls)]
49#![deny(rustdoc::broken_intra_doc_links)]
50#![deny(rustdoc::missing_crate_level_docs)]
51#![deny(rustdoc::invalid_codeblock_attributes)]
52#![deny(rustdoc::invalid_rust_codeblocks)]
53
54use std::io::Result;
55use std::io::Error;
56use std::io::ErrorKind;
57use std::collections::HashMap;
58
59/**
60Parsed command line options.
61
62Created by [`new`] function. Structure contains isolated command line
63arguments and collected options with their optional values.
64
65Structure can contain options which are not listed in *optstring*
66passed to [`new`] function. For strict parse mode pass this
67structure to [`validate`] function.
68
69[`new`]: ./fn.new.html
70[`validate`]: ./fn.validate.html
71*/
72pub struct getopt {
73   /**
74   Map of command line options and their optional values extracted from command line arguments.
75
76   If an option does not have a value, an empty String "" is stored.
77   */
78   pub options: HashMap<char, String>,
79   /** Isolated command line arguments without options. */
80   pub arguments: Vec<String>,
81   /** Map indicating whether an option has a required argument.
82
83   This map contains all recognized options.
84   Inclusion of an option in this map does not mean that the option must always be present
85   or supplied as a command line argument. */
86   pub option_has_arg: HashMap<char, argument>,
87}
88
89/**
90  Information if option have an argument
91*/
92#[derive(PartialEq, Eq, Debug, Clone, Copy)]
93pub enum argument {
94   /** No argument */
95   NO,
96   /** Has argument */
97   YES,
98   /** Optional argument */
99   OPTIONAL,
100}
101
102impl getopt {
103   /**
104      Return number of command line arguments.
105
106      Its convience shortcut for getopt.arguments.len()
107
108     #### Example
109     ```rust
110     use std::env::args;
111     use getopt2::hideBin;
112
113     let getopt_rc = getopt2::new(hideBin(args()), "ab:c");
114     if let Ok(g) = getopt_rc {
115        println!("Number of command line arguments is {}", g.len());
116     };
117     ```
118   */
119   pub fn len(&self) -> usize {
120      self.arguments.len()
121   }
122
123   /**
124      Returns Iterator over command line arguments.
125
126      #### Example
127      ```rust
128      use std::env::args;
129      use getopt2::hideBin;
130
131      let getopt_rc = getopt2::new(hideBin(args()), "abc");
132      if let Ok(my_getopt) = getopt_rc {
133         for arg in my_getopt.iter() {
134            println!("Argument: {}", arg);
135         }
136      }
137      ```
138   */
139   pub fn iter(&self) -> std::slice::Iter<'_, String> {
140      self.arguments.iter()
141   }
142
143   /**
144      Return command line option value.
145
146      Its convience shortcut for getopt.options.get()
147
148      #### Return value
149      1. If option were supplied on command line returned value is `Some`.
150      1. If option doesn't have argument or argument is
151         missing, reference to empty `String` is returned.
152      1. If option were not supplied by user returned value is `None`.
153
154      #### Example
155      ```rust
156      use std::env::args;
157      use getopt2::hideBin;
158
159      let getopt_rc = getopt2::new(hideBin(args()), "ab:c");
160      if let Ok(my_getopt) = getopt_rc {
161         if let Some(b_value) = my_getopt.get('b') {
162            println!("-b argument is: {}", b_value);
163         }
164      }
165      ```
166   */
167   pub fn get(&self, option: char) -> Option<&String> {
168      self.options.get(&option)
169   }
170
171   /**
172      Check if command line option were supplied on command line.
173
174      Its convience shortcut for getopt.options.contains_key()
175
176      #### Return value
177      1. If option were supplied on command line returned value is `true`.
178      1. If option were not supplied by user returned value is `false`.
179
180      #### Example
181      ```rust
182      use std::env::args;
183      use getopt2::hideBin;
184
185      let getopt_rc = getopt2::new(hideBin(args()), "ab:c");
186      if let Ok(my_getopt) = getopt_rc {
187         if my_getopt.has('b') {
188            println!("-b argument is: {}", my_getopt.get('b').unwrap());
189         }
190      }
191      ```
192   */
193   pub fn has(&self, option: char) -> bool {
194      self.options.contains_key(&option)
195   }
196
197   /**
198      Check if any non option arguments were supplied.
199
200      It is a convenience shortcut for *getopt.arguments.is_empty()*.
201
202      #### Return value
203      1. If any arguments were supplied on the command line returned value is `true`.
204      1. If no arguments were supplied on the command line returned value is `false`.
205
206      #### Example
207      ```rust
208      use std::env::args;
209      use getopt2::hideBin;
210
211      let getopt_rc = getopt2::new(hideBin(args()), "ab:c");
212      if let Ok(my_getopt) = getopt_rc {
213         if !my_getopt.is_empty() {
214            println!("Arguments were supplied on command line");
215         }
216      }
217      ```
218   */
219   pub fn is_empty(&self) -> bool {
220      self.arguments.is_empty()
221   }
222}
223
224/**
225  Access to positional arguments by index.
226*/
227impl std::ops::Index<usize> for getopt {
228    type Output = String;
229
230    fn index(&self, index: usize) -> &Self::Output {
231        self.arguments.index(index)
232    }
233}
234
235impl IntoIterator for getopt {
236   type Item = String;
237   type IntoIter = std::vec::IntoIter<String>;
238
239   fn into_iter(self) -> Self::IntoIter {
240      self.arguments.into_iter()
241   }
242}
243
244impl<'a> IntoIterator for &'a getopt {
245   type Item = &'a String;
246   type IntoIter = std::slice::Iter<'a, String>;
247
248   fn into_iter(self) -> Self::IntoIter {
249      self.arguments.iter()
250   }
251}
252
253/**
254  Parse command line arguments.
255
256  Parses command line arguments using GNU getopt(3) parsing rules with
257  double dash "--" support.
258  Long arguments starting with double dash "--" are not supported.
259
260  Parsing is done in non strict and non POSIX mode.
261  Call [`validate`] on result to detect missing or unknown arguments.
262
263  POSIX parsing mode is not yet supported.
264
265  ## Arguments
266
267  * `arg` - String Iterator with command line arguments. Can be empty.
268
269  * `optstring` - List of legitimate alphanumeric plus '?' option characters.
270                  If character is followed by colon, the option requires
271                  an argument. Must not be empty.
272  ## Parsing rules
273  1. GNU argument parsing rules. It means that options can be anywhere in command line before --
274  1. Double dash -- support. Everything after -- is not treated as options.
275  1. Long options are not supported.
276  1. Multiple options not requiring argument can be chained together.
277     -abc is the same as -a -b -c
278  1. Last chained option can have an argument.
279     -abcpasta is the same as -a -b -c pasta
280  1. Argument does not require space. -wfile is same as -w file
281  1. optional argument `::` GNU optstring extension is not implemented.
282  1. Strict POSIX parse mode where first non option stops option parsing is not supported.
283     This mode is triggered in GNU getopt by setting `POSIXLY_CORRECT` variable or by
284     optstring starting with a plus sign `+`.
285  1. The POSIX-specified extension for the *getopt* function, which allows the optstring to
286     start with a colon (:), is always supported.
287     This extension enables the use of the '?' character as a command-line option.
288     We *always allow* use of the '?' as option without need to manually activate it using
289     optstring.
290     Starting optstring with ':' is possible and supported as valid syntax.
291
292  ## Errors
293  1. Parsing error **only happens** if optstring parameter is invalid or empty.
294  1. If required argument is missing function _new()_ still returns succesfully and missing argument will
295     be replaced by empty String.
296
297  ### See also
298  1. [GNU libc getopt](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) function.
299  1. [POSIX getopt](https://pubs.opengroup.org/onlinepubs/9799919799/functions/getopt.html) function.
300
301[`validate`]: ./fn.validate.html
302*/
303pub fn new(arg: impl IntoIterator<Item = impl AsRef<str>>, optstring: impl AsRef<str>) -> Result<getopt> {
304   /** output options values */
305   let mut opts = HashMap::new();
306   /** output argument list */
307   let mut args = Vec::new();
308   /** option for previous loop iteration */
309   let mut next_opt: Option<char> = None;
310   /** are we still parsing or we just copying rest of arguments */
311   let mut stop_parsing = false;
312   /** map of options -> having an argument */
313   let options_map: HashMap<char,argument> = build_options_map(validate_optstring(optstring.as_ref())?);
314   /** is posix mode enabled */
315   let posix: bool = optstring.as_ref().starts_with("+");
316
317   for el in arg {
318      let element = el.as_ref();
319      if stop_parsing {
320         // we do not parse options anymore all what's left
321         // are arguments
322         args.push(element.to_string());
323      } else if let Some(next_opt_char) = next_opt {
324         // option with possible parameter in previous loop iteration
325         match options_map.get(&next_opt_char) {
326            Some(argument::YES) => {
327               // option with mandatory argument
328               opts.insert(next_opt_char, element.to_string());
329               next_opt = None;
330            },
331            Some(argument::OPTIONAL) => {
332               // option with an optional argument
333               if is_option(element, &options_map) {
334                  // current element is an option
335                  // push prev. option without an arg
336                  opts.insert(next_opt_char, String::from(""));
337                  next_opt = Some(element.as_bytes()[1] as char);
338               } else if element.eq("--") {
339                  // current element is a separator
340                  // push prev. option without an arg
341                  opts.insert(next_opt_char, String::from(""));
342                  // and stop parsing
343                  stop_parsing = true;
344                  next_opt = None;
345               } else {
346                  // current element is an argument
347                  opts.insert(next_opt_char, element.to_string());
348                  next_opt = None;
349               }
350            },
351            Some(argument::NO) => {
352               // previous option have no argument
353               // push it to opts
354               opts.insert(next_opt_char, String::from(""));
355               // is current element option or argument
356               if is_option(element, &options_map) {
357                  next_opt = Some(element.as_bytes()[1] as char);
358               } else if element.eq("--") {
359                  stop_parsing = true;
360                  next_opt = None;
361               } else {
362                 args.push(element.to_string());
363                 next_opt = None;
364                 if ( posix ) { stop_parsing = true; };
365               }
366            }
367            None => {
368               // unknown option, should not happen
369               let mut s = String::from("-");
370               s.push(next_opt_char);
371               args.push(s);
372            }
373         }
374      } else if element.eq("--") {
375         stop_parsing = true;
376         next_opt = None;
377      } else {
378               // is current element option or argument
379               if is_option(element, &options_map) {
380                  next_opt = Some(element.as_bytes()[1] as char);
381               } else {
382                 args.push(element.to_string());
383                 if ( posix ) { stop_parsing = true };
384               }
385
386      }
387   }
388
389   // HANDLE MISSING ARGUMENT FOR LAST OPTION
390   if let Some(next_opt_char) = next_opt {
391      match options_map.get(&next_opt_char) {
392            Some(argument::YES) | None => {
393               // option with mandatory argument
394               // option argument is no more possible
395               // push option as argument
396               let mut s = String::from("-");
397               s.push(next_opt_char);
398               args.push(s);
399            },
400            Some(argument::NO) | Some(argument::OPTIONAL) => {
401               // store option itself
402               opts.insert(next_opt_char, String::from(""));
403            }
404      }
405   }
406
407   Ok(getopt {
408      options: opts,
409      arguments: args,
410      option_has_arg: options_map,
411   })
412}
413
414/**
415  Check if string reference is an option.
416
417  To pass check following conditions must be true:
418  1. string is exactly 2 characters long
419  1. string must start with a '-'
420  1. second char must be in options map
421*/
422fn is_option(opt: &str, options_map: &HashMap<char, argument>) -> bool {
423  if ( opt.chars().count() == 2 ) {
424    if ( opt.starts_with('-') ) {
425      let second_char: char = opt.chars().nth(1).unwrap();
426      options_map.contains_key(&second_char)
427    } else {
428      // not starts with a '-'
429      false
430   }
431  } else {
432    // length not 2
433    false
434  }
435}
436
437/**
438  Check if string reference can be an option.
439
440  To pass check following conditions must be true:
441  1. string reference is exactly 2 characters long
442  1. string must start with a '-'
443  1. opstring validation must pass
444*/
445fn is_possible_option(opt: &str) -> bool {
446  if ( opt.chars().count() == 2 ) {
447    if ( opt.starts_with('-') ) {
448      let possible_optstring: String = opt.chars().skip(1).collect();
449      validate_optstring(&possible_optstring).is_ok()
450    } else {
451      // not starts with a '-'
452      false
453   }
454  } else {
455    // length not 2
456    false
457  }
458}
459
460
461/**
462  Checks if optstring is valid.
463
464  ### Validation rules
465  1. optstring can't contain triple ':::'
466  1. optstring can't be empty
467  1. allowed option characters are a-z A-Z 0-9 and '?'
468
469  ### Implemented extensions
470  1. if optstring starts with ':' then use of '?' is allowed.
471     This extension is always active; You do not need to start
472     optstring with ':' to enable '?'.
473  1. optional arguments. If two semicolons '::' follows argument, it is an
474     optional argument.
475  1. POSIX mode. If string *starts with* '+' then parser will run in
476     strict POSIX mode. In this case ':' extension can be second character
477     '+:'. If '+' appears at any other position validation fails.
478*/
479fn validate_optstring(optstring: &str) -> Result<&str> {
480   if optstring.is_empty() {
481      Err(Error::new(ErrorKind::UnexpectedEof, "optstring can't be empty"))
482   } else {
483      // check for valid optstring characters
484      for c in optstring.chars() {
485         match c {
486            'a'..='z' => Ok(()),
487            'A'..='Z' => Ok(()),
488            '0'..='9' => Ok(()),
489            '?' => Ok(()),
490            ':' => Ok(()),
491            '+' => Ok(()),
492            _ => Err(Error::new(
493               ErrorKind::InvalidInput,
494               "unsupported character in optstring. Only a-z A-Z 0-9 and ?:+ are allowed",
495            )),
496         }?
497      }
498      let plus = optstring.rfind("+");
499      if plus.unwrap_or(0) > 0 {
500         Err(Error::new(ErrorKind::InvalidInput, "plus sign '+' must be first character"))
501      } else if optstring.contains(":::") {
502         Err(Error::new(ErrorKind::InvalidInput, "triple ':' are not permited in optstring"))
503      } else if optstring.starts_with("::") {
504         Err(Error::new(ErrorKind::InvalidInput, "optstring can't start with '::'"))
505      } else if optstring.starts_with("+::") {
506         Err(Error::new(ErrorKind::InvalidInput, "optstring can't start with '+::'"))
507      } else {
508         Ok(optstring)
509      }
510   }
511}
512
513/**
514 * Build options map from *validated* optstring.
515 *
516 * returns map <option,argument>
517*/
518fn build_options_map(optstring: &str) -> HashMap<char, argument> {
519   let mut rc: HashMap<char, argument> = HashMap::with_capacity(optstring.len());
520   let mut previous1: char = ':';
521   let mut previous2: char = ':';
522   let mut insert_one = |c: char| -> () {
523      match c {
524         ':' if previous1 != ':' && previous1 != '+' => rc.insert(*&previous1, argument::YES),
525         ':' if previous1 == ':' && previous2 != ':' => rc.insert(*&previous2, argument::OPTIONAL),
526         '+' => None,
527         _ if previous1 != ':' && previous1 != '+' => rc.insert(*&previous1, argument::NO),
528         _ => None,
529      };
530      previous2 = previous1;
531      previous1 = c;
532   };
533
534   for c in optstring.chars() {
535      insert_one(c);
536   }
537   // re-run option map building logic on last character in optstring if it is not ':'
538   // if last character is ':', it has been already inserted to map and running it
539   // again would cause insertion of ':' into options map because it is previous character
540   optstring.chars().last().filter(|c| *c != ':').into_iter().for_each(|c| insert_one(c));
541   rc
542}
543
544/**
545* Validate parsed options in strict mode.
546*
547* ## Success
548*
549*   If strict mode validation passes, unchanged argument wrapped in [`Result`]
550*   is returned.
551*
552* ## Errors
553*
554*   Returns [`Err`] if option not listed in optstring is encountered or
555*   required argument for an option is missing.
556**/
557pub fn validate(getopt: getopt) -> Result<getopt> {
558   // validate missing required arguments
559   for (opt, _) in getopt.option_has_arg.iter().filter(|(_, arg)| **arg == argument::YES) {
560      let mut opt_string = String::from("-");
561      opt_string.push(*opt);
562      if getopt.arguments.contains(&opt_string) {
563            return Err(Error::new(
564               ErrorKind::InvalidInput,
565               format!("Option -{} does not have required argument", opt),
566            ));
567      }
568   }
569
570   // validate unknown options
571   for opt in getopt.arguments.iter() {
572      if is_possible_option(&opt) {
573         return Err(Error::new(ErrorKind::InvalidInput, format!("Unknown option -{}", opt)));
574      }
575   }
576
577   Ok(getopt)
578}
579
580/**
581 * Removes first element from the IntoIterator.
582 *
583 * This utility function is supposed to be used on value returned by
584 * `std::env::args()` before passing it to [`new`].
585 *
586 * First argument returned by `args()` corresponds to the program
587 * executable name and its undesirable to have program name included
588 * between parsed arguments.
589 *
590 * This function exists for making code more readable and because
591 * widely used [yargs npm](https://www.npmjs.com/package/yargs) argument
592 * parser have same function.
593 *
594 * `hideBin` function can be replaced by calling `.skip(1)`
595 * on `Iterator` before passing it to [`new`]. Choose what is more
596 * readable for you.
597 *
598 * #### Example
599 * ```rust
600 * use std::env::args;
601 * use getopt2::hideBin;
602 *
603 * let rc = getopt2::new(hideBin(args()), "ab:c");
604 * if let Ok(g) = rc {
605 *    // command line options parsed sucessfully
606 *    if let Some(_) = g.options.get(&'a') {
607 *       // -a option found on command line
608 *    };
609 *  };
610 * ```
611 * [`new`]: ./fn.new.html
612*/
613pub fn hideBin(argv: impl IntoIterator<Item = String>) -> impl IntoIterator<Item = String> {
614   argv.into_iter().skip(1)
615}
616
617// unit tests
618
619#[cfg(test)]
620#[path = "getopt_helpers_tests.rs"]
621mod helpers;
622
623#[cfg(test)]
624#[path = "getopt_tests.rs"]
625mod tests;
626
627#[cfg(test)]
628#[path = "getopt_posix_tests.rs"]
629mod posix_tests;
630
631#[cfg(test)]
632#[path = "getopt_validate_api_tests.rs"]
633mod validateapi;
634
635#[cfg(test)]
636#[path = "getopt_hidebin_tests.rs"]
637mod hidebin;
638
639#[cfg(test)]
640#[path = "getopt_doubledash_tests.rs"]
641mod doubledash;
642
643#[cfg(test)]
644#[path = "getopt_iterator_tests.rs"]
645mod it;
646
647#[cfg(test)]
648#[path = "getopt_index_tests.rs"]
649mod index;