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;