lexarg_parser/
lib.rs

1//! Minimal, API stable CLI parser
2//!
3//! Inspired by [lexopt](https://crates.io/crates/lexopt), `lexarg` simplifies the formula down
4//! further so it can be used for CLI plugin systems.
5//!
6//! ## Example
7//!
8//! ```no_run
9#![doc = include_str!("../examples/hello-parser.rs")]
10//! ```
11
12#![cfg_attr(docsrs, feature(doc_auto_cfg))]
13#![allow(clippy::result_unit_err)]
14#![warn(missing_debug_implementations)]
15#![warn(missing_docs)]
16#![warn(clippy::print_stderr)]
17#![warn(clippy::print_stdout)]
18
19mod ext;
20
21use std::ffi::OsStr;
22
23use ext::OsStrExt as _;
24
25/// A parser for command line arguments.
26#[derive(Debug, Clone)]
27pub struct Parser<'a> {
28    raw: &'a dyn RawArgs,
29    current: usize,
30    state: Option<State<'a>>,
31    was_attached: bool,
32}
33
34impl<'a> Parser<'a> {
35    /// Create a parser from an iterator. This is useful for testing among other things.
36    ///
37    /// The first item from the iterator **must** be the binary name, as from [`std::env::args_os`].
38    ///
39    /// The iterator is consumed immediately.
40    ///
41    /// # Example
42    /// ```
43    /// let args = ["myapp", "-n", "10", "./foo.bar"];
44    /// let mut parser = lexarg_parser::Parser::new(&&args[1..]);
45    /// ```
46    pub fn new(raw: &'a dyn RawArgs) -> Self {
47        Parser {
48            raw,
49            current: 0,
50            state: None,
51            was_attached: false,
52        }
53    }
54
55    /// Get the next option or positional [`Arg`].
56    ///
57    /// Returns `None` if the command line has been exhausted.
58    ///
59    /// Returns [`Arg::Unexpected`] on failure
60    ///
61    /// Notes:
62    /// - `=` is always accepted as a [`Arg::Short("=")`].  If that isn't the case in your
63    ///   application, you may want to special case the error for that.
64    pub fn next_arg(&mut self) -> Option<Arg<'a>> {
65        // Always reset
66        self.was_attached = false;
67
68        match self.state {
69            Some(State::PendingValue(attached)) => {
70                // Last time we got `--long=value`, and `value` hasn't been used.
71                self.state = None;
72                self.current += 1;
73                Some(Arg::Unexpected(attached))
74            }
75            Some(State::PendingShorts(valid, invalid, index)) => {
76                // We're somewhere inside a `-abc` chain. Because we're in `.next_arg()`, not `.next_flag_value()`, we
77                // can assume that the next character is another option.
78                if let Some(next_index) = ceil_char_boundary(valid, index) {
79                    if next_index < valid.len() {
80                        self.state = Some(State::PendingShorts(valid, invalid, next_index));
81                    } else if !invalid.is_empty() {
82                        self.state = Some(State::PendingValue(invalid));
83                    } else {
84                        // No more flags
85                        self.state = None;
86                        self.current += 1;
87                    }
88                    let flag = &valid[index..next_index];
89                    Some(Arg::Short(flag))
90                } else {
91                    debug_assert_ne!(invalid, "");
92                    if index == 0 {
93                        panic!("there should have been a `-`")
94                    } else if index == 1 {
95                        // Like long flags, include `-`
96                        let arg = self
97                            .raw
98                            .get(self.current)
99                            .expect("`current` is valid if state is `Shorts`");
100                        self.state = None;
101                        self.current += 1;
102                        Some(Arg::Unexpected(arg))
103                    } else {
104                        self.state = None;
105                        self.current += 1;
106                        Some(Arg::Unexpected(invalid))
107                    }
108                }
109            }
110            Some(State::Escaped) => {
111                self.state = Some(State::Escaped);
112                self.next_raw_().map(Arg::Value)
113            }
114            None => {
115                let arg = self.raw.get(self.current)?;
116                if arg == "--" {
117                    self.state = Some(State::Escaped);
118                    self.current += 1;
119                    Some(Arg::Escape(arg.to_str().expect("`--` is valid UTF-8")))
120                } else if arg == "-" {
121                    self.state = None;
122                    self.current += 1;
123                    Some(Arg::Value(arg))
124                } else if let Some(long) = arg.strip_prefix("--") {
125                    let (name, value) = long
126                        .split_once("=")
127                        .map(|(n, v)| (n, Some(v)))
128                        .unwrap_or((long, None));
129                    if name.is_empty() {
130                        self.state = None;
131                        self.current += 1;
132                        Some(Arg::Unexpected(arg))
133                    } else if let Ok(name) = name.try_str() {
134                        if let Some(value) = value {
135                            self.state = Some(State::PendingValue(value));
136                        } else {
137                            self.state = None;
138                            self.current += 1;
139                        }
140                        Some(Arg::Long(name))
141                    } else {
142                        self.state = None;
143                        self.current += 1;
144                        Some(Arg::Unexpected(arg))
145                    }
146                } else if arg.starts_with("-") {
147                    let (valid, invalid) = split_nonutf8_once(arg);
148                    let invalid = invalid.unwrap_or_default();
149                    self.state = Some(State::PendingShorts(valid, invalid, 1));
150                    self.next_arg()
151                } else {
152                    self.state = None;
153                    self.current += 1;
154                    Some(Arg::Value(arg))
155                }
156            }
157        }
158    }
159
160    /// Get a flag's value
161    ///
162    /// This function should normally be called right after seeing a flag that expects a value;
163    /// positional arguments should be collected with [`Parser::next_arg()`].
164    ///
165    /// A value is collected even if it looks like an option (i.e., starts with `-`).
166    ///
167    /// `None` is returned if there is not another applicable flag value, including:
168    /// - No more arguments are present
169    /// - `--` was encountered, meaning all remaining arguments are positional
170    /// - Being called again when the first value was attached (`--flag=value`, `-Fvalue`, `-F=value`)
171    pub fn next_flag_value(&mut self) -> Option<&'a OsStr> {
172        if self.was_attached {
173            debug_assert!(!self.has_pending());
174            None
175        } else if let Some(value) = self.next_attached_value() {
176            Some(value)
177        } else {
178            self.next_detached_value()
179        }
180    }
181
182    /// Get a flag's attached value (`--flag=value`, `-Fvalue`, `-F=value`)
183    ///
184    /// This is a more specialized variant of [`Parser::next_flag_value`] for when only attached
185    /// values are allowed, e.g. `--color[=<when>]`.
186    pub fn next_attached_value(&mut self) -> Option<&'a OsStr> {
187        match self.state? {
188            State::PendingValue(attached) => {
189                self.state = None;
190                self.current += 1;
191                self.was_attached = true;
192                Some(attached)
193            }
194            State::PendingShorts(_, _, index) => {
195                let arg = self
196                    .raw
197                    .get(self.current)
198                    .expect("`current` is valid if state is `Shorts`");
199                self.state = None;
200                self.current += 1;
201                if index == arg.len() {
202                    None
203                } else {
204                    // SAFETY: everything preceding `index` were a short flags, making them valid UTF-8
205                    let remainder = unsafe { ext::split_at(arg, index) }.1;
206                    let remainder = remainder.strip_prefix("=").unwrap_or(remainder);
207                    self.was_attached = true;
208                    Some(remainder)
209                }
210            }
211            State::Escaped => None,
212        }
213    }
214
215    fn next_detached_value(&mut self) -> Option<&'a OsStr> {
216        if self.state == Some(State::Escaped) {
217            // Escaped values are positional-only
218            return None;
219        }
220
221        if self.peek_raw_()? == "--" {
222            None
223        } else {
224            self.next_raw_()
225        }
226    }
227
228    /// Get the next argument, independent of what it looks like
229    ///
230    /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present
231    pub fn next_raw(&mut self) -> Result<Option<&'a OsStr>, ()> {
232        if self.has_pending() {
233            Err(())
234        } else {
235            self.was_attached = false;
236            Ok(self.next_raw_())
237        }
238    }
239
240    /// Collect all remaining arguments, independent of what they look like
241    ///
242    /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present
243    pub fn remaining_raw(&mut self) -> Result<impl Iterator<Item = &'a OsStr> + '_, ()> {
244        if self.has_pending() {
245            Err(())
246        } else {
247            self.was_attached = false;
248            Ok(std::iter::from_fn(|| self.next_raw_()))
249        }
250    }
251
252    /// Get the next argument, independent of what it looks like
253    ///
254    /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present
255    pub fn peek_raw(&self) -> Result<Option<&'a OsStr>, ()> {
256        if self.has_pending() {
257            Err(())
258        } else {
259            Ok(self.peek_raw_())
260        }
261    }
262
263    fn peek_raw_(&self) -> Option<&'a OsStr> {
264        self.raw.get(self.current)
265    }
266
267    fn next_raw_(&mut self) -> Option<&'a OsStr> {
268        debug_assert!(!self.has_pending());
269        debug_assert!(!self.was_attached);
270
271        let next = self.raw.get(self.current)?;
272        self.current += 1;
273        Some(next)
274    }
275
276    fn has_pending(&self) -> bool {
277        self.state.as_ref().map(State::has_pending).unwrap_or(false)
278    }
279}
280
281/// Accessor for unparsed arguments
282pub trait RawArgs: std::fmt::Debug + private::Sealed {
283    /// Returns a reference to an element or subslice depending on the type of index.
284    ///
285    /// - If given a position, returns a reference to the element at that position or None if out
286    ///   of bounds.
287    /// - If given a range, returns the subslice corresponding to that range, or None if out
288    ///   of bounds.
289    fn get(&self, index: usize) -> Option<&OsStr>;
290
291    /// Returns the number of elements in the slice.
292    fn len(&self) -> usize;
293
294    /// Returns `true` if the slice has a length of 0.
295    fn is_empty(&self) -> bool;
296}
297
298impl<const C: usize, S> RawArgs for [S; C]
299where
300    S: AsRef<OsStr> + std::fmt::Debug,
301{
302    #[inline]
303    fn get(&self, index: usize) -> Option<&OsStr> {
304        self.as_slice().get(index).map(|s| s.as_ref())
305    }
306
307    #[inline]
308    fn len(&self) -> usize {
309        C
310    }
311
312    #[inline]
313    fn is_empty(&self) -> bool {
314        C != 0
315    }
316}
317
318impl<S> RawArgs for &'_ [S]
319where
320    S: AsRef<OsStr> + std::fmt::Debug,
321{
322    #[inline]
323    fn get(&self, index: usize) -> Option<&OsStr> {
324        (*self).get(index).map(|s| s.as_ref())
325    }
326
327    #[inline]
328    fn len(&self) -> usize {
329        (*self).len()
330    }
331
332    #[inline]
333    fn is_empty(&self) -> bool {
334        (*self).is_empty()
335    }
336}
337
338impl<S> RawArgs for Vec<S>
339where
340    S: AsRef<OsStr> + std::fmt::Debug,
341{
342    #[inline]
343    fn get(&self, index: usize) -> Option<&OsStr> {
344        self.as_slice().get(index).map(|s| s.as_ref())
345    }
346
347    #[inline]
348    fn len(&self) -> usize {
349        self.len()
350    }
351
352    #[inline]
353    fn is_empty(&self) -> bool {
354        self.is_empty()
355    }
356}
357
358#[derive(Debug, Copy, Clone, PartialEq, Eq)]
359enum State<'a> {
360    /// We have a value left over from `--option=value`
361    PendingValue(&'a OsStr),
362    /// We're in the middle of `-abc`
363    ///
364    /// On Windows and other non-UTF8-OsString platforms this Vec should
365    /// only ever contain valid UTF-8 (and could instead be a String).
366    PendingShorts(&'a str, &'a OsStr, usize),
367    /// We saw `--` and know no more options are coming.
368    Escaped,
369}
370
371impl State<'_> {
372    fn has_pending(&self) -> bool {
373        match self {
374            Self::PendingValue(_) | Self::PendingShorts(_, _, _) => true,
375            Self::Escaped => false,
376        }
377    }
378}
379
380/// A command line argument found by [`Parser`], either an option or a positional argument
381#[derive(Debug, Copy, Clone, PartialEq, Eq)]
382pub enum Arg<'a> {
383    /// A short option, e.g. `Short("q")` for `-q`
384    Short(&'a str),
385    /// A long option, e.g. `Long("verbose")` for `--verbose`
386    ///
387    /// The dashes are not included
388    Long(&'a str),
389    /// A positional argument, e.g. `/dev/null`
390    Value(&'a OsStr),
391    /// Marks the following values have been escaped with `--`
392    Escape(&'a str),
393    /// User passed something in that doesn't work
394    Unexpected(&'a OsStr),
395}
396
397fn split_nonutf8_once(b: &OsStr) -> (&str, Option<&OsStr>) {
398    match b.try_str() {
399        Ok(s) => (s, None),
400        Err(err) => {
401            // SAFETY: `char_indices` ensures `index` is at a valid UTF-8 boundary
402            let (valid, after_valid) = unsafe { ext::split_at(b, err.valid_up_to()) };
403            let valid = valid.try_str().unwrap();
404            (valid, Some(after_valid))
405        }
406    }
407}
408
409fn ceil_char_boundary(s: &str, curr_boundary: usize) -> Option<usize> {
410    (curr_boundary + 1..=s.len()).find(|i| s.is_char_boundary(*i))
411}
412
413mod private {
414    use super::OsStr;
415
416    #[allow(unnameable_types)]
417    pub trait Sealed {}
418    impl<const C: usize, S> Sealed for [S; C] where S: AsRef<OsStr> + std::fmt::Debug {}
419    impl<S> Sealed for &'_ [S] where S: AsRef<OsStr> + std::fmt::Debug {}
420    impl<S> Sealed for Vec<S> where S: AsRef<OsStr> + std::fmt::Debug {}
421}
422
423#[cfg(test)]
424mod tests {
425    use super::Arg::*;
426    use super::*;
427
428    #[test]
429    fn test_basic() {
430        let mut p = Parser::new(&["-n", "10", "foo", "-", "--", "baz", "-qux"]);
431        assert_eq!(p.next_arg().unwrap(), Short("n"));
432        assert_eq!(p.next_flag_value().unwrap(), "10");
433        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("foo")));
434        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-")));
435        assert_eq!(p.next_arg().unwrap(), Escape("--"));
436        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("baz")));
437        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-qux")));
438        assert_eq!(p.next_arg(), None);
439        assert_eq!(p.next_arg(), None);
440        assert_eq!(p.next_arg(), None);
441    }
442
443    #[test]
444    fn test_combined() {
445        let mut p = Parser::new(&["-abc", "-fvalue", "-xfvalue"]);
446        assert_eq!(p.next_arg().unwrap(), Short("a"));
447        assert_eq!(p.next_arg().unwrap(), Short("b"));
448        assert_eq!(p.next_arg().unwrap(), Short("c"));
449        assert_eq!(p.next_arg().unwrap(), Short("f"));
450        assert_eq!(p.next_flag_value().unwrap(), "value");
451        assert_eq!(p.next_arg().unwrap(), Short("x"));
452        assert_eq!(p.next_arg().unwrap(), Short("f"));
453        assert_eq!(p.next_flag_value().unwrap(), "value");
454        assert_eq!(p.next_arg(), None);
455    }
456
457    #[test]
458    fn test_long() {
459        let mut p = Parser::new(&["--foo", "--bar=qux", "--foobar=qux=baz"]);
460        assert_eq!(p.next_arg().unwrap(), Long("foo"));
461        assert_eq!(p.next_arg().unwrap(), Long("bar"));
462        assert_eq!(p.next_flag_value().unwrap(), "qux");
463        assert_eq!(p.next_flag_value(), None);
464        assert_eq!(p.next_arg().unwrap(), Long("foobar"));
465        assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("qux=baz")));
466        assert_eq!(p.next_arg(), None);
467    }
468
469    #[test]
470    fn test_dash_args() {
471        // "--" should indicate the end of the options
472        let mut p = Parser::new(&["-x", "--", "-y"]);
473        assert_eq!(p.next_arg().unwrap(), Short("x"));
474        assert_eq!(p.next_arg().unwrap(), Escape("--"));
475        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y")));
476        assert_eq!(p.next_arg(), None);
477
478        // ...even if it's an argument of an option
479        let mut p = Parser::new(&["-x", "--", "-y"]);
480        assert_eq!(p.next_arg().unwrap(), Short("x"));
481        assert_eq!(p.next_flag_value(), None);
482        assert_eq!(p.next_arg().unwrap(), Escape("--"));
483        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y")));
484        assert_eq!(p.next_arg(), None);
485
486        // "-" is a valid value that should not be treated as an option
487        let mut p = Parser::new(&["-x", "-", "-y"]);
488        assert_eq!(p.next_arg().unwrap(), Short("x"));
489        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-")));
490        assert_eq!(p.next_arg().unwrap(), Short("y"));
491        assert_eq!(p.next_arg(), None);
492
493        // '-' is a silly and hard to use short option, but other parsers treat
494        // it like an option in this position
495        let mut p = Parser::new(&["-x-y"]);
496        assert_eq!(p.next_arg().unwrap(), Short("x"));
497        assert_eq!(p.next_arg().unwrap(), Short("-"));
498        assert_eq!(p.next_arg().unwrap(), Short("y"));
499        assert_eq!(p.next_arg(), None);
500    }
501
502    #[test]
503    fn test_missing_value() {
504        let mut p = Parser::new(&["-o"]);
505        assert_eq!(p.next_arg().unwrap(), Short("o"));
506        assert_eq!(p.next_flag_value(), None);
507
508        let mut q = Parser::new(&["--out"]);
509        assert_eq!(q.next_arg().unwrap(), Long("out"));
510        assert_eq!(q.next_flag_value(), None);
511
512        let args: [&OsStr; 0] = [];
513        let mut r = Parser::new(&args);
514        assert_eq!(r.next_flag_value(), None);
515    }
516
517    #[test]
518    fn test_weird_args() {
519        let mut p = Parser::new(&[
520            "--=", "--=3", "-", "-x", "--", "-", "-x", "--", "", "-", "-x",
521        ]);
522        assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--=")));
523        assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--=3")));
524        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-")));
525        assert_eq!(p.next_arg().unwrap(), Short("x"));
526        assert_eq!(p.next_arg().unwrap(), Escape("--"));
527        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-")));
528        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-x")));
529        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("--")));
530        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("")));
531        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-")));
532        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-x")));
533        assert_eq!(p.next_arg(), None);
534
535        let bad = bad_string("--=@");
536        let args = [&bad];
537        let mut q = Parser::new(&args);
538        assert_eq!(q.next_arg().unwrap(), Unexpected(OsStr::new(&bad)));
539
540        let mut r = Parser::new(&[""]);
541        assert_eq!(r.next_arg().unwrap(), Value(OsStr::new("")));
542    }
543
544    #[test]
545    fn test_unicode() {
546        let mut p = Parser::new(&["-aµ", "--µ=10", "µ", "--foo=µ"]);
547        assert_eq!(p.next_arg().unwrap(), Short("a"));
548        assert_eq!(p.next_arg().unwrap(), Short("µ"));
549        assert_eq!(p.next_arg().unwrap(), Long("µ"));
550        assert_eq!(p.next_flag_value().unwrap(), "10");
551        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("µ")));
552        assert_eq!(p.next_arg().unwrap(), Long("foo"));
553        assert_eq!(p.next_flag_value().unwrap(), "µ");
554    }
555
556    #[cfg(any(unix, target_os = "wasi", windows))]
557    #[test]
558    fn test_mixed_invalid() {
559        let args = [bad_string("--foo=@@@")];
560        let mut p = Parser::new(&args);
561        assert_eq!(p.next_arg().unwrap(), Long("foo"));
562        assert_eq!(p.next_flag_value().unwrap(), bad_string("@@@"));
563
564        let args = [bad_string("-💣@@@")];
565        let mut q = Parser::new(&args);
566        assert_eq!(q.next_arg().unwrap(), Short("💣"));
567        assert_eq!(q.next_flag_value().unwrap(), bad_string("@@@"));
568
569        let args = [bad_string("-f@@@")];
570        let mut r = Parser::new(&args);
571        assert_eq!(r.next_arg().unwrap(), Short("f"));
572        assert_eq!(r.next_arg().unwrap(), Unexpected(&bad_string("@@@")));
573        assert_eq!(r.next_arg(), None);
574
575        let args = [bad_string("--foo=bar=@@@")];
576        let mut s = Parser::new(&args);
577        assert_eq!(s.next_arg().unwrap(), Long("foo"));
578        assert_eq!(s.next_flag_value().unwrap(), bad_string("bar=@@@"));
579    }
580
581    #[cfg(any(unix, target_os = "wasi", windows))]
582    #[test]
583    fn test_separate_invalid() {
584        let args = [bad_string("--foo"), bad_string("@@@")];
585        let mut p = Parser::new(&args);
586        assert_eq!(p.next_arg().unwrap(), Long("foo"));
587        assert_eq!(p.next_flag_value().unwrap(), bad_string("@@@"));
588    }
589
590    #[cfg(any(unix, target_os = "wasi", windows))]
591    #[test]
592    fn test_invalid_long_option() {
593        let args = [bad_string("--@=10")];
594        let mut p = Parser::new(&args);
595        assert_eq!(p.next_arg().unwrap(), Unexpected(&args[0]));
596        assert_eq!(p.next_arg(), None);
597
598        let args = [bad_string("--@")];
599        let mut p = Parser::new(&args);
600        assert_eq!(p.next_arg().unwrap(), Unexpected(&args[0]));
601        assert_eq!(p.next_arg(), None);
602    }
603
604    #[cfg(any(unix, target_os = "wasi", windows))]
605    #[test]
606    fn test_invalid_short_option() {
607        let args = [bad_string("-@")];
608        let mut p = Parser::new(&args);
609        assert_eq!(p.next_arg().unwrap(), Unexpected(&args[0]));
610        assert_eq!(p.next_arg(), None);
611    }
612
613    #[test]
614    fn short_opt_equals_sign() {
615        let mut p = Parser::new(&["-a=b"]);
616        assert_eq!(p.next_arg().unwrap(), Short("a"));
617        assert_eq!(p.next_flag_value().unwrap(), OsStr::new("b"));
618        assert_eq!(p.next_arg(), None);
619
620        let mut p = Parser::new(&["-a=b", "c"]);
621        assert_eq!(p.next_arg().unwrap(), Short("a"));
622        assert_eq!(p.next_flag_value().unwrap(), OsStr::new("b"));
623        assert_eq!(p.next_flag_value(), None);
624        assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("c")));
625        assert_eq!(p.next_arg(), None);
626
627        let mut p = Parser::new(&["-a=b"]);
628        assert_eq!(p.next_arg().unwrap(), Short("a"));
629        assert_eq!(p.next_arg().unwrap(), Short("="));
630        assert_eq!(p.next_arg().unwrap(), Short("b"));
631        assert_eq!(p.next_arg(), None);
632
633        let mut p = Parser::new(&["-a="]);
634        assert_eq!(p.next_arg().unwrap(), Short("a"));
635        assert_eq!(p.next_flag_value().unwrap(), OsStr::new(""));
636        assert_eq!(p.next_arg(), None);
637
638        let mut p = Parser::new(&["-a=="]);
639        assert_eq!(p.next_arg().unwrap(), Short("a"));
640        assert_eq!(p.next_flag_value().unwrap(), OsStr::new("="));
641        assert_eq!(p.next_arg(), None);
642
643        let mut p = Parser::new(&["-abc=de"]);
644        assert_eq!(p.next_arg().unwrap(), Short("a"));
645        assert_eq!(p.next_flag_value().unwrap(), OsStr::new("bc=de"));
646        assert_eq!(p.next_arg(), None);
647
648        let mut p = Parser::new(&["-abc==de"]);
649        assert_eq!(p.next_arg().unwrap(), Short("a"));
650        assert_eq!(p.next_arg().unwrap(), Short("b"));
651        assert_eq!(p.next_arg().unwrap(), Short("c"));
652        assert_eq!(p.next_flag_value().unwrap(), OsStr::new("=de"));
653        assert_eq!(p.next_arg(), None);
654
655        let mut p = Parser::new(&["-a="]);
656        assert_eq!(p.next_arg().unwrap(), Short("a"));
657        assert_eq!(p.next_arg().unwrap(), Short("="));
658        assert_eq!(p.next_arg(), None);
659
660        let mut p = Parser::new(&["-="]);
661        assert_eq!(p.next_arg().unwrap(), Short("="));
662        assert_eq!(p.next_arg(), None);
663
664        let mut p = Parser::new(&["-=a"]);
665        assert_eq!(p.next_arg().unwrap(), Short("="));
666        assert_eq!(p.next_arg().unwrap(), Short("a"));
667        assert_eq!(p.next_arg(), None);
668    }
669
670    #[cfg(any(unix, target_os = "wasi", windows))]
671    #[test]
672    fn short_opt_equals_sign_invalid() {
673        let bad = bad_string("@");
674        let args = [bad_string("-a=@")];
675        let mut p = Parser::new(&args);
676        assert_eq!(p.next_arg().unwrap(), Short("a"));
677        assert_eq!(p.next_flag_value().unwrap(), bad_string("@"));
678        assert_eq!(p.next_arg(), None);
679
680        let mut p = Parser::new(&args);
681        assert_eq!(p.next_arg().unwrap(), Short("a"));
682        assert_eq!(p.next_arg().unwrap(), Short("="));
683        assert_eq!(p.next_arg().unwrap(), Unexpected(&bad));
684        assert_eq!(p.next_arg(), None);
685    }
686
687    #[test]
688    fn remaining_raw() {
689        let mut p = Parser::new(&["-a", "b", "c", "d"]);
690        assert_eq!(
691            p.remaining_raw().unwrap().collect::<Vec<_>>(),
692            &["-a", "b", "c", "d"]
693        );
694        // Consumed all
695        assert!(p.next_arg().is_none());
696        assert!(p.remaining_raw().is_ok());
697        assert_eq!(p.remaining_raw().unwrap().collect::<Vec<_>>().len(), 0);
698
699        let mut p = Parser::new(&["-ab", "c", "d"]);
700        p.next_arg().unwrap();
701        // Attached value
702        assert!(p.remaining_raw().is_err());
703        p.next_attached_value().unwrap();
704        assert_eq!(p.remaining_raw().unwrap().collect::<Vec<_>>(), &["c", "d"]);
705        // Consumed all
706        assert!(p.next_arg().is_none());
707        assert_eq!(p.remaining_raw().unwrap().collect::<Vec<_>>().len(), 0);
708    }
709
710    /// Transform @ characters into invalid unicode.
711    fn bad_string(text: &str) -> std::ffi::OsString {
712        #[cfg(any(unix, target_os = "wasi"))]
713        {
714            #[cfg(unix)]
715            use std::os::unix::ffi::OsStringExt;
716            #[cfg(target_os = "wasi")]
717            use std::os::wasi::ffi::OsStringExt;
718            let mut text = text.as_bytes().to_vec();
719            for ch in &mut text {
720                if *ch == b'@' {
721                    *ch = b'\xFF';
722                }
723            }
724            std::ffi::OsString::from_vec(text)
725        }
726        #[cfg(windows)]
727        {
728            use std::os::windows::ffi::OsStringExt;
729            let mut out = Vec::new();
730            for ch in text.chars() {
731                if ch == '@' {
732                    out.push(0xD800);
733                } else {
734                    let mut buf = [0; 2];
735                    out.extend(&*ch.encode_utf16(&mut buf));
736                }
737            }
738            std::ffi::OsString::from_wide(&out)
739        }
740        #[cfg(not(any(unix, target_os = "wasi", windows)))]
741        {
742            if text.contains('@') {
743                unimplemented!("Don't know how to create invalid OsStrings on this platform");
744            }
745            text.into()
746        }
747    }
748
749    /// Basic exhaustive testing of short combinations of "interesting"
750    /// arguments. They should not panic, not hang, and pass some checks.
751    ///
752    /// The advantage compared to full fuzzing is that it runs on all platforms
753    /// and together with the other tests. cargo-fuzz doesn't work on Windows
754    /// and requires a special incantation.
755    ///
756    /// A disadvantage is that it's still limited by arguments I could think of
757    /// and only does very short sequences. Another is that it's bad at
758    /// reporting failure, though the println!() helps.
759    ///
760    /// This test takes a while to run.
761    #[test]
762    fn basic_fuzz() {
763        #[cfg(any(windows, unix, target_os = "wasi"))]
764        const VOCABULARY: &[&str] = &[
765            "", "-", "--", "---", "a", "-a", "-aa", "@", "-@", "-a@", "-@a", "--a", "--@", "--a=a",
766            "--a=", "--a=@", "--@=a", "--=", "--=@", "--=a", "-@@", "-a=a", "-a=", "-=", "-a-",
767        ];
768        #[cfg(not(any(windows, unix, target_os = "wasi")))]
769        const VOCABULARY: &[&str] = &[
770            "", "-", "--", "---", "a", "-a", "-aa", "--a", "--a=a", "--a=", "--=", "--=a", "-a=a",
771            "-a=", "-=", "-a-",
772        ];
773        let args: [&OsStr; 0] = [];
774        exhaust(Parser::new(&args), vec![]);
775        let vocabulary: Vec<std::ffi::OsString> =
776            VOCABULARY.iter().map(|&s| bad_string(s)).collect();
777        let mut permutations = vec![vec![]];
778        for _ in 0..3 {
779            let mut new = Vec::new();
780            for old in permutations {
781                for word in &vocabulary {
782                    let mut extended = old.clone();
783                    extended.push(word);
784                    new.push(extended);
785                }
786            }
787            permutations = new;
788            for permutation in &permutations {
789                println!("Starting {permutation:?}");
790                let p = Parser::new(permutation);
791                exhaust(p, vec![]);
792            }
793        }
794    }
795
796    /// Run many sequences of methods on a Parser.
797    fn exhaust(parser: Parser<'_>, path: Vec<String>) {
798        if path.len() > 100 {
799            panic!("Stuck in loop: {path:?}");
800        }
801
802        if parser.has_pending() {
803            {
804                let mut parser = parser.clone();
805                let next = parser.next_arg();
806                assert!(
807                    matches!(next, Some(Unexpected(_)) | Some(Short(_))),
808                    "{next:?} via {path:?}",
809                );
810                let mut path = path.clone();
811                path.push(format!("pending-next-{next:?}"));
812                exhaust(parser, path);
813            }
814
815            {
816                let mut parser = parser.clone();
817                let next = parser.next_flag_value();
818                assert!(next.is_some(), "{next:?} via {path:?}",);
819                let mut path = path;
820                path.push(format!("pending-value-{next:?}"));
821                exhaust(parser, path);
822            }
823        } else {
824            {
825                let mut parser = parser.clone();
826                let next = parser.next_arg();
827                match &next {
828                    None => {
829                        assert!(
830                            matches!(parser.state, None | Some(State::Escaped)),
831                            "{next:?} via {path:?}",
832                        );
833                        assert_eq!(parser.current, parser.raw.len(), "{next:?} via {path:?}",);
834                    }
835                    _ => {
836                        let mut path = path.clone();
837                        path.push(format!("next-{next:?}"));
838                        exhaust(parser, path);
839                    }
840                }
841            }
842
843            {
844                let mut parser = parser.clone();
845                let next = parser.next_flag_value();
846                match &next {
847                    None => {
848                        assert!(
849                            matches!(parser.state, None | Some(State::Escaped)),
850                            "{next:?} via {path:?}",
851                        );
852                        if parser.state.is_none()
853                            && !parser.was_attached
854                            && parser.peek_raw_() != Some(OsStr::new("--"))
855                        {
856                            assert_eq!(parser.current, parser.raw.len(), "{next:?} via {path:?}",);
857                        }
858                    }
859                    Some(_) => {
860                        assert!(
861                            matches!(parser.state, None | Some(State::Escaped)),
862                            "{next:?} via {path:?}",
863                        );
864                        let mut path = path;
865                        path.push(format!("value-{next:?}"));
866                        exhaust(parser, path);
867                    }
868                }
869            }
870        }
871    }
872}
873
874#[doc = include_str!("../README.md")]
875#[cfg(doctest)]
876pub struct ReadmeDoctests;