Skip to main content

yash_builtin/common/
syntax.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU 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 General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Command-line argument syntax parser
18//!
19//! This module provides functionalities for parsing command-line arguments into
20//! options and operands.
21//!
22//! This module's parser can parse command lines that adhere to [POSIX Utility
23//! Syntax Guidelines] and support non-standard syntax extensions such as long
24//! options and options after operands.
25//!
26//! [POSIX Utility Syntax Guidelines]: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap12.html#tag_12_02
27//!
28//! # Usage
29//!
30//! To parse arguments, first create a list of [option specs](OptionSpec). Each
31//! option spec describes a single possible option. Then call
32//! [`parse_arguments`] with the option specs and the list of arguments to
33//! parse. The function returns a pair of [option occurrences](OptionOccurrence)
34//! and operands. In case of an error, the function returns a [`ParseError`].
35//!
36//! [`ConflictingOptionError`] is a helper object for constructing an error
37//! message from a list of conflicting option occurrences. You need to
38//! instantiate this object for yourself as this module does not provide a
39//! function for detecting conflicting options.
40//!
41//! # Example
42//!
43//! ```
44//! use yash_builtin::common::syntax::*;
45//! let specs = &[
46//!     OptionSpec::new().short('a'),
47//!     OptionSpec::new().short('b').long("bar"),
48//!     OptionSpec::new().long("baz").argument(OptionArgumentSpec::Required),
49//! ];
50//!
51//! let arguments = Field::dummies(["-ba", "--baz", "--", "--bar", "--", "-a", "foo"]);
52//! let (options, operands) = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
53//! assert_eq!(options.len(), 4);
54//! assert_eq!(options[0].spec, &specs[1]); // 'b' in "-ba"
55//! assert_eq!(options[0].argument, None);
56//! assert_eq!(options[1].spec, &specs[0]); // 'a' in "-ba"
57//! assert_eq!(options[1].argument, None);
58//! assert_eq!(options[2].spec, &specs[2]); // "--baz"
59//! assert_eq!(options[2].argument, Some(Field::dummy("--")));
60//! assert_eq!(options[3].spec, &specs[1]); // "--bar"
61//! assert_eq!(options[3].argument, None);
62//! assert_eq!(operands, Field::dummies(["-a", "foo"]));
63//! ```
64
65use std::iter::Peekable;
66use thiserror::Error;
67use yash_env::source::pretty::{Report, ReportType, Snippet};
68use yash_env::source::{
69    Location,
70    pretty::{Span, SpanRole, add_span},
71};
72
73#[doc(no_inline)]
74pub use yash_env::semantics::Field;
75
76/// Specification for an options's argument
77#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
78#[non_exhaustive]
79pub enum OptionArgumentSpec {
80    /// The option does not take an argument. (default)
81    #[default]
82    None,
83    /// The option requires an argument.
84    Required,
85    // /// The option may have an argument.
86    // Optional,
87}
88
89/// Specification of an option
90///
91/// This structure may contain the following properties:
92///
93/// - Short option name (a single character)
94/// - Long option name (a string)
95/// - Whether this option takes an argument
96///
97/// All of these are optional, but either or both of the short and long names
98/// should be set for the option spec to have meaningful effect.
99#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
100pub struct OptionSpec<'a> {
101    short: Option<char>,
102    long: Option<&'a str>,
103    argument: OptionArgumentSpec,
104}
105
106impl OptionSpec<'static> {
107    /// Creates a new empty option spec.
108    pub const fn new() -> Self {
109        OptionSpec {
110            short: None,
111            long: None,
112            argument: OptionArgumentSpec::None,
113        }
114    }
115}
116
117#[test]
118fn new_option_spec_eq_default() {
119    assert_eq!(OptionSpec::new(), OptionSpec::default());
120}
121
122impl OptionSpec<'_> {
123    /// Returns the short option name.
124    pub const fn get_short(&self) -> Option<char> {
125        self.short
126    }
127
128    /// Gives a short name for this option.
129    ///
130    /// The name should not be a hyphen.
131    pub fn set_short(&mut self, name: char) {
132        self.short = Some(name);
133    }
134
135    /// Chained version of [`set_short`](Self::set_short)
136    pub const fn short(mut self, name: char) -> Self {
137        self.short = Some(name);
138        self
139    }
140}
141
142impl<'a> OptionSpec<'a> {
143    /// Returns the long option name.
144    pub const fn get_long(&self) -> Option<&'a str> {
145        self.long
146    }
147
148    /// Gives a long name for this option.
149    ///
150    /// The name should not start with `"--"` or include `"="`.
151    pub fn set_long(&mut self, name: &'a str) {
152        self.long = Some(name);
153    }
154
155    /// Chained version of [`set_long`](Self::set_long)
156    pub const fn long(mut self, name: &'a str) -> Self {
157        self.long = Some(name);
158        self
159    }
160}
161
162impl OptionSpec<'_> {
163    /// Returns whether this option takes an argument.
164    pub const fn get_argument(&self) -> OptionArgumentSpec {
165        self.argument
166    }
167
168    /// Specifies whether this option takes an argument.
169    pub fn set_argument(&mut self, argument: OptionArgumentSpec) {
170        self.argument = argument;
171    }
172
173    /// Chained version of [`set_argument`](Self::set_argument)
174    pub const fn argument(mut self, argument: OptionArgumentSpec) -> Self {
175        self.argument = argument;
176        self
177    }
178}
179
180/// Returns the option name like `-f` or `--foo`.
181///
182/// If the spec has both short and long names, the result is like `-f/--foo`.
183/// If the spec has neither of them, the result is `?`.
184impl std::fmt::Display for OptionSpec<'_> {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        if let Some(short) = self.short {
187            write!(f, "-{short}")?;
188            if let Some(long) = self.long {
189                write!(f, "/--{long}")?;
190            }
191            Ok(())
192        } else if let Some(long) = self.long {
193            write!(f, "--{long}")
194        } else {
195            write!(f, "?")
196        }
197    }
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201enum LongMatch {
202    None,
203    Partial,
204    Exact,
205}
206
207impl OptionSpec<'_> {
208    fn long_match(&self, name: &str) -> LongMatch {
209        if let Some(long) = self.long {
210            if long.starts_with(name) {
211                return if long.len() == name.len() {
212                    LongMatch::Exact
213                } else {
214                    LongMatch::Partial
215                };
216            }
217        }
218        LongMatch::None
219    }
220}
221
222/// Configuration for customizing the argument parsing behavior
223///
224/// # Examples
225///
226/// The default configuration disables all non-portable extensions:
227///
228/// ```
229/// # use yash_builtin::common::syntax::Mode;
230/// let mode = Mode::default();
231/// assert!(!mode.accepts_long_options());
232/// # // TODO other properties
233/// ```
234///
235/// The [`with_extensions`](Self::with_extensions) function returns a `Mode`
236/// with those extensions enabled.
237///
238/// ```
239/// # use yash_builtin::common::syntax::Mode;
240/// let mode = Mode::with_extensions();
241/// assert!(mode.accepts_long_options());
242/// # // TODO other properties
243/// ```
244#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
245pub struct Mode {
246    long_options: bool,
247    // TODO Change long_options to non_portable_option_names
248    // TODO options_after_operands
249    // TODO negative_integer_operands
250}
251
252impl Mode {
253    /// Returns a new `Mode` with non-portable extensions enabled.
254    pub const fn with_extensions() -> Self {
255        Mode { long_options: true }
256    }
257
258    /// Convenience initializer
259    ///
260    /// This function returns `Self::default()` or `Self::with_extensions()`
261    /// depending on `env.options.get(PosixlyCorrect)`.
262    pub fn with_env<S>(env: &yash_env::Env<S>) -> Self {
263        use yash_env::option::{Off, On, PosixlyCorrect};
264        match env.options.get(PosixlyCorrect) {
265            On => Self::default(),
266            Off => Self::with_extensions(),
267        }
268    }
269
270    /// Whether the parser accepts long options or not
271    pub const fn accepts_long_options(&self) -> bool {
272        self.long_options
273    }
274
275    /// Sets whether the parser accepts long options or not.
276    pub fn accept_long_options(&mut self, accept: bool) -> &mut Self {
277        self.long_options = accept;
278        self
279    }
280}
281
282/// Occurrence of an option
283#[derive(Clone, Debug, Eq, PartialEq)]
284#[non_exhaustive]
285pub struct OptionOccurrence<'a> {
286    /// Specification for this option
287    pub spec: &'a OptionSpec<'a>,
288
289    /// Location of the field containing this option
290    pub location: Location,
291
292    /// Argument to this option
293    ///
294    /// This value is always `None` for an option that does not take an argument.
295    ///
296    /// This value always contains a field for an option that requires an argument.
297    ///
298    /// If the option name and its argument are given in a single field,
299    /// the field value is modified to contain only the option argument.
300    pub argument: Option<Field>,
301}
302
303/// Error in command line parsing
304#[derive(Clone, Debug, Eq, Error, PartialEq)]
305#[non_exhaustive]
306pub enum ParseError<'a> {
307    /// Short option that is not defined in the option specs
308    #[error("unknown option {0:?}")]
309    UnknownShortOption(char, Field),
310
311    /// Long option that is not defined in the option specs
312    #[error("unknown option {:?}", long_option_name(.0))]
313    UnknownLongOption(Field),
314
315    // TODO Change this to NonPortableOptionName
316    /// Long option that is defined in an option spec but disabled by
317    /// configuration ([`Mode`]).
318    #[error("unsupported option {:?}", .0.value)]
319    UnsupportedLongOption(Field, &'a OptionSpec<'a>),
320
321    /// Long option that matches more than one option spec
322    ///
323    /// The second item of the tuple is a list of all option specs that matched.
324    #[error("ambiguous option {:?}", long_option_name(.0))]
325    AmbiguousLongOption(Field, Vec<&'a OptionSpec<'a>>),
326
327    /// Option missing its required argument
328    #[error("option {:?} missing an argument", .0.value)]
329    MissingOptionArgument(Field, &'a OptionSpec<'a>),
330
331    /// Long option having an unexpected argument
332    #[error("option {:?} with an unexpected argument", .0.value)]
333    UnexpectedOptionArgument(Field, &'a OptionSpec<'a>),
334}
335
336fn long_option_name(field: &Field) -> &str {
337    match field.value.find('=') {
338        None => &field.value,
339        Some(index) => &field.value[..index],
340    }
341}
342
343impl ParseError<'_> {
344    /// Returns a reference to the field in which the error occurred.
345    pub fn field(&self) -> &Field {
346        use ParseError::*;
347        match self {
348            UnknownShortOption(_char, field) => field,
349            UnknownLongOption(field) => field,
350            UnsupportedLongOption(field, _spec) => field,
351            AmbiguousLongOption(field, _specs) => field,
352            MissingOptionArgument(field, _spec) => field,
353            UnexpectedOptionArgument(field, _spec) => field,
354        }
355    }
356
357    /// Converts this error to a [`Report`].
358    #[must_use]
359    pub fn to_report(&self) -> Report<'_> {
360        let field = self.field();
361        let mut report = Report::new();
362        report.r#type = ReportType::Error;
363        report.title = self.to_string().into();
364        report.snippets = Snippet::with_primary_span(&field.origin, field.value.as_str().into());
365        // TODO provide more info about the erroneous option
366        report
367    }
368}
369
370impl<'a> From<&'a ParseError<'a>> for Report<'a> {
371    #[inline]
372    fn from(error: &'a ParseError<'a>) -> Self {
373        error.to_report()
374    }
375}
376
377/// Parses short options in an argument.
378///
379/// This function examines the first field yielded by `arguments` and consumes
380/// it if it contains one or more short options. If the last option requires an
381/// argument and the field does not include one, the following field is consumed
382/// as the argument.
383///
384/// This function returns `Ok(true)` if consumed one or more fields.
385fn parse_short_options<'a, I: Iterator<Item = Field>>(
386    option_specs: &'a [OptionSpec<'a>],
387    arguments: &mut Peekable<I>,
388    option_occurrences: &mut Vec<OptionOccurrence<'a>>,
389) -> Result<bool, ParseError<'a>> {
390    fn starts_with_single_hyphen(field: &Field) -> bool {
391        let mut chars = field.value.chars();
392        chars.next() == Some('-') && !matches!(chars.next(), None | Some('-'))
393    }
394
395    let field = match arguments.next_if(starts_with_single_hyphen) {
396        None => return Ok(false),
397        Some(field) => field,
398    };
399
400    let mut chars = field.value.chars();
401    chars.next(); // Skip the initial hyphen
402
403    while let Some(c) = chars.next() {
404        let spec = match option_specs.iter().find(|spec| spec.get_short() == Some(c)) {
405            None => return Err(ParseError::UnknownShortOption(c, field)),
406            Some(spec) => spec,
407        };
408        match spec.get_argument() {
409            OptionArgumentSpec::None => {
410                option_occurrences.push(OptionOccurrence {
411                    spec,
412                    location: field.origin.clone(),
413                    argument: None,
414                });
415            }
416            OptionArgumentSpec::Required => {
417                let remainder_len = chars.as_str().len();
418                let location = field.origin.clone();
419                let argument = if remainder_len == 0 {
420                    // The option argument is the next command-line argument.
421                    arguments
422                        .next()
423                        .ok_or(ParseError::MissingOptionArgument(field, spec))?
424                } else {
425                    // The option argument is the rest of the current command-line argument.
426                    let prefix = field.value.len() - remainder_len;
427                    let mut field = field;
428                    field.value.drain(..prefix);
429                    field
430                };
431                option_occurrences.push(OptionOccurrence {
432                    spec,
433                    location,
434                    argument: Some(argument),
435                });
436                break;
437            }
438        };
439    }
440    Ok(true)
441}
442
443/// Finds an option spec that matches the given long option name.
444///
445/// Returns `Err(all_matched_options)` if there is no match or more than one match.
446fn long_match<'a>(
447    option_specs: &'a [OptionSpec<'a>],
448    name: &str,
449) -> Result<&'a OptionSpec<'a>, Vec<&'a OptionSpec<'a>>> {
450    let mut matches = Vec::new();
451    for spec in option_specs {
452        match spec.long_match(name) {
453            LongMatch::None => (),
454            LongMatch::Partial => {
455                matches.push(spec);
456            }
457            LongMatch::Exact => return Ok(spec),
458        }
459    }
460    if matches.len() == 1 {
461        Ok(matches[0])
462    } else {
463        Err(matches)
464    }
465}
466
467/// Parses a long option.
468///
469/// This function examines the first field yielded by `arguments` and consumes
470/// it if it is a long option. If the option requires an argument and the field
471/// does not include a delimiting `=` sign, the following field is consumed as
472/// the argument.
473fn parse_long_option<'a, I: Iterator<Item = Field>>(
474    option_specs: &'a [OptionSpec<'a>],
475    mode: Mode,
476    arguments: &mut Peekable<I>,
477) -> Result<Option<OptionOccurrence<'a>>, ParseError<'a>> {
478    fn starts_with_double_hyphen(field: &Field) -> bool {
479        match field.value.strip_prefix("--") {
480            Some(body) => !body.is_empty(),
481            None => false,
482        }
483    }
484
485    let field = match arguments.next_if(starts_with_double_hyphen) {
486        Some(field) => field,
487        None => return Ok(None),
488    };
489
490    let equal = field.value.find('=');
491
492    let name = match equal {
493        Some(index) => &field.value[2..index],
494        None => &field.value[2..],
495    };
496
497    let spec = match long_match(option_specs, name) {
498        Ok(spec) if mode.accepts_long_options() => spec,
499        Ok(spec) => return Err(ParseError::UnsupportedLongOption(field, spec)),
500        Err(matched_specs) => {
501            return Err(if matched_specs.is_empty() {
502                ParseError::UnknownLongOption(field)
503            } else {
504                ParseError::AmbiguousLongOption(field, matched_specs)
505            });
506        }
507    };
508
509    let location = field.origin.clone();
510
511    let argument = match (spec.get_argument(), equal) {
512        (OptionArgumentSpec::None, None) => None,
513        (OptionArgumentSpec::None, Some(_)) => {
514            return Err(ParseError::UnexpectedOptionArgument(field, spec));
515        }
516        (OptionArgumentSpec::Required, None) => {
517            let argument = arguments.next();
518            if argument.is_none() {
519                return Err(ParseError::MissingOptionArgument(field, spec));
520            }
521            argument
522        }
523        (OptionArgumentSpec::Required, Some(index)) => {
524            let mut field = field;
525            field.value.drain(..index + 1); // Remove "--", name, and "="
526            Some(field)
527        }
528    };
529
530    Ok(Some(OptionOccurrence {
531        spec,
532        location,
533        argument,
534    }))
535}
536
537/// Parses command-line arguments into options and operands.
538///
539/// The arguments should not include a leading command name field.
540///
541/// If successful, returns a pair of option occurrences and operands.
542pub fn parse_arguments<'a>(
543    option_specs: &'a [OptionSpec<'a>],
544    mode: Mode,
545    arguments: Vec<Field>,
546) -> Result<(Vec<OptionOccurrence<'a>>, Vec<Field>), ParseError<'a>> {
547    let mut arguments = arguments.into_iter().peekable();
548
549    let mut option_occurrences = vec![];
550    loop {
551        if parse_short_options(option_specs, &mut arguments, &mut option_occurrences)? {
552            continue;
553        }
554        if let Some(occurrence) = parse_long_option(option_specs, mode, &mut arguments)? {
555            option_occurrences.push(occurrence);
556            continue;
557        }
558        break;
559    }
560
561    arguments.next_if(|argument| argument.value == "--");
562
563    let operands = arguments.collect();
564    Ok((option_occurrences, operands))
565}
566
567/// Error indicating that two or more options conflict with each other
568///
569/// This is a helper object for constructing an error message from a list of
570/// conflicting option occurrences. An instance of this type can be created
571/// using [`new`](Self::new) or [`pick_from_indexes`](Self::pick_from_indexes)
572/// and printed with [`report_error`](crate::common::report::report_error).
573#[derive(Clone, Debug, Eq, Error, PartialEq)]
574#[error("conflicting options")]
575pub struct ConflictingOptionError<'a> {
576    options: Vec<OptionOccurrence<'a>>,
577}
578
579impl<'a> ConflictingOptionError<'a> {
580    /// Creates a new `ConflictingOptionError` from a list of conflicting options.
581    ///
582    /// The vector should contain at least two elements, or the returned error
583    /// object may panic when formatted.
584    #[must_use]
585    pub fn new<T: Into<Vec<OptionOccurrence<'a>>>>(options: T) -> Self {
586        let options = options.into();
587        Self { options }
588    }
589
590    /// Creates a new `ConflictingOptionError` with conflicting options
591    /// extracted from a vector.
592    ///
593    /// This function retains only the options in the vector whose indexes are
594    /// specified in `indexes`. The other options are discarded.
595    ///
596    /// The `indexes` may be specified in any order as they are sorted in this
597    /// function.
598    ///
599    /// `indexes` should contain at least two elements, or the returned error
600    /// object may panic when formatted. This function panics immediately if
601    /// `indexes` contains a duplicate index.
602    ///
603    /// This function is useful for constructing a `ConflictingOptionError` from
604    /// the result of [`parse_arguments`].
605    /// After examining the `OptionOccurrence` vector returned by the function,
606    /// the caller can pick the indexes of the conflicting options and pass them
607    /// to this function.
608    ///
609    /// For example, calling `ConflictingOptionError::pick_from_indexes(vec![a,
610    /// b, c, d, e], [3, 0])` is equivalent to `ConflictingOptionError::new([a,
611    /// d])`.
612    #[must_use]
613    pub fn pick_from_indexes<const N: usize>(
614        mut options: Vec<OptionOccurrence<'a>>,
615        mut indexes: [usize; N],
616    ) -> Self {
617        indexes.sort();
618
619        // Remove the options that are not picked.
620        let mut option_index = 0;
621        let mut index_index = 0;
622        options.retain(|_| {
623            if index_index >= N {
624                return false;
625            }
626            assert!(
627                option_index <= indexes[index_index],
628                "duplicate index {}",
629                indexes[index_index]
630            );
631            let pick = option_index == indexes[index_index];
632            option_index += 1;
633            if pick {
634                index_index += 1;
635            }
636            pick
637        });
638
639        Self { options }
640    }
641
642    /// Returns the list of conflicting options.
643    #[must_use]
644    pub fn options(&self) -> &[OptionOccurrence<'a>] {
645        &self.options
646    }
647
648    /// Converts this error into a report.
649    #[must_use]
650    pub fn to_report(&'a self) -> Report<'a> {
651        let mut report = Report::new();
652        report.r#type = ReportType::Error;
653        report.title = self.to_string().into();
654        report.snippets = Snippet::with_primary_span(
655            &self.options[0].location,
656            format!("the {} option ...", &self.options[0].spec).into(),
657        );
658        for option in &self.options[1..] {
659            let span = Span {
660                range: option.location.byte_range(),
661                role: SpanRole::Primary {
662                    label: format!("... cannot be used with the {} option", &option.spec).into(),
663                },
664            };
665            add_span(&option.location.code, span, &mut report.snippets);
666        }
667        report
668    }
669}
670
671impl<'a> From<Vec<OptionOccurrence<'a>>> for ConflictingOptionError<'a> {
672    /// Creates a new `ConflictingOptionError` from a list of conflicting options.
673    ///
674    /// The vector should contain at least two elements, or the returned error
675    /// object may panic when formatted.
676    fn from(options: Vec<OptionOccurrence<'a>>) -> Self {
677        ConflictingOptionError { options }
678    }
679}
680
681impl<'a> From<ConflictingOptionError<'a>> for Vec<OptionOccurrence<'a>> {
682    fn from(error: ConflictingOptionError<'a>) -> Self {
683        error.options
684    }
685}
686
687impl<'a> From<&'a ConflictingOptionError<'a>> for Report<'a> {
688    #[inline]
689    fn from(error: &'a ConflictingOptionError<'a>) -> Self {
690        error.to_report()
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use assert_matches::assert_matches;
698
699    #[test]
700    fn empty_arguments() {
701        let (options, operands) = parse_arguments(&[], Mode::default(), vec![]).unwrap();
702        assert_eq!(options, []);
703        assert_eq!(operands, []);
704    }
705
706    #[test]
707    fn only_operands() {
708        let arguments = Field::dummies([""]);
709        let (options, operands) = parse_arguments(&[], Mode::default(), arguments).unwrap();
710        assert_eq!(options, []);
711        assert_eq!(operands, Field::dummies([""]));
712
713        let arguments = Field::dummies(["foo", "bar", "", "baz"]);
714        let (options, operands) = parse_arguments(&[], Mode::default(), arguments).unwrap();
715        assert_eq!(options, []);
716        assert_eq!(operands, Field::dummies(["foo", "bar", "", "baz"]));
717    }
718
719    #[test]
720    fn operands_following_separator() {
721        let arguments = Field::dummies(["--"]);
722        let (options, operands) = parse_arguments(&[], Mode::default(), arguments).unwrap();
723        assert_eq!(options, []);
724        assert_eq!(operands, []);
725
726        let arguments = Field::dummies(["--", "1"]);
727        let (options, operands) = parse_arguments(&[], Mode::default(), arguments).unwrap();
728        assert_eq!(options, []);
729        assert_eq!(operands, Field::dummies(["1"]));
730
731        let arguments = Field::dummies(["--", "a", "", "z"]);
732        let (options, operands) = parse_arguments(&[], Mode::default(), arguments).unwrap();
733        assert_eq!(options, []);
734        assert_eq!(operands, Field::dummies(["a", "", "z"]));
735    }
736
737    #[test]
738    fn non_occurring_short_option() {
739        let specs = &[OptionSpec::new().short('a')];
740
741        let arguments = vec![];
742        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
743        assert_eq!(options, []);
744        assert_eq!(operands, []);
745
746        let arguments = Field::dummies([""]);
747        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
748        assert_eq!(options, []);
749        assert_eq!(operands, Field::dummies([""]));
750    }
751
752    #[test]
753    fn single_occurrence_of_short_option() {
754        let specs = &[OptionSpec::new().short('a')];
755
756        let arguments = Field::dummies(["-a"]);
757        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
758        assert_eq!(options.len(), 1, "options = {options:?}");
759        assert_eq!(options[0].spec.get_short(), Some('a'));
760        assert_eq!(options[0].location, Location::dummy("-a"));
761        assert_eq!(options[0].argument, None);
762        assert_eq!(operands, []);
763
764        let arguments = Field::dummies(["-a", "foo"]);
765        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
766        assert_eq!(options.len(), 1, "options = {options:?}");
767        assert_eq!(options[0].spec.get_short(), Some('a'));
768        assert_eq!(options[0].location, Location::dummy("-a"));
769        assert_eq!(options[0].argument, None);
770        assert_eq!(operands, Field::dummies(["foo"]));
771    }
772
773    #[test]
774    fn multiple_occurrences_of_same_option_spec_short() {
775        let specs = &[OptionSpec::new().short('b')];
776
777        let arguments = Field::dummies(["-b", "-b"]);
778        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
779        assert_eq!(options.len(), 2, "options = {options:?}");
780        assert_eq!(options[0].spec.get_short(), Some('b'));
781        assert_eq!(options[1].spec.get_short(), Some('b'));
782        assert_eq!(operands, []);
783
784        let arguments = Field::dummies(["-b", "-b", "argument"]);
785        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
786        assert_eq!(options.len(), 2, "options = {options:?}");
787        assert_eq!(options[0].spec.get_short(), Some('b'));
788        assert_eq!(options[1].spec.get_short(), Some('b'));
789        assert_eq!(operands, Field::dummies(["argument"]));
790    }
791
792    #[test]
793    fn occurrences_of_multiple_option_specs_short() {
794        let specs = &[OptionSpec::new().short('x'), OptionSpec::new().short('y')];
795
796        let arguments = Field::dummies(["-x", "-y", "!"]);
797        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
798        assert_eq!(options.len(), 2, "options = {options:?}");
799        assert_eq!(options[0].spec.get_short(), Some('x'));
800        assert_eq!(options[1].spec.get_short(), Some('y'));
801        assert_eq!(operands, Field::dummies(["!"]));
802
803        let arguments = Field::dummies(["-y", "-x", "-y"]);
804        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
805        assert_eq!(options.len(), 3, "options = {options:?}");
806        assert_eq!(options[0].spec.get_short(), Some('y'));
807        assert_eq!(options[1].spec.get_short(), Some('x'));
808        assert_eq!(options[2].spec.get_short(), Some('y'));
809        assert_eq!(operands, []);
810    }
811
812    #[test]
813    fn multiple_occurrences_of_short_options_in_single_argument() {
814        let specs = &[OptionSpec::new().short('p'), OptionSpec::new().short('q')];
815
816        let arguments = Field::dummies(["-pq", "!"]);
817        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
818        assert_eq!(options.len(), 2, "options = {options:?}");
819        assert_eq!(options[0].spec.get_short(), Some('p'));
820        assert_eq!(options[0].location, Location::dummy("-pq"));
821        assert_eq!(options[1].spec.get_short(), Some('q'));
822        assert_eq!(options[1].location, Location::dummy("-pq"));
823        assert_eq!(operands, Field::dummies(["!"]));
824
825        let arguments = Field::dummies(["-qpq"]);
826        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
827        assert_eq!(options.len(), 3, "options = {options:?}");
828        assert_eq!(options[0].spec.get_short(), Some('q'));
829        assert_eq!(options[1].spec.get_short(), Some('p'));
830        assert_eq!(options[2].spec.get_short(), Some('q'));
831        assert_eq!(operands, []);
832    }
833
834    #[test]
835    fn single_hyphen_argument_is_not_option() {
836        let specs = &[OptionSpec::new().short('a')];
837
838        let arguments = Field::dummies(["-"]);
839        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
840        assert_eq!(options, []);
841        assert_eq!(operands, Field::dummies(["-"]));
842
843        let arguments = Field::dummies(["-", "-"]);
844        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
845        assert_eq!(options, []);
846        assert_eq!(operands, Field::dummies(["-", "-"]));
847    }
848
849    #[test]
850    fn options_are_not_recognized_after_separator() {
851        let specs = &[OptionSpec::new().short('a')];
852
853        let arguments = Field::dummies(["-a", "--", "-a", "--", "-a"]);
854        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
855        assert_eq!(options.len(), 1, "options = {options:?}");
856        assert_eq!(options[0].spec.get_short(), Some('a'));
857        assert_eq!(operands, Field::dummies(["-a", "--", "-a"]));
858    }
859
860    #[test]
861    fn options_are_not_recognized_after_operand_by_default() {
862        let specs = &[OptionSpec::new().short('x'), OptionSpec::new().short('y')];
863
864        let arguments = Field::dummies(["-x", "foo", "-y", "bar"]);
865        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
866        assert_eq!(options.len(), 1, "options = {options:?}");
867        assert_eq!(options[0].spec.get_short(), Some('x'));
868        assert_eq!(operands, Field::dummies(["foo", "-y", "bar"]));
869    }
870
871    #[test]
872    fn adjacent_argument_to_short_option() {
873        let specs = &[OptionSpec::new()
874            .short('a')
875            .argument(OptionArgumentSpec::Required)];
876
877        let arguments = Field::dummies(["-afoo"]);
878        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
879        assert_eq!(options.len(), 1, "options = {options:?}");
880        assert_eq!(options[0].spec.get_short(), Some('a'));
881        assert_eq!(options[0].location, Location::dummy("-afoo"));
882        assert_matches!(options[0].argument, Some(ref field) => {
883            assert_eq!(field.value, "foo");
884            assert_eq!(field.origin, Location::dummy("-afoo"));
885        });
886        assert_eq!(operands, []);
887
888        let arguments = Field::dummies(["-a1", "-a2", "3"]);
889        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
890        assert_eq!(options.len(), 2, "options = {options:?}");
891        assert_eq!(options[0].spec.get_short(), Some('a'));
892        assert_matches!(options[0].argument, Some(ref field) => {
893            assert_eq!(field.value, "1");
894            assert_eq!(field.origin, Location::dummy("-a1"));
895        });
896        assert_eq!(options[1].spec.get_short(), Some('a'));
897        assert_matches!(options[1].argument, Some(ref field) => {
898            assert_eq!(field.value, "2");
899            assert_eq!(field.origin, Location::dummy("-a2"));
900        });
901        assert_eq!(operands, Field::dummies(["3"]));
902    }
903
904    #[test]
905    fn separate_argument_to_short_option() {
906        let specs = &[OptionSpec::new()
907            .short('a')
908            .argument(OptionArgumentSpec::Required)];
909
910        let arguments = Field::dummies(["-a", "foo"]);
911        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
912        assert_eq!(options.len(), 1, "options = {options:?}");
913        assert_eq!(options[0].spec.get_short(), Some('a'));
914        assert_matches!(options[0].argument, Some(ref field) => {
915            assert_eq!(field.value, "foo");
916            assert_eq!(field.origin, Location::dummy("foo"));
917        });
918        assert_eq!(operands, []);
919
920        let arguments = Field::dummies(["-a", "1", "-a", "2", "3"]);
921        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
922        assert_eq!(options.len(), 2, "options = {options:?}");
923        assert_eq!(options[0].spec.get_short(), Some('a'));
924        assert_matches!(options[0].argument, Some(ref field) => {
925            assert_eq!(field.value, "1");
926            assert_eq!(field.origin, Location::dummy("1"));
927        });
928        assert_eq!(options[1].spec.get_short(), Some('a'));
929        assert_matches!(options[1].argument, Some(ref field) => {
930            assert_eq!(field.value, "2");
931            assert_eq!(field.origin, Location::dummy("2"));
932        });
933        assert_eq!(operands, Field::dummies(["3"]));
934    }
935
936    #[test]
937    fn argument_taking_option_adjacent_to_another_option() {
938        let specs = &[
939            OptionSpec::new()
940                .short('a')
941                .argument(OptionArgumentSpec::None),
942            OptionSpec::new()
943                .short('b')
944                .argument(OptionArgumentSpec::None),
945            OptionSpec::new()
946                .short('c')
947                .argument(OptionArgumentSpec::Required),
948        ];
949
950        let arguments = Field::dummies(["-abcdef"]);
951        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
952        assert_eq!(options.len(), 3, "options = {options:?}");
953        assert_eq!(options[0].spec.get_short(), Some('a'));
954        assert_eq!(options[0].argument, None);
955        assert_eq!(options[1].spec.get_short(), Some('b'));
956        assert_eq!(options[1].argument, None);
957        assert_eq!(options[2].spec.get_short(), Some('c'));
958        assert_matches!(options[2].argument, Some(ref field) => {
959            assert_eq!(field.value, "def");
960            assert_eq!(field.origin, Location::dummy("-abcdef"));
961        });
962        assert_eq!(operands, []);
963    }
964
965    #[test]
966    fn empty_argument_to_short_option() {
967        let specs = &[OptionSpec::new()
968            .short('a')
969            .argument(OptionArgumentSpec::Required)];
970
971        let arguments = Field::dummies(["-a", ""]);
972        let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
973        assert_eq!(options.len(), 1, "options = {options:?}");
974        assert_eq!(options[0].spec.get_short(), Some('a'));
975        assert_matches!(options[0].argument, Some(ref field) => {
976            assert_eq!(field.value, "");
977            assert_eq!(field.origin, Location::dummy(""));
978        });
979        assert_eq!(operands, []);
980    }
981
982    #[test]
983    fn non_occurring_long_option() {
984        let specs = &[OptionSpec::new().long("option")];
985
986        let arguments = vec![];
987        let (options, operands) =
988            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
989        assert_eq!(options, []);
990        assert_eq!(operands, []);
991
992        let arguments = Field::dummies([""]);
993        let (options, operands) =
994            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
995        assert_eq!(options, []);
996        assert_eq!(operands, Field::dummies([""]));
997    }
998
999    #[test]
1000    fn single_occurrence_of_long_option() {
1001        let specs = &[OptionSpec::new().long("option")];
1002
1003        let arguments = Field::dummies(["--option"]);
1004        let (options, operands) =
1005            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1006        assert_eq!(options.len(), 1, "options = {options:?}");
1007        assert_eq!(options[0].spec.get_long(), Some("option"));
1008        assert_eq!(options[0].location, Location::dummy("--option"));
1009        assert_eq!(options[0].argument, None);
1010        assert_eq!(operands, []);
1011
1012        let arguments = Field::dummies(["--option", "foo"]);
1013        let (options, operands) =
1014            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1015        assert_eq!(options.len(), 1, "options = {options:?}");
1016        assert_eq!(options[0].spec.get_long(), Some("option"));
1017        assert_eq!(options[0].location, Location::dummy("--option"));
1018        assert_eq!(options[0].argument, None);
1019        assert_eq!(operands, Field::dummies(["foo"]));
1020    }
1021
1022    #[test]
1023    fn multiple_occurrences_of_same_option_spec_long() {
1024        let specs = &[OptionSpec::new().long("foo")];
1025
1026        let arguments = Field::dummies(["--foo", "--foo"]);
1027        let (options, operands) =
1028            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1029        assert_eq!(options.len(), 2, "options = {options:?}");
1030        assert_eq!(options[0].spec.get_long(), Some("foo"));
1031        assert_eq!(options[1].spec.get_long(), Some("foo"));
1032        assert_eq!(operands, []);
1033
1034        let arguments = Field::dummies(["--foo", "--foo", "argument"]);
1035        let (options, operands) =
1036            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1037        assert_eq!(options.len(), 2, "options = {options:?}");
1038        assert_eq!(options[0].spec.get_long(), Some("foo"));
1039        assert_eq!(options[1].spec.get_long(), Some("foo"));
1040        assert_eq!(operands, Field::dummies(["argument"]));
1041    }
1042
1043    #[test]
1044    fn occurrences_of_multiple_option_specs_long() {
1045        let specs = &[OptionSpec::new().long("foo"), OptionSpec::new().long("bar")];
1046
1047        let arguments = Field::dummies(["--foo", "--bar", "!"]);
1048        let (options, operands) =
1049            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1050        assert_eq!(options.len(), 2, "options = {options:?}");
1051        assert_eq!(options[0].spec.get_long(), Some("foo"));
1052        assert_eq!(options[1].spec.get_long(), Some("bar"));
1053        assert_eq!(operands, Field::dummies(["!"]));
1054
1055        let arguments = Field::dummies(["--bar", "--foo", "--bar"]);
1056        let (options, operands) =
1057            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1058        assert_eq!(options.len(), 3, "options = {options:?}");
1059        assert_eq!(options[0].spec.get_long(), Some("bar"));
1060        assert_eq!(options[1].spec.get_long(), Some("foo"));
1061        assert_eq!(options[2].spec.get_long(), Some("bar"));
1062        assert_eq!(operands, []);
1063    }
1064
1065    #[test]
1066    fn abbreviated_long_option_without_non_match() {
1067        let specs = &[OptionSpec::new().long("min")];
1068
1069        let arguments = Field::dummies(["--mi"]);
1070        let (options, operands) =
1071            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1072        assert_eq!(options.len(), 1, "options = {options:?}");
1073        assert_eq!(options[0].spec.get_long(), Some("min"));
1074        assert_eq!(operands, []);
1075    }
1076
1077    #[test]
1078    fn abbreviated_long_option_with_non_match() {
1079        let specs = &[OptionSpec::new().long("max"), OptionSpec::new().long("min")];
1080
1081        let arguments = Field::dummies(["--mi"]);
1082        let (options, operands) =
1083            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1084        assert_eq!(options.len(), 1, "options = {options:?}");
1085        assert_eq!(options[0].spec.get_long(), Some("min"));
1086        assert_eq!(operands, []);
1087    }
1088
1089    #[test]
1090    fn long_option_prefers_exact_match() {
1091        let specs = &[
1092            OptionSpec::new().long("many"),
1093            OptionSpec::new().long("man"),
1094            OptionSpec::new().long("manual"),
1095        ];
1096
1097        let arguments = Field::dummies(["--man"]);
1098        let (options, operands) =
1099            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1100        assert_eq!(options.len(), 1, "options = {options:?}");
1101        assert_eq!(options[0].spec.get_long(), Some("man"));
1102        assert_eq!(operands, []);
1103    }
1104
1105    #[test]
1106    fn adjacent_argument_to_long_option() {
1107        let specs = &[OptionSpec::new()
1108            .long("option")
1109            .argument(OptionArgumentSpec::Required)];
1110
1111        let arguments = Field::dummies(["--option="]);
1112        let (options, operands) =
1113            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1114        assert_eq!(options.len(), 1, "options = {options:?}");
1115        assert_eq!(options[0].spec.get_long(), Some("option"));
1116        assert_eq!(options[0].location, Location::dummy("--option="));
1117        assert_matches!(options[0].argument, Some(ref field) => {
1118            assert_eq!(field.value, "");
1119            assert_eq!(field.origin, Location::dummy("--option="));
1120        });
1121        assert_eq!(operands, []);
1122
1123        let arguments = Field::dummies(["--option=x", "--option=value", "argument"]);
1124        let (options, operands) =
1125            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1126        assert_eq!(options.len(), 2, "options = {options:?}");
1127        assert_eq!(options[0].spec.get_long(), Some("option"));
1128        assert_eq!(options[0].location, Location::dummy("--option=x"));
1129        assert_matches!(options[0].argument, Some(ref field) => {
1130            assert_eq!(field.value, "x");
1131            assert_eq!(field.origin, Location::dummy("--option=x"));
1132        });
1133        assert_eq!(options[1].spec.get_long(), Some("option"));
1134        assert_eq!(options[1].location, Location::dummy("--option=value"));
1135        assert_matches!(options[1].argument, Some(ref field) => {
1136            assert_eq!(field.value, "value");
1137            assert_eq!(field.origin, Location::dummy("--option=value"));
1138        });
1139        assert_eq!(operands, Field::dummies(["argument"]));
1140    }
1141
1142    #[test]
1143    fn separate_argument_to_long_option() {
1144        let specs = &[OptionSpec::new()
1145            .long("option")
1146            .argument(OptionArgumentSpec::Required)];
1147
1148        let arguments = Field::dummies(["--option", ""]);
1149        let (options, operands) =
1150            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1151        assert_eq!(options.len(), 1, "options = {options:?}");
1152        assert_eq!(options[0].spec.get_long(), Some("option"));
1153        assert_eq!(options[0].location, Location::dummy("--option"));
1154        assert_matches!(options[0].argument, Some(ref field) => {
1155            assert_eq!(field.value, "");
1156            assert_eq!(field.origin, Location::dummy(""));
1157        });
1158        assert_eq!(operands, []);
1159
1160        let arguments = Field::dummies(["--option", "x", "--option", "value", "argument"]);
1161        let (options, operands) =
1162            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1163        assert_eq!(options.len(), 2, "options = {options:?}");
1164        assert_eq!(options[0].spec.get_long(), Some("option"));
1165        assert_eq!(options[0].location, Location::dummy("--option"));
1166        assert_matches!(options[0].argument, Some(ref field) => {
1167            assert_eq!(field.value, "x");
1168            assert_eq!(field.origin, Location::dummy("x"));
1169        });
1170        assert_eq!(options[1].spec.get_long(), Some("option"));
1171        assert_eq!(options[1].location, Location::dummy("--option"));
1172        assert_matches!(options[1].argument, Some(ref field) => {
1173            assert_eq!(field.value, "value");
1174            assert_eq!(field.origin, Location::dummy("value"));
1175        });
1176        assert_eq!(operands, Field::dummies(["argument"]));
1177    }
1178
1179    #[test]
1180    fn option_argument_that_looks_like_separator() {
1181        let specs = &[OptionSpec::new()
1182            .short('a')
1183            .argument(OptionArgumentSpec::Required)];
1184
1185        let arguments = Field::dummies(["-a", "argument", "-a", "--", "--", "operand"]);
1186        let (options, operands) =
1187            parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
1188        assert_eq!(options.len(), 2, "options = {options:?}");
1189        assert_eq!(options[0].spec.get_short(), Some('a'));
1190        assert_matches!(options[0].argument, Some(ref field) => {
1191            assert_eq!(field.value, "argument");
1192            assert_eq!(field.origin, Location::dummy("argument"));
1193        });
1194        assert_eq!(options[1].spec.get_short(), Some('a'));
1195        assert_matches!(options[1].argument, Some(ref field) => {
1196            assert_eq!(field.value, "--");
1197            assert_eq!(field.origin, Location::dummy("--"));
1198        });
1199        assert_eq!(operands, Field::dummies(["operand"]));
1200    }
1201
1202    // TODO options_are_recognized_after_operand (depending mode)
1203    // TODO digit_options_are_recognized (depending mode)
1204    // TODO rejecting_non_portable_options (depending mode)
1205
1206    #[test]
1207    fn unknown_short_option() {
1208        let specs = &[OptionSpec::new().short('a')];
1209
1210        let arguments = Field::dummies(["-x"]);
1211        let error = parse_arguments(&[], Mode::default(), arguments).unwrap_err();
1212        assert_matches!(&error, ParseError::UnknownShortOption('x', field) => {
1213            assert_eq!(field.value, "-x");
1214        });
1215        assert_eq!(error.to_string(), "unknown option 'x'");
1216
1217        let arguments = Field::dummies(["-x"]);
1218        let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
1219        assert_matches!(&error, ParseError::UnknownShortOption('x', field) => {
1220            assert_eq!(field.value, "-x");
1221        });
1222        assert_eq!(error.to_string(), "unknown option 'x'");
1223    }
1224
1225    #[test]
1226    fn unknown_long_option() {
1227        let specs = &[OptionSpec::new().long("one")];
1228
1229        let arguments = Field::dummies(["--two"]);
1230        let error = parse_arguments(&[], Mode::with_extensions(), arguments).unwrap_err();
1231        assert_matches!(&error, ParseError::UnknownLongOption(field) => {
1232            assert_eq!(field.value, "--two");
1233        });
1234        assert_eq!(error.to_string(), "unknown option \"--two\"");
1235
1236        let arguments = Field::dummies(["--two=three"]);
1237        let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
1238        assert_matches!(&error, ParseError::UnknownLongOption(field) => {
1239            assert_eq!(field.value, "--two=three");
1240        });
1241        assert_eq!(error.to_string(), "unknown option \"--two\"");
1242    }
1243
1244    #[test]
1245    fn disabled_long_option() {
1246        let specs = &[OptionSpec::new().long("option")];
1247
1248        let mode = *Mode::with_extensions().accept_long_options(false);
1249        let arguments = Field::dummies(["--option"]);
1250        let error = parse_arguments(specs, mode, arguments).unwrap_err();
1251        assert_matches!(&error, &ParseError::UnsupportedLongOption(ref field, spec) => {
1252            assert_eq!(field.value, "--option");
1253            assert_eq!(spec, &specs[0]);
1254        });
1255        assert_eq!(error.to_string(), "unsupported option \"--option\"");
1256    }
1257
1258    #[test]
1259    fn ambiguous_long_option() {
1260        let specs = &[
1261            OptionSpec::new().long("max"),
1262            OptionSpec::new().long("min"),
1263            OptionSpec::new().long("value"),
1264        ];
1265
1266        let arguments = Field::dummies(["--m"]);
1267        let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
1268        assert_matches!(&error, ParseError::AmbiguousLongOption(field, matched_specs) => {
1269            assert_eq!(field.value, "--m");
1270            assert_eq!(matched_specs.as_slice(), [&specs[0], &specs[1]]);
1271        });
1272        assert_eq!(error.to_string(), "ambiguous option \"--m\"");
1273    }
1274
1275    #[test]
1276    fn missing_argument_to_short_option() {
1277        use OptionArgumentSpec::Required;
1278        let specs = &[
1279            OptionSpec::new().short('a').argument(Required),
1280            OptionSpec::new().short('b'),
1281        ];
1282
1283        let arguments = Field::dummies(["-a"]);
1284        let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
1285        assert_matches!(&error, &ParseError::MissingOptionArgument(ref field, spec) => {
1286            assert_eq!(field.value, "-a");
1287            assert_eq!(spec, &specs[0]);
1288        });
1289        assert_eq!(error.to_string(), "option \"-a\" missing an argument");
1290
1291        let arguments = Field::dummies(["-ba"]);
1292        let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
1293        assert_matches!(&error, &ParseError::MissingOptionArgument(ref field, spec) => {
1294            assert_eq!(field.value, "-ba");
1295            assert_eq!(spec, &specs[0]);
1296        });
1297        assert_eq!(error.to_string(), "option \"-ba\" missing an argument");
1298    }
1299
1300    #[test]
1301    fn missing_argument_to_long_option() {
1302        use OptionArgumentSpec::Required;
1303        let specs = &[
1304            OptionSpec::new().long("foo").argument(Required),
1305            OptionSpec::new().long("bar"),
1306        ];
1307
1308        let arguments = Field::dummies(["--fo"]);
1309        let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
1310        assert_matches!(&error, &ParseError::MissingOptionArgument(ref field, spec) => {
1311            assert_eq!(field.value, "--fo");
1312            assert_eq!(spec, &specs[0]);
1313        });
1314        assert_eq!(error.to_string(), "option \"--fo\" missing an argument");
1315    }
1316
1317    #[test]
1318    fn unexpected_argument_to_long_option() {
1319        use OptionArgumentSpec::Required;
1320        let specs = &[
1321            OptionSpec::new().long("foo").argument(Required),
1322            OptionSpec::new().long("bar"),
1323        ];
1324
1325        let arguments = Field::dummies(["--bar=baz"]);
1326        let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
1327        assert_matches!(&error, &ParseError::UnexpectedOptionArgument(ref field, spec) => {
1328            assert_eq!(field.value, "--bar=baz");
1329            assert_eq!(spec, &specs[1]);
1330        });
1331        assert_eq!(
1332            error.to_string(),
1333            "option \"--bar=baz\" with an unexpected argument"
1334        );
1335    }
1336
1337    const OPTION_SPEC_A: OptionSpec = OptionSpec::new().short('a');
1338    const OPTION_SPEC_B: OptionSpec = OptionSpec::new().short('b');
1339    const OPTION_SPEC_C: OptionSpec = OptionSpec::new().short('c');
1340    const OPTION_SPEC_D: OptionSpec = OptionSpec::new().short('d');
1341    const OPTION_SPEC_E: OptionSpec = OptionSpec::new().short('e');
1342
1343    fn dummy_options() -> Vec<OptionOccurrence<'static>> {
1344        vec![
1345            OptionOccurrence {
1346                spec: &OPTION_SPEC_A,
1347                location: Location::dummy("-a"),
1348                argument: None,
1349            },
1350            OptionOccurrence {
1351                spec: &OPTION_SPEC_B,
1352                location: Location::dummy("-b"),
1353                argument: None,
1354            },
1355            OptionOccurrence {
1356                spec: &OPTION_SPEC_C,
1357                location: Location::dummy("-c"),
1358                argument: None,
1359            },
1360            OptionOccurrence {
1361                spec: &OPTION_SPEC_D,
1362                location: Location::dummy("-d"),
1363                argument: None,
1364            },
1365            OptionOccurrence {
1366                spec: &OPTION_SPEC_E,
1367                location: Location::dummy("-e"),
1368                argument: None,
1369            },
1370        ]
1371    }
1372
1373    #[test]
1374    fn pick_from_2_indexes() {
1375        let result = ConflictingOptionError::pick_from_indexes(dummy_options(), [1, 3]);
1376        let options = Vec::from(result);
1377        assert_matches!(options.as_slice(), [b, d] => {
1378            assert_eq!(b.spec, &OPTION_SPEC_B);
1379            assert_eq!(d.spec, &OPTION_SPEC_D);
1380        });
1381    }
1382
1383    #[test]
1384    fn pick_from_2_indexes_reversed() {
1385        let result = ConflictingOptionError::pick_from_indexes(dummy_options(), [3, 1]);
1386        let options = Vec::from(result);
1387        assert_matches!(options.as_slice(), [b, d] => {
1388            assert_eq!(b.spec, &OPTION_SPEC_B);
1389            assert_eq!(d.spec, &OPTION_SPEC_D);
1390        });
1391    }
1392
1393    #[test]
1394    fn pick_from_3_indexes() {
1395        let result = ConflictingOptionError::pick_from_indexes(dummy_options(), [0, 2, 4]);
1396        let options = Vec::from(result);
1397        assert_matches!(options.as_slice(), [a, c, e] => {
1398            assert_eq!(a.spec, &OPTION_SPEC_A);
1399            assert_eq!(c.spec, &OPTION_SPEC_C);
1400            assert_eq!(e.spec, &OPTION_SPEC_E);
1401        });
1402    }
1403
1404    #[test]
1405    fn pick_from_4_indexes_shuffled() {
1406        let result = ConflictingOptionError::pick_from_indexes(dummy_options(), [3, 0, 4, 2]);
1407        let options = Vec::from(result);
1408        assert_matches!(options.as_slice(), [a, c, d, e] => {
1409            assert_eq!(a.spec, &OPTION_SPEC_A);
1410            assert_eq!(c.spec, &OPTION_SPEC_C);
1411            assert_eq!(d.spec, &OPTION_SPEC_D);
1412            assert_eq!(e.spec, &OPTION_SPEC_E);
1413        });
1414    }
1415
1416    #[test]
1417    #[should_panic(expected = "duplicate index 1")]
1418    fn pick_from_duplicate_indexes() {
1419        let result = ConflictingOptionError::pick_from_indexes(dummy_options(), [1, 1]);
1420        unreachable!("{result:?}");
1421    }
1422}