Skip to main content

getoptions_long/
lib.rs

1//! getoptions-long - library to parse command line inspired by perl's Getopt::Long
2//! Copyright (C) 2024  Ira Peach
3//!
4//! This program is free software: you can redistribute it and/or modify
5//! it under the terms of the GNU Affero General Public License as published by
6//! the Free Software Foundation, either version 3 of the License, or
7//! (at your option) any later version.
8//!
9//! This program is distributed in the hope that it will be useful,
10//! but WITHOUT ANY WARRANTY; without even the implied warranty of
11//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12//! GNU Affero General Public License for more details.
13//!
14//! You should have received a copy of the GNU Affero General Public License
15//! along with this program.  If not, see <https://www.gnu.org/licenses/>.
16//!
17//! ===============
18//! getoptions-long
19//! ===============
20//! Argument parser inspired by Perl's Getopt::Long.
21//!
22//! Provides a likely familiar option parser for those who may be familiar with Getopt::Long from
23//! Perl.  The specific options for Getopt::Long that were targeted are `qw(:config gnu_getopt
24//! no_ignore_case no_auto_abbrev)`.
25//!
26//! ==========
27//! Quickstart
28//! ==========
29//!
30//! ```
31//! use std::cell::RefCell;
32//!
33//! use getoptions_long::*;
34//! let args: Vec<String> = std::env::args().collect();
35//! // example invocation:
36//! //
37//! //    backup-profile --list -n --backup-dir=/mnt/usr1/backups --backup-dir \
38//! //        "/mnt/usr1/backups test -v -- -f.txt
39//! let prog = args[0].clone();
40//! let args: Vec<String> = args.into_iter().skip(1).collect();
41//! let usage =  || {
42//!     let prog = std::path::Path::new(&prog).file_name().unwrap().to_string_lossy();
43//!     println!("usage: {prog} [-hlcpnv] [-b BACKUP] [--] [TAR_ARGUMENTS]");
44//!     println!("  backup firefox profiles on win32 from cygwin");
45//!     println!("  -h,--help               display this usage");
46//!     println!("  -b,--backup-dir BACKUP  output backup files to BACKUP (default '.')");
47//!     println!("  -l,--list               list profiles and their paths");
48//!     println!("  -n,--dry-run            perform dry run with commands printed");
49//!     println!("  -p,--print              print Firefox configuration root");
50//!     println!("  -v,--verbose            be noisier");
51//! };
52//!
53//! let mut backup_dir = String::new();
54//! let mut dry_run = false;
55//! let free_args = RefCell::new(vec![]);
56//! let mut list = false;
57//! let mut print = false;
58//! let mut verbose = false;
59//!
60//! let result = get_options(&mut [
61//!     Opt::SubSwitch("help|h", &|_| { usage(); std::process::exit(0); }),
62//!     Opt::Switch   ("list|l", &mut list),
63//!     Opt::Switch   ("p|print", &mut print),
64//!     Opt::Switch   ("dry-run|n", &mut dry_run),
65//!     Opt::Arg      ("backup-dir|b", &mut backup_dir),
66//!     Opt::Switch   ("verbose|v", &mut verbose),
67//!     Opt::Free     (&|arg| free_args.borrow_mut().push(arg.to_string())),
68//! ], &args);
69//!
70//! if let Err(ref err) = result {
71//!     eprintln!("ERROR: parsing command line arguments: {err}");
72//!     //std::process::exit(1); // don't do this in a test
73//! }
74//!
75//! // ...
76//! ```
77//!
78//! ====
79//! Why?
80//! ====
81//!
82//! Why another command line parser?  Well, I missed the options Getopt::Long can do.
83//!
84//! In comparison to existing crates, such as clap and gumdrop, this parser is active in your code
85//! only at the function invocation site (usually `get_options` if you have a fully realized list
86//! of strings, or `get_options_env` for quick and dirty).  It also uses a //lot// of mutability,
87//! which Rust exposes in excruciating detail.  You do not have to define your own type, use derive
88//! macros, and hope that it does what you want (clap), or drop to a frustrating builder invocation
89//! chain to enable last-argument precedence (clap), or end up in a frustrating API for using
90//! callbacks (clap, gumdrop).  (I'm actually not sure it's possible to use callbacks with clap &
91//! grumdrop, because my brain might just not understand the documentation) (it is, but you have to
92//! use the Arg API in clap)
93//!
94//! Callbacks allow a lot of things.  The main thing I use them for is mutually exclusive command
95//! line arguments with last precedence, such as:
96//!
97//! ```
98//! use getoptions_long::*;
99//! let args = ["--foo", "--baz", "--bar", "--baz"];
100//!
101//! let command = std::cell::RefCell::new(String::new());
102//!
103//! let sub: &dyn for<'a> Fn(&'a str) = &|arg| {
104//!     let mut command = command.borrow_mut();
105//!     *command = format!("command-{}", arg);
106//! };
107//!
108//! let result = get_options_str(&mut [
109//!     Opt::SubSwitch("foo", sub),
110//!     Opt::SubSwitch("bar", sub),
111//!     Opt::SubSwitch("baz", sub),
112//! ], &args);
113//!
114//! assert_eq!(result, Ok(()));
115//! assert_eq!(command.into_inner(), "command-baz");
116//! ```
117//!
118//! They can be trivially extended to adding repeat arguments, as well.
119//!
120//! If you aren't familiar with the Rustonomicon (I definitely am not), you might be wondering why
121//! it is quite unergonomic to need to use RefCell.  Encapsulating a value that is later borrowed
122//! from a RefCell is one of the scarce few ways to pass the same closure to multiple switches
123//! (because while we're not violating the multiple mutable borrows rule, we do promise Rust that
124//! we're being careful).
125//!
126//! ====
127//! Q&A:
128//! ====
129//!
130//! 1. Is it fast?  Seems fast enough for my needs.
131//! 2. How much does it allocate?  No idea.  Definitely allocates.
132//! 3. no_std?  No STDs, but I don't think compiling with no_std is in scope.
133//! 4. Other Getopt::Long options?  Maybe.  I mostly went with what I go with and seems fairly
134//!    consistent with POSIX getopt with GNU extensions (I'm actually interested in testing exactly
135//!    how compliant we are).
136//! 5. Any benchmarks?  Nope.
137//! 6. Dependencies?  std.  That's it.
138//! 7. How long did it take you?  Not long enough that I actually released it; less than 20 hours
139//!    of full headed stubborness.
140//! 8. Rewrite Perl in Rust?  Have you looked at the source code?  My eyes go spirally when I see
141//!    way too many macros and ifdefs.  I ain't doing that.  (unless...?)
142//! 9. Why not clap or gumdrop?  See above.
143//! 10. Is this based off of Getopt::Long?  Nope.  No source code was referenced; if it were I
144//!     would keep the same licensing terms (and I'm probably open to relicensing given enough
145//!     value for labor).
146
147use std::fmt::Debug;
148use std::fmt::Formatter;
149use std::slice::Iter;
150
151use error::Error;
152
153/// Define options
154pub enum Opt<'a> {
155    /// Represents an option that is true if given.
156    Switch(&'a str, &'a mut bool),
157    /// Represents an option that takes an argument.
158    Arg(&'a str, &'a mut String),
159    /// Represents an option whose argument may be parsed via a callback.
160    SubSwitch(&'a str, FnSubSwitch<'a>),
161    /// Represents an option whose argument and value may be parsed via a callback.
162    SubArg(&'a str, FnSubArg<'a>),
163    /// Represents a set of arguments not bound to any options.
164    Free(FnFree<'a>),
165}
166
167pub mod error {
168    use std::fmt::Display;
169    use std::fmt::Formatter;
170
171    /// Error type.  Each will print to a friendly string.
172    #[derive(Clone,Debug,Eq,PartialEq)]
173    pub enum Error {
174        /// Error describing that an argument was needed, but not available (i.e. underflow in
175        /// arguments).
176        NeedArgument(String),
177        /// Error describing an unexpected flag that was not defined, including free arguments that
178        /// were not sent to a callback.
179        Unexpected(String),
180    }
181
182
183    impl Display for Error {
184        fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
185            match self {
186                Error::NeedArgument(s) => write!(f, "need argument for '{}'", s),
187                Error::Unexpected(s) => write!(f, "unexpected argument '{}'", s),
188            }
189        }
190    }
191}
192
193/// Type for a closure for a switch.
194///
195/// Using RefCell to change the captured environment is required.  While it isn't strictly
196/// required, it is required if we want to reuse the closure.
197///
198/// (technically, this is a function pointer, not a closure, but the syntax creates the function
199/// pointer)
200///
201/// Example:
202///
203/// ```
204/// use std::cell::RefCell;
205/// use getoptions_long::*;
206///
207/// let args: Vec<String> = vec!["-t", "2", "-t", "20", "-t", "9"].into_iter().map(|x| x.to_string()).collect();
208/// let flag = RefCell::new(String::new());
209///
210/// let result = get_options(&mut [
211///     Opt::SubArg("t|toggle", &|_, val| { if val.len() < 2 { flag.replace_with(|_| val.to_string() ); }}),
212/// ], &args);
213/// assert_eq!(flag.into_inner(), "9");
214/// ```
215pub type FnSubSwitch<'a> = &'a dyn Fn(&str);
216/// Type for a closure for an argument.
217///
218/// Using RefCell to change the captured environment is required.  While it isn't strictly
219/// required, it is required if we want to reuse the closure.
220///
221/// (technically, this is a function pointer, not a closure, but the syntax creates the function
222/// pointer)
223///
224/// Example:
225///
226/// ```
227/// use std::cell::RefCell;
228/// use getoptions_long::*;
229///
230/// let args: Vec<String> = vec!["-t", "--toggle", "-t", "-t"].into_iter().map(|x| x.to_string()).collect();
231/// let flag = RefCell::new(false);
232///
233/// let result = get_options(&mut [
234///     Opt::SubSwitch("t|toggle", &|_| { flag.replace_with(|t| !*t ); }),
235/// ], &args);
236/// assert!(!flag.into_inner());
237/// ```
238pub type FnSubArg<'a> = &'a dyn Fn(&str,&str);
239/// Type for a closure for free arguments.
240///
241/// Using RefCell to change the captured environment is required.  While it isn't strictly
242/// required, it is required if we want to reuse the closure.
243///
244/// (technically, this is a function pointer, not a closure, but the syntax creates the function
245/// pointer)
246///
247/// Example:
248///
249/// ```
250/// use std::cell::RefCell;
251/// use getoptions_long::*;
252///
253/// let args: Vec<String> = vec!["foo", "bar", "baz"].into_iter().map(|x| x.to_string()).collect();
254/// let free_args = RefCell::new(vec![]);
255///
256/// let result = get_options(&mut [
257///     Opt::Free     (&|arg| free_args.borrow_mut().push(arg.to_string())),
258/// ], &args);
259/// // ...
260/// ```
261pub type FnFree<'a> = &'a dyn Fn(&str);
262
263impl Debug for Opt<'_> {
264    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
265        match self {
266            Opt::Switch(x, b) => f.debug_tuple("Opt::Switch").field(x).field(b).finish(),
267            Opt::Arg(x, s) => f.debug_tuple("Opt::Arg").field(x).field(s).finish(),
268            Opt::SubArg(x, _) => f.debug_tuple("Opt::SubArg").field(x).field(&"Fn").finish(),
269            Opt::SubSwitch(x, _) => f.debug_tuple("Opt::SubSwitch").field(x).field(&"Fn").finish(),
270            Opt::Free(_) => f.debug_tuple("Opt::Free").field(&"Fn").finish(),
271        }
272    }
273}
274
275/// Split off the pipe symbol.
276fn split_off(slice: &str) -> Result<(&str, &str),&str> {
277    let split = slice.split_once('|');
278    if let Some((s1, s2)) = split {
279        Ok((s1, s2))
280    }
281    else {
282        Err(slice)
283    }
284}
285
286/// Match a short option.
287fn match_short(ch: char, switch: &str) -> bool {
288    let mut buf = [0u8; 4];
289    let ch: &str = ch.encode_utf8(&mut buf);
290    let split = split_off(switch);
291    if let Ok((s1, s2)) = split {
292        if s1 == ch {
293            return true;
294        }
295        else if s2 == ch {
296            return true;
297        }
298    }
299    else if let Err(s) = split {
300        if s == ch {
301            return true;
302        }
303    }
304    return false;
305}
306
307/// Match a long option.
308fn match_long(arg: &str, switch: &str) -> bool {
309    if arg.chars().count() == 1 {
310        return false;
311    }
312
313    let split = split_off(switch);
314    if let Ok((s1, s2)) = split {
315        if s1 == arg {
316            return true;
317        }
318        else if s2 == arg {
319            return true;
320        }
321    }
322    else if let Err(s) = split {
323        if s == arg {
324            return true;
325        }
326    }
327    return false;
328}
329
330/// Handle a short option.
331///
332/// Will unbundle multiple switches, or consume the rest of the bundle if an argument requires.
333fn handle_short_opt(args_iter: &mut Iter<&str>, arg: &str, opts: &mut [Opt]) -> Result<bool,Error> {
334    let mut chars_iter = arg.chars().peekable();
335    let mut parsed = false;
336    while let Some(ch) = chars_iter.next() {
337        let mut iter = opts.iter_mut();
338        while let Some(opt) = iter.next() {
339            if let Opt::Switch(switch, &mut ref mut b) = opt {
340                if match_short(ch, switch) {
341                    *b = true;
342                    parsed = true;
343                }
344            }
345            else if let Opt::Arg(switch, &mut ref mut s) = opt {
346                if match_short(ch, switch) {
347                    if chars_iter.peek().is_some() {
348                        let a: String = chars_iter.collect();
349                        *s = a;
350                        // collect the rest as an argument here; if we don't return, we will
351                        // possibly error when we didn't mean to.
352                        return Ok(true);
353                    }
354                    else if let Some(a) = args_iter.next() {
355                        *s = a.to_string();
356                        parsed = true;
357                    }
358                    else {
359                        return Err(Error::NeedArgument(format!("-{}", arg)));
360                    }
361                }
362            }
363            else if let Opt::SubArg(switch, func) = opt {
364                if match_short(ch, switch) {
365                    if let Some(a) = args_iter.next() {
366                        func(arg, *a);
367                        parsed = true;
368                    }
369                    else {
370                        return Err(Error::NeedArgument(format!("-{}", arg)));
371                    }
372                }
373            }
374            else if let Opt::SubSwitch(switch, func) = opt {
375                if match_short(ch, switch) {
376                    func(arg);
377                    parsed = true;
378                }
379            }
380        }
381    }
382
383    if parsed {
384        return Ok(true);
385    }
386    Err(Error::Unexpected(format!("-{}", arg)))
387}
388
389/// Extract the argument given by an equals.
390fn extract_equals(arg: &str) -> Option<(&str,&str)> {
391    let split = arg.split_once('=');
392    if let Some((arg, equals_value)) = split {
393        Some((arg, equals_value))
394    }
395    else {
396        None
397    }
398}
399
400/// Handle a long option.
401fn handle_long_opt(args_iter: &mut Iter<&str>, arg: &str, opts: &mut [Opt]) -> Result<bool,Error> {
402    for opt in &mut *opts {
403        if let Opt::Switch(switch, &mut ref mut b) = opt {
404            if switch.chars().count() == 1 {
405                continue;
406            }
407
408            if match_long(arg, switch) {
409                *b = true;
410                return Ok(true);
411            }
412        }
413        else if let Opt::Arg(switch, &mut ref mut s) = opt {
414            let (arg, equals_value) = extract_equals(arg).unwrap_or((arg, ""));
415
416            if match_long(arg, switch) {
417                if equals_value != "" {
418                    *s = (equals_value).to_string();
419                    return Ok(true);
420                }
421                else if let Some(a) = args_iter.next() {
422                    *s = (*a).to_string();
423                    return Ok(true);
424                }
425                else {
426                    return Err(Error::NeedArgument(format!("--{}", arg)));
427                }
428            }
429        }
430        else if let Opt::SubArg(switch, func) = opt {
431            let (arg, equals_value) = extract_equals(arg).unwrap_or((arg, ""));
432            if match_long(arg, switch) {
433                if equals_value != "" {
434                    func(arg, equals_value);
435                    return Ok(true);
436                }
437                else if let Some(a) = args_iter.next() {
438                    func(arg, *a);
439                    return Ok(true);
440                }
441                else {
442                    return Err(Error::NeedArgument(format!("--{}", arg)));
443                }
444            }
445        }
446        else if let Opt::SubSwitch(switch, func) = opt {
447            if match_long(arg, switch) {
448                func(arg);
449                return Ok(true);
450            }
451        }
452    }
453    Err(Error::Unexpected(format!("--{}", arg)))
454}
455
456/// Handle a free argument.
457///
458/// The "--" escaping argument is allowed, even if there's no way to bind a free argument.
459fn handle_free_arg(arg: &str, opts: &mut [Opt]) -> Result<(),Error> {
460    for opt in opts.iter_mut() {
461        if let Opt::Free(f) = opt {
462            let f: FnFree = f;
463            f(arg);
464            return Ok(());
465        }
466    }
467    if arg == "--" {
468        return Ok(());
469    }
470    Err(Error::Unexpected(arg.to_string()))
471}
472
473/// Parse options from std::env::args().
474pub fn get_options_env(opts: &mut [Opt]) -> Result<(),Error> {
475    let env_args: Vec<String> = std::env::args().collect();
476    get_options(opts, &env_args)
477}
478
479/// Parse options from a slice of String.
480pub fn get_options(opts: &mut [Opt], args: &[String]) -> Result<(),Error> {
481    let args: Vec<&str> = args.iter().map(|x| x.as_ref()).collect();
482    get_options_str(opts, &args)
483}
484
485/// Parse options from a slice of &str.
486///
487/// Mostly used if you pass arguments via &'static str.
488pub fn get_options_str(opts: &mut [Opt], args: &[&str]) -> Result<(),Error> {
489    let mut iter = args.iter();
490    while let Some(arg) = iter.next() {
491        if *arg == "--" {
492            handle_free_arg(arg, opts)?;
493            while let Some(arg) = iter.next() {
494                handle_free_arg(arg, opts)?;
495                return Ok(());
496            }
497        }
498        else if arg.starts_with("--") {
499            let mut chars = arg.chars();
500            chars.next();
501            chars.next();
502            let arg = chars.as_str();
503            handle_long_opt(&mut iter, arg, opts)?;
504        }
505        else if arg.starts_with("-") {
506            let mut chars = arg.chars();
507            chars.next();
508            let arg = chars.as_str();
509            handle_short_opt(&mut iter, arg, opts)?;
510        }
511        else {
512            handle_free_arg(arg, opts)?;
513        }
514    }
515    Ok(())
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    use std::cell::RefCell;
523
524    #[test]
525    fn test_no_input() {
526        let args = [];
527        let result = get_options_str(&mut [], &args);
528        assert!(result.is_ok());
529    }
530
531    #[test]
532    fn test_short_switch() {
533        let args = ["-a"];
534
535        let mut a_opt = false;
536
537        let result = get_options_str(&mut [
538            Opt::Switch("a", &mut a_opt),
539        ], &args);
540
541        assert!(result.is_ok());
542        assert!(a_opt);
543    }
544
545    #[test]
546    fn test_short_switch_correct() {
547        let args = ["-c"];
548
549        let mut a_opt = false;
550        let mut b_opt = false;
551
552        let result = get_options_str(&mut [
553            Opt::Switch("a", &mut a_opt),
554            Opt::Switch("b", &mut b_opt),
555        ], &args);
556
557        assert!(!a_opt);
558        assert!(!b_opt);
559        assert!(result.is_err());
560    }
561
562    #[test]
563    fn test_long_switch() {
564        let args = ["--long-flag"];
565
566        let mut long_flag = false;
567        let mut another_flag = false;
568
569        let result = get_options_str(&mut [
570            Opt::Switch("long-flag", &mut long_flag),
571            Opt::Switch("another-flag", &mut another_flag),
572        ], &args);
573
574        assert!(result.is_ok());
575        assert!(long_flag);
576        assert!(!another_flag);
577    }
578
579    #[test]
580    fn test_long_and_short_switch() {
581        let args = ["-a", "--long-flag"];
582
583        let mut short_flag = false;
584        let mut another_short_flag = false;
585        let mut long_flag = false;
586        let mut another_flag = false;
587
588        let result = get_options_str(&mut [
589            Opt::Switch("a", &mut short_flag),
590            Opt::Switch("2", &mut another_short_flag),
591            Opt::Switch("long-flag", &mut long_flag),
592            Opt::Switch("another-flag", &mut another_flag),
593        ], &args);
594
595        assert!(result.is_ok());
596        assert!(long_flag);
597        assert!(!another_flag);
598    }
599
600    #[test]
601    fn test_short_or_long_flag() {
602        let args = ["--a-flag"];
603
604        let mut flag = false;
605        let mut flag2 = false;
606
607        let result = get_options_str(&mut [
608            Opt::Switch("a|a-flag", &mut flag),
609            Opt::Switch("b|b-flag", &mut flag2),
610        ], &args);
611
612        assert!(result.is_ok());
613        assert!(flag);
614        assert!(!flag2);
615
616        let args = ["-a"];
617
618        let mut flag = false;
619        let mut flag2 = false;
620
621        let result = get_options_str(&mut [
622            Opt::Switch("a|a-flag", &mut flag),
623            Opt::Switch("b|b-flag", &mut flag2),
624        ], &args);
625
626        assert!(result.is_ok());
627        assert!(flag);
628        assert!(!flag2);
629    }
630
631    #[test]
632    fn test_option_argument() {
633        let args = ["-a", "foo bar"];
634
635        let mut opt = String::new();
636
637        let result = get_options_str(&mut [
638            Opt::Arg("a|a-flag", &mut opt),
639        ], &args);
640
641        assert!(result.is_ok());
642        assert_eq!(opt, "foo bar");
643
644        let args = ["--a-flag", "foo bar"];
645
646        let mut opt = String::new();
647
648        let result = get_options_str(&mut [
649            Opt::Arg("a|a-flag", &mut opt),
650        ], &args);
651
652        assert!(result.is_ok());
653        assert_eq!(opt, "foo bar");
654    }
655
656    #[test]
657    fn test_option_argument_take_last() {
658        let args = ["-a", "foo bar", "--a-flag", "bar baz"];
659
660        let mut opt = String::new();
661
662        let result = get_options_str(&mut [
663            Opt::Arg("a|a-flag", &mut opt),
664        ], &args);
665
666        assert!(result.is_ok());
667        assert_eq!(opt, "bar baz");
668
669        let args = ["--a-flag", "foo bar", "-a", "bar baz"];
670
671        let mut opt = String::new();
672
673        let result = get_options_str(&mut [
674            Opt::Arg("a|a-flag", &mut opt),
675        ], &args);
676
677        assert!(result.is_ok());
678        assert_eq!(opt, "bar baz");
679    }
680
681    #[test]
682    fn test_option_argument_underflow() {
683        let args = ["-a"];
684
685        let mut opt = String::new();
686
687        let result = get_options_str(&mut [
688            Opt::Arg("a|a-flag", &mut opt),
689        ], &args);
690
691        assert_eq!(result, Err(Error::NeedArgument("-a".to_string())));
692        assert_eq!(result.unwrap_err().to_string(), "need argument for '-a'");
693
694        let args = ["--a-flag"];
695
696        let mut opt = String::new();
697
698        let result = get_options_str(&mut [
699            Opt::Arg("a|a-flag", &mut opt),
700        ], &args);
701
702        assert_eq!(result, Err(Error::NeedArgument("--a-flag".to_string())));
703    }
704
705    #[test]
706    fn test_callback() {
707        let args = ["-f", "foo", "-f", "bar"];
708
709        let opt = RefCell::new(String::new());
710
711        let result = get_options_str(&mut [
712            Opt::SubArg("flag|f", &|_arg, val| { opt.replace_with(|opt| format!("{}{}", opt, val)); }),
713        ], &args);
714
715        assert_eq!(result, Ok(()));
716        assert_eq!(opt.into_inner(), "foobar");
717
718        let args = ["--flag", "bar", "--flag", "foo"];
719
720        let opt = RefCell::new(String::new());
721
722        let result = get_options_str(&mut [
723            Opt::SubArg("flag|f", &|_arg, val| { opt.replace_with(|opt| format!("{}{}", opt, val)); }),
724        ], &args);
725
726        assert_eq!(result, Ok(()));
727        assert_eq!(opt.into_inner(), "barfoo");
728    }
729
730    #[test]
731    fn test_callback_switch() {
732        let args = ["-v", "-f", "-v", "-e", "-g"];
733
734        let opt: RefCell<i64> = 0.into();
735
736        let sub: &dyn for<'a> Fn(&'a str) = &|arg| {
737            if arg == "f" {
738                *opt.borrow_mut() += 2;
739            }
740            else if arg == "g" {
741                *opt.borrow_mut() -= 3;
742            }
743        };
744
745        let result = get_options_str(&mut [
746            Opt::SubSwitch("verbose|v", &mut |_arg| {
747                *opt.borrow_mut() += 1;
748            }),
749            Opt::SubSwitch("e", &mut |_arg| {
750                *opt.borrow_mut() += 4;
751            }),
752            Opt::SubSwitch("f|g", sub),
753        ], &args);
754
755        assert_eq!(result, Ok(()));
756        assert_eq!(opt, 5.into());
757    }
758
759    #[test]
760    fn test_long_equals() {
761        let args = ["--a-flag=boofar"];
762
763        let mut opt = String::new();
764
765        let result = get_options_str(&mut [
766            Opt::Arg("a|a-flag", &mut opt),
767        ], &args);
768
769        assert_eq!(result, Ok(()));
770        assert_eq!(opt, "boofar");
771    }
772
773    #[test]
774    fn test_free_arguments() {
775        let args = ["a", "bb", "ccc"];
776
777        let free = RefCell::new(vec![]);
778
779        let sub: FnFree = &|s| {
780            free.borrow_mut().push(s.to_string());
781        };
782
783        let result = get_options_str(&mut [
784            Opt::Free(sub),
785        ], &args);
786
787        assert_eq!(result, Ok(()));
788        assert_eq!(free.into_inner(), vec!["a", "bb", "ccc"]);
789    }
790
791    #[test]
792    fn test_double_dash_no_omit() {
793        let args = ["--"];
794
795        let free = RefCell::new(vec![]);
796
797        let sub: FnFree = &|s| {
798            free.borrow_mut().push(s.to_string());
799        };
800
801        let result = get_options_str(&mut [
802            Opt::Free(sub),
803        ], &args);
804
805        assert_eq!(result, Ok(()));
806        assert!(free.into_inner().len() == 1);
807    }
808
809    #[test]
810    fn test_double_dash_escape() {
811        let args = ["--", "-f"];
812
813        let mut flag = false;
814        let free = RefCell::new(vec![]);
815
816        let sub: FnFree = &|s| {
817            free.borrow_mut().push(s.to_string());
818        };
819
820        let result = get_options_str(&mut [
821            Opt::Switch("f|flag", &mut flag),
822            Opt::Free(sub),
823        ], &args);
824
825        let free = free.into_inner();
826        assert_eq!(result, Ok(()));
827        assert!(!flag);
828        assert!(free.len() == 2);
829        assert_eq!(free, vec!("--", "-f"));
830    }
831
832    #[test]
833    fn test_switch_bundling() {
834        let args = ["-ab"];
835
836        let mut flag_a = false;
837        let mut flag_b = false;
838
839        let result = get_options_str(&mut [
840            Opt::Switch("a", &mut flag_a),
841            Opt::Switch("b", &mut flag_b),
842        ], &args);
843
844        assert!(result.is_ok());
845        assert!(flag_a);
846        assert!(flag_b);
847    }
848
849    #[test]
850    fn test_switch_arg_bundling() {
851        let args = ["-ab", "2"];
852
853        let mut flag_a = false;
854        let mut flag_b = String::new();
855
856        let result = get_options_str(&mut [
857            Opt::Switch("a", &mut flag_a),
858            Opt::Arg("b", &mut flag_b),
859        ], &args);
860
861        assert!(result.is_ok());
862        assert!(flag_a);
863        assert_eq!(flag_b, "2");
864    }
865
866    #[test]
867    fn test_arg_nospace() {
868        let args = ["-a7"];
869
870        let mut flag_a = String::new();
871
872        let result = get_options_str(&mut [
873            Opt::Arg("a", &mut flag_a),
874        ], &args);
875
876        assert!(result.is_ok());
877        assert_eq!(flag_a, "7");
878    }
879
880    #[test]
881    fn test_arg_nospace_longer() {
882        let args = ["-aFooBarBaz"];
883
884        let mut flag_a = String::new();
885
886        let result = get_options_str(&mut [
887            Opt::Arg("a", &mut flag_a),
888        ], &args);
889
890        assert!(result.is_ok());
891        assert_eq!(flag_a, "FooBarBaz");
892    }
893
894    #[test]
895    fn test_bundle_switch_arg_nospace() {
896        let args = ["-ba8"];
897
898        let mut flag_a = String::new();
899        let mut flag_b = false;
900
901        let result = get_options_str(&mut [
902            Opt::Arg("a", &mut flag_a),
903            Opt::Switch("b", &mut flag_b),
904        ], &args);
905
906        assert!(result.is_ok());
907        assert_eq!(flag_a, "8");
908        assert!(flag_b);
909    }
910
911    #[test]
912    fn test_bundle_arg_switch_nospace() {
913        let args = ["-ab8"];
914
915        let mut flag_a = String::new();
916        let mut flag_b = false;
917
918        let result = get_options_str(&mut [
919            Opt::Arg("a", &mut flag_a),
920            Opt::Switch("b", &mut flag_b),
921        ], &args);
922
923        assert!(result.is_ok());
924        assert_eq!(flag_a, "b8");
925        assert!(!flag_b);
926    }
927
928    #[test]
929    fn test_flag_invalid() {
930        let args = ["-f"];
931
932        let mut flag_a = false;
933
934        let result = get_options_str(&mut [
935            Opt::Switch("a", &mut flag_a),
936        ], &args);
937
938        assert_eq!(result, Err(Error::Unexpected("-f".to_string())));
939        assert!(!flag_a);
940
941        let args = ["--foo", "7"];
942
943        let mut flag_a = false;
944        let free = RefCell::new(vec![]);
945
946        let sub: FnFree = &|s| {
947            free.borrow_mut().push(s.to_string());
948        };
949
950        let result = get_options_str(&mut [
951            Opt::Switch("a", &mut flag_a),
952            Opt::Free(sub),
953        ], &args);
954
955        assert_eq!(result, Err(Error::Unexpected("--foo".to_string())));
956        assert_eq!(result.unwrap_err().to_string(), "unexpected argument '--foo'");
957        assert_eq!(free.into_inner().len(), 0);
958        assert!(!flag_a);
959    }
960
961    #[test]
962    fn test_undeclared_free() {
963        let args = ["foo", "bar", "baz"];
964
965        let mut flag_a = false;
966
967        let result = get_options_str(&mut [
968            Opt::Switch("a", &mut flag_a),
969        ], &args);
970
971        assert_eq!(result, Err(Error::Unexpected("foo".to_string())));
972        assert!(!flag_a);
973
974        let args = ["--", "-7"];
975
976        let mut flag_a = false;
977
978        let result = get_options_str(&mut [
979            Opt::Switch("a", &mut flag_a),
980        ], &args);
981
982        assert!(!flag_a);
983        assert_eq!(result, Err(Error::Unexpected("-7".to_string())));
984        assert_eq!(result.unwrap_err().to_string(), "unexpected argument '-7'");
985    }
986
987    #[test]
988    fn test_callback_underfloaw() {
989        let args = ["--foo", "7", "--bar"];
990
991        let flag_a = RefCell::new(String::new());
992        let flag_b = RefCell::new(String::new());
993
994        let result = get_options_str(&mut [
995            Opt::SubArg("foo", &mut |_arg, val| {
996                flag_a.replace_with(|_| val.to_owned());
997            }),
998            Opt::SubArg("bar", &mut |_arg, val| {
999                flag_b.replace_with(|_| val.to_owned());
1000            }),
1001        ], &args);
1002
1003        assert_eq!(result, Err(Error::NeedArgument("--bar".to_string())));
1004        assert_eq!(flag_b.into_inner(), "");
1005    }
1006
1007    #[test]
1008    fn test_mutually_exclusive() {
1009        let args = ["--foo", "--baz", "--bar", "--baz"];
1010
1011        let command = RefCell::new(String::new());
1012
1013        let sub: &dyn for<'a> Fn(&'a str) = &|arg| {
1014            let mut command = command.borrow_mut();
1015            *command = format!("command-{}", arg);
1016        };
1017
1018        let result = get_options_str(&mut [
1019            Opt::SubSwitch("foo", sub),
1020            Opt::SubSwitch("bar", sub),
1021            Opt::SubSwitch("baz", sub),
1022        ], &args);
1023
1024        assert_eq!(result, Ok(()));
1025        assert_eq!(command.into_inner(), "command-baz");
1026    }
1027
1028    #[test]
1029    fn test_sledgehammer_example() {
1030        // pretend we've already done:
1031        // let args: Vec<String> = std::env::args().collect();
1032        let args: Vec<String> = vec!["/usr/local/bin/backup-profile", "--list", "-n", "--backup-dir=/mnt/usr1/backups", "--backup-dir", "/mnt/usr1/backups-test", "-v", "--", "-f.txt"].into_iter().map(|x| x.to_string()).collect();
1033        let prog = args[0].clone();
1034        let args: Vec<String> = args.into_iter().skip(1).collect();
1035        let usage =  || {
1036            let prog = std::path::Path::new(&prog).file_name().unwrap().to_string_lossy();
1037            println!("usage: {prog} [-hlcpnv] [-b BACKUP] [--] [TAR_ARGUMENTS]");
1038            println!("  backup firefox profiles on win32 from cygwin");
1039            println!("  -h,--help               display this usage");
1040            println!("  -b,--backup-dir BACKUP  output backup files to BACKUP (default '.')");
1041            println!("  -l,--list               list profiles and their paths");
1042            println!("  -n,--dry-run            perform dry run with commands printed");
1043            println!("  -p,--print              print Firefox configuration root");
1044            println!("  -v,--verbose            be noisier");
1045        };
1046
1047        let mut backup_dir = String::new();
1048        let mut dry_run = false;
1049        let free_args = RefCell::new(vec![]);
1050        let mut list = false;
1051        let mut print = false;
1052        let mut verbose = false;
1053
1054        let sub: FnFree = &|s| {
1055            free_args.borrow_mut().push(s.to_string());
1056        };
1057
1058        let result = get_options(&mut [
1059            Opt::SubSwitch("help|h", &mut |_| { usage(); std::process::exit(0); }),
1060            Opt::Switch   ("list|l", &mut list),
1061            Opt::Switch   ("p|print", &mut print),
1062            Opt::Switch   ("dry-run|n", &mut dry_run),
1063            Opt::Arg      ("backup-dir|b", &mut backup_dir),
1064            Opt::Switch   ("verbose|v", &mut verbose),
1065            Opt::Free     (sub),
1066        ], &args);
1067
1068        if let Err(ref err) = result {
1069            eprintln!("ERROR: parsing command line arguments: {err}");
1070            //std::process::exit(1); // don't do this in a test
1071        }
1072
1073        // use the above options in a program...
1074
1075        assert_eq!(backup_dir, "/mnt/usr1/backups-test");
1076        assert!(dry_run);
1077        assert_eq!(free_args.into_inner(), vec!["--", "-f.txt"]);
1078        assert!(list);
1079        assert!(!print);
1080        assert!(verbose);
1081
1082        assert_eq!(result, Ok(()));
1083    }
1084}
1085
1086// perl example:
1087//
1088//     sub usage { say "usage: $0 [OPTIONS]... FILES..."; }
1089//     my @args = ();
1090//     sub add_arg {
1091//         push @args, @_;
1092//     }
1093//
1094//     my $options_file = ".optionsrc";
1095//     my $verbose;
1096//
1097//     GetOptions(
1098//         "h|help" => sub { usage; exit 0 },
1099//         "f|options-file" => \$options_file,
1100//         "v|verbose!" => \$verbose,
1101//         "<>" => \&add_arg)
1102//     or die "error parsing command line arguments: $@";