Skip to main content

yash_env/
option.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//! Type definitions for shell options
18//!
19//! This module defines the [`OptionSet`] struct, a map from [`Option`] to
20//! [`State`]. The option set represents whether each option is on or off.
21//!
22//! Note that `OptionSet` merely manages the state of options. It is not the
23//! responsibility of `OptionSet` to change the behavior of the shell according
24//! to the options.
25
26use enumset::EnumSet;
27use enumset::EnumSetIter;
28use enumset::EnumSetType;
29use std::borrow::Cow;
30use std::fmt::Display;
31use std::fmt::Formatter;
32use std::ops::Not;
33use std::str::FromStr;
34use thiserror::Error;
35
36/// State of an option: either enabled or disabled.
37#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
38pub enum State {
39    /// Enabled.
40    On,
41    /// Disabled.
42    Off,
43}
44
45pub use State::*;
46
47impl State {
48    /// Returns a string describing the state (`"on"` or `"off"`).
49    #[must_use]
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            On => "on",
53            Off => "off",
54        }
55    }
56}
57
58/// Converts a state to a string (`on` or `off`).
59impl Display for State {
60    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
61        self.as_str().fmt(f)
62    }
63}
64
65impl Not for State {
66    type Output = Self;
67    fn not(self) -> Self {
68        match self {
69            On => Off,
70            Off => On,
71        }
72    }
73}
74
75/// Converts a Boolean to a state
76impl From<bool> for State {
77    fn from(is_on: bool) -> Self {
78        if is_on { On } else { Off }
79    }
80}
81
82/// Converts a state to a Boolean
83impl From<State> for bool {
84    fn from(state: State) -> Self {
85        match state {
86            On => true,
87            Off => false,
88        }
89    }
90}
91
92/// Shell option
93#[derive(Clone, Copy, Debug, EnumSetType, Eq, Hash, PartialEq)]
94#[enumset(no_super_impls)]
95#[non_exhaustive]
96pub enum Option {
97    /// Makes all variables exported when they are assigned.
98    AllExport,
99    /// Allows overwriting and truncating an existing file with the `>`
100    /// redirection.
101    Clobber,
102    /// Executes a command string specified as a command line argument.
103    CmdLine,
104    /// Makes the shell to exit when a command returns a non-zero exit status.
105    ErrExit,
106    /// Makes the shell to actually run commands.
107    Exec,
108    /// Enables pathname expansion.
109    Glob,
110    /// Performs command search for each command in a function on its
111    /// definition.
112    HashOnDefinition,
113    /// Prevents the interactive shell from exiting when the user enters an
114    /// end-of-file.
115    IgnoreEof,
116    /// Enables features for interactive use.
117    Interactive,
118    /// Allows function definition commands to be recorded in the command
119    /// history.
120    Log,
121    /// Sources the profile file on startup.
122    Login,
123    /// Enables job control.
124    Monitor,
125    /// Automatically reports the results of asynchronous jobs.
126    Notify,
127    /// Makes a pipeline reflect the exit status of the last failed component.
128    PipeFail,
129    /// Disables non-portable features.
130    Portable,
131    /// Disables POSIX-incompatible features.
132    PosixlyCorrect,
133    /// Reads commands from the standard input.
134    Stdin,
135    /// Expands unset variables to an empty string rather than erroring out.
136    Unset,
137    /// Echos the input before parsing and executing.
138    Verbose,
139    /// Enables vi-like command line editing.
140    Vi,
141    /// Prints expanded words during command execution.
142    XTrace,
143}
144
145pub use self::Option::*;
146
147impl Option {
148    /// Whether this option can be modified by the set built-in.
149    ///
150    /// Unmodifiable options can be set only on shell startup.
151    #[must_use]
152    pub const fn is_modifiable(self) -> bool {
153        !matches!(self, CmdLine | Interactive | Stdin)
154    }
155
156    /// Returns the single-character option name.
157    ///
158    /// This function returns a short name for the option and the state rendered
159    /// by the name.
160    /// The name can be converted back to `Option` with [`parse_short`].
161    /// Note that the result is `None` for options that do not have a short
162    /// name.
163    #[must_use]
164    pub const fn short_name(self) -> std::option::Option<(char, State)> {
165        match self {
166            AllExport => Some(('a', On)),
167            Clobber => Some(('C', Off)),
168            CmdLine => Some(('c', On)),
169            ErrExit => Some(('e', On)),
170            Exec => Some(('n', Off)),
171            Glob => Some(('f', Off)),
172            HashOnDefinition => Some(('h', On)),
173            IgnoreEof => None,
174            Interactive => Some(('i', On)),
175            Log => None,
176            Login => Some(('l', On)),
177            Monitor => Some(('m', On)),
178            Notify => Some(('b', On)),
179            PipeFail => None,
180            Portable => None,
181            PosixlyCorrect => None,
182            Stdin => Some(('s', On)),
183            Unset => Some(('u', Off)),
184            Verbose => Some(('v', On)),
185            Vi => None,
186            XTrace => Some(('x', On)),
187        }
188    }
189
190    /// Returns the single-character option name specified by POSIX.
191    ///
192    /// This function is like [`short_name`](Self::short_name), but returns
193    /// `None` for an option whose short name is not specified by POSIX. The
194    /// result is otherwise the same as `short_name`.
195    ///
196    /// ```
197    /// # use yash_env::option::*;
198    /// assert_eq!(ErrExit.portable_short_name(), Some(('e', On)));
199    /// assert_eq!(Clobber.portable_short_name(), Some(('C', Off)));
200    /// assert_eq!(Login.portable_short_name(), None);
201    /// ```
202    ///
203    /// Note that the names returned by this function are the ones POSIX
204    /// specifies for the shell invocation. The `set` built-in accepts a subset
205    /// of them: POSIX does not allow `-c`, `-i`, and `-s` for the built-in.
206    /// Use [`is_modifiable`](Self::is_modifiable) to exclude those.
207    #[must_use]
208    pub const fn portable_short_name(self) -> std::option::Option<(char, State)> {
209        match self {
210            AllExport => Some(('a', On)),
211            Clobber => Some(('C', Off)),
212            CmdLine => Some(('c', On)),
213            ErrExit => Some(('e', On)),
214            Exec => Some(('n', Off)),
215            Glob => Some(('f', Off)),
216            HashOnDefinition => Some(('h', On)),
217            Interactive => Some(('i', On)),
218            Monitor => Some(('m', On)),
219            Notify => Some(('b', On)),
220            Stdin => Some(('s', On)),
221            Unset => Some(('u', Off)),
222            Verbose => Some(('v', On)),
223            XTrace => Some(('x', On)),
224            IgnoreEof | Log | Login | PipeFail | Portable | PosixlyCorrect | Vi => None,
225        }
226    }
227
228    /// Returns the option name, all in lower case without punctuations.
229    ///
230    /// This function returns a string like `"allexport"` and `"exec"`.
231    /// The name can be converted back to `Option` with [`parse_long`].
232    #[must_use]
233    pub const fn long_name(self) -> &'static str {
234        match self {
235            AllExport => "allexport",
236            Clobber => "clobber",
237            CmdLine => "cmdline",
238            ErrExit => "errexit",
239            Exec => "exec",
240            Glob => "glob",
241            HashOnDefinition => "hashondefinition",
242            IgnoreEof => "ignoreeof",
243            Interactive => "interactive",
244            Log => "log",
245            Login => "login",
246            Monitor => "monitor",
247            Notify => "notify",
248            PipeFail => "pipefail",
249            Portable => "portable",
250            PosixlyCorrect => "posixlycorrect",
251            Stdin => "stdin",
252            Unset => "unset",
253            Verbose => "verbose",
254            Vi => "vi",
255            XTrace => "xtrace",
256        }
257    }
258
259    /// Returns the option name specified by POSIX for the `-o` option.
260    ///
261    /// This function returns the name POSIX defines for the option and the
262    /// state rendered by the name, or `None` if POSIX defines no name for the
263    /// option. Unlike [`long_name`](Self::long_name), which always names the
264    /// enabled state, POSIX names some options in their disabled state, so the
265    /// returned name may differ from the one `long_name` returns.
266    ///
267    /// ```
268    /// # use yash_env::option::*;
269    /// assert_eq!(ErrExit.portable_long_name(), Some(("errexit", On)));
270    /// assert_eq!(Clobber.long_name(), "clobber");
271    /// assert_eq!(Clobber.portable_long_name(), Some(("noclobber", Off)));
272    /// assert_eq!(Login.portable_long_name(), None);
273    /// ```
274    ///
275    /// The name can be converted back to `Option` with [`parse_long`], which
276    /// yields the same state as this function returns.
277    #[must_use]
278    pub const fn portable_long_name(self) -> std::option::Option<(&'static str, State)> {
279        match self {
280            AllExport => Some(("allexport", On)),
281            Clobber => Some(("noclobber", Off)),
282            ErrExit => Some(("errexit", On)),
283            Exec => Some(("noexec", Off)),
284            Glob => Some(("noglob", Off)),
285            IgnoreEof => Some(("ignoreeof", On)),
286            Log => Some(("nolog", Off)),
287            Monitor => Some(("monitor", On)),
288            Notify => Some(("notify", On)),
289            PipeFail => Some(("pipefail", On)),
290            Unset => Some(("nounset", Off)),
291            Verbose => Some(("verbose", On)),
292            Vi => Some(("vi", On)),
293            XTrace => Some(("xtrace", On)),
294            CmdLine | HashOnDefinition | Interactive | Login | Portable | PosixlyCorrect
295            | Stdin => None,
296        }
297    }
298}
299
300/// Prints the option name, all in lower case without punctuations.
301impl Display for Option {
302    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
303        self.long_name().fmt(f)
304    }
305}
306
307/// Error type indicating that the input string does not name a valid option.
308#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
309pub enum FromStrError {
310    /// The input string does not match any option name.
311    #[error("no such option")]
312    NoSuchOption,
313
314    /// The input string is a prefix of more than one valid option name.
315    #[error("ambiguous option name")]
316    Ambiguous,
317}
318
319pub use FromStrError::*;
320
321/// Parses an option name.
322///
323/// The input string should be a canonical option name, that is, all the
324/// characters should be lowercase and there should be no punctuations or other
325/// irrelevant characters. You can [canonicalize] the name before parsing it.
326///
327/// The option name may be abbreviated as long as it is an unambiguous prefix of
328/// a valid option name. For example, `Option::from_str("clob")` will return
329/// `Ok(Clobber)` like `Option::from_str("clobber")`. If the name is ambiguous,
330/// `from_str` returns `Err(Ambiguous)`. A full option name is never considered
331/// ambiguous. For example, `"log"` is not ambiguous even though it is also a
332/// prefix of another valid option `"login"`.
333///
334/// Note that new options may be added in the future, which can turn an
335/// unambiguous option name into an ambiguous one. You should use full option
336/// names for maximum compatibility.
337impl FromStr for Option {
338    type Err = FromStrError;
339    fn from_str(name: &str) -> Result<Self, FromStrError> {
340        const OPTIONS: &[(&str, Option)] = &[
341            ("allexport", AllExport),
342            ("clobber", Clobber),
343            ("cmdline", CmdLine),
344            ("errexit", ErrExit),
345            ("exec", Exec),
346            ("glob", Glob),
347            ("hashondefinition", HashOnDefinition),
348            ("ignoreeof", IgnoreEof),
349            ("interactive", Interactive),
350            ("log", Log),
351            ("login", Login),
352            ("monitor", Monitor),
353            ("notify", Notify),
354            ("pipefail", PipeFail),
355            ("portable", Portable),
356            ("posixlycorrect", PosixlyCorrect),
357            ("stdin", Stdin),
358            ("unset", Unset),
359            ("verbose", Verbose),
360            ("vi", Vi),
361            ("xtrace", XTrace),
362        ];
363
364        match OPTIONS.binary_search_by_key(&name, |&(full_name, _option)| full_name) {
365            Ok(index) => Ok(OPTIONS[index].1),
366            Err(index) => {
367                let mut options = OPTIONS[index..]
368                    .iter()
369                    .filter(|&(full_name, _option)| full_name.starts_with(name));
370                match options.next() {
371                    Some(first) => match options.next() {
372                        Some(_second) => Err(Ambiguous),
373                        None => Ok(first.1),
374                    },
375                    None => Err(NoSuchOption),
376                }
377            }
378        }
379    }
380}
381
382/// Parses a short option name.
383///
384/// This function parses the following single-character option names.
385///
386/// ```
387/// # use yash_env::option::*;
388/// assert_eq!(parse_short('a'), Some((AllExport, On)));
389/// assert_eq!(parse_short('b'), Some((Notify, On)));
390/// assert_eq!(parse_short('C'), Some((Clobber, Off)));
391/// assert_eq!(parse_short('c'), Some((CmdLine, On)));
392/// assert_eq!(parse_short('e'), Some((ErrExit, On)));
393/// assert_eq!(parse_short('f'), Some((Glob, Off)));
394/// assert_eq!(parse_short('h'), Some((HashOnDefinition, On)));
395/// assert_eq!(parse_short('i'), Some((Interactive, On)));
396/// assert_eq!(parse_short('l'), Some((Login, On)));
397/// assert_eq!(parse_short('m'), Some((Monitor, On)));
398/// assert_eq!(parse_short('n'), Some((Exec, Off)));
399/// assert_eq!(parse_short('s'), Some((Stdin, On)));
400/// assert_eq!(parse_short('u'), Some((Unset, Off)));
401/// assert_eq!(parse_short('v'), Some((Verbose, On)));
402/// assert_eq!(parse_short('x'), Some((XTrace, On)));
403/// ```
404///
405/// The name argument is case-sensitive.
406///
407/// This function returns `None` if the argument does not match any of the short
408/// option names above. Note that new names may be added in the future and it is
409/// not considered a breaking API change.
410#[must_use]
411pub const fn parse_short(name: char) -> std::option::Option<(self::Option, State)> {
412    match name {
413        'a' => Some((AllExport, On)),
414        'b' => Some((Notify, On)),
415        'C' => Some((Clobber, Off)),
416        'c' => Some((CmdLine, On)),
417        'e' => Some((ErrExit, On)),
418        'f' => Some((Glob, Off)),
419        'h' => Some((HashOnDefinition, On)),
420        'i' => Some((Interactive, On)),
421        'l' => Some((Login, On)),
422        'm' => Some((Monitor, On)),
423        'n' => Some((Exec, Off)),
424        's' => Some((Stdin, On)),
425        'u' => Some((Unset, Off)),
426        'v' => Some((Verbose, On)),
427        'x' => Some((XTrace, On)),
428        _ => None,
429    }
430}
431
432/// Iterator of options
433///
434/// This iterator yields all available options in alphabetical order.
435///
436/// An `Iter` can be created by [`Option::iter()`].
437#[derive(Clone, Debug)]
438pub struct Iter {
439    inner: EnumSetIter<Option>,
440}
441
442impl Iterator for Iter {
443    type Item = Option;
444    fn next(&mut self) -> std::option::Option<self::Option> {
445        self.inner.next()
446    }
447    fn size_hint(&self) -> (usize, std::option::Option<usize>) {
448        self.inner.size_hint()
449    }
450}
451
452impl DoubleEndedIterator for Iter {
453    fn next_back(&mut self) -> std::option::Option<self::Option> {
454        self.inner.next_back()
455    }
456}
457
458impl ExactSizeIterator for Iter {}
459
460impl Option {
461    /// Creates an iterator that yields all available options in alphabetical
462    /// order.
463    pub fn iter() -> Iter {
464        Iter {
465            inner: EnumSet::<Option>::all().iter(),
466        }
467    }
468}
469
470/// Parses a long option name.
471///
472/// This function is similar to `impl FromStr for Option`, but allows prefixing
473/// the option name with `no` to negate the state.
474///
475/// ```
476/// # use yash_env::option::{parse_long, FromStrError::NoSuchOption, Option::*, State::*};
477/// assert_eq!(parse_long("notify"), Ok((Notify, On)));
478/// assert_eq!(parse_long("nonotify"), Ok((Notify, Off)));
479/// assert_eq!(parse_long("tify"), Err(NoSuchOption));
480/// ```
481///
482/// Note that new options may be added in the future, which can turn an
483/// unambiguous option name into an ambiguous one. You should use full option
484/// names for forward compatibility.
485///
486/// You cannot parse a short option name with this function. Use [`parse_short`]
487/// for that purpose.
488pub fn parse_long(name: &str) -> Result<(Option, State), FromStrError> {
489    if "no".starts_with(name) {
490        return Err(Ambiguous);
491    }
492
493    let intact = Option::from_str(name);
494    let without_no = name
495        .strip_prefix("no")
496        .ok_or(NoSuchOption)
497        .and_then(Option::from_str);
498
499    match (intact, without_no) {
500        (Ok(option), Err(NoSuchOption)) => Ok((option, On)),
501        (Err(NoSuchOption), Ok(option)) => Ok((option, Off)),
502        (Err(Ambiguous), _) | (_, Err(Ambiguous)) => Err(Ambiguous),
503        _ => Err(NoSuchOption),
504    }
505}
506
507/// Canonicalize an option name.
508///
509/// This function converts the string to lower case and removes non-alphanumeric
510/// characters. Exceptionally, this function does not convert non-ASCII
511/// uppercase characters because they will not constitute a valid option name
512/// anyway.
513pub fn canonicalize(name: &str) -> Cow<'_, str> {
514    if name
515        .chars()
516        .all(|c| c.is_alphanumeric() && !c.is_ascii_uppercase())
517    {
518        Cow::Borrowed(name)
519    } else {
520        Cow::Owned(
521            name.chars()
522                .filter(|c| c.is_alphanumeric())
523                .map(|c| c.to_ascii_lowercase())
524                .collect(),
525        )
526    }
527}
528
529/// Set of the shell options and their states.
530#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
531pub struct OptionSet {
532    enabled_options: EnumSet<Option>,
533}
534
535/// Defines the default option set.
536///
537/// Note that the default set is not empty. The following options are enabled by
538/// default: `Clobber`, `Exec`, `Glob`, `Log`, `Unset`
539impl Default for OptionSet {
540    fn default() -> Self {
541        let enabled_options = Clobber | Exec | Glob | Log | Unset;
542        OptionSet { enabled_options }
543    }
544}
545
546impl OptionSet {
547    /// Creates an option set with all options disabled.
548    pub fn empty() -> Self {
549        OptionSet {
550            enabled_options: EnumSet::empty(),
551        }
552    }
553
554    // Some options are mutually exclusive, so there is no "all" function that
555    // returns an option set with all options enabled.
556
557    /// Returns the current state of the option.
558    pub fn get(&self, option: Option) -> State {
559        if self.enabled_options.contains(option) {
560            On
561        } else {
562            Off
563        }
564    }
565
566    /// Changes an option's state.
567    ///
568    /// Some options should not be changed after the shell startup, but that
569    /// does not affect the behavior of this function.
570    ///
571    /// TODO: What if an option that is mutually exclusive with another is set?
572    pub fn set(&mut self, option: Option, state: State) {
573        match state {
574            On => self.enabled_options.insert(option),
575            Off => self.enabled_options.remove(option),
576        };
577    }
578}
579
580impl Extend<Option> for OptionSet {
581    fn extend<T: IntoIterator<Item = Option>>(&mut self, iter: T) {
582        self.enabled_options.extend(iter);
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    #[test]
591    fn short_name_round_trip() {
592        for option in EnumSet::<Option>::all() {
593            if let Some((name, state)) = option.short_name() {
594                assert_eq!(parse_short(name), Some((option, state)));
595            }
596        }
597        for name in 'A'..='z' {
598            if let Some((option, state)) = parse_short(name) {
599                assert_eq!(option.short_name(), Some((name, state)));
600            }
601        }
602    }
603
604    #[test]
605    fn portable_short_names() {
606        assert_eq!(AllExport.portable_short_name(), Some(('a', On)));
607        assert_eq!(Clobber.portable_short_name(), Some(('C', Off)));
608        assert_eq!(CmdLine.portable_short_name(), Some(('c', On)));
609        assert_eq!(ErrExit.portable_short_name(), Some(('e', On)));
610        assert_eq!(Exec.portable_short_name(), Some(('n', Off)));
611        assert_eq!(Glob.portable_short_name(), Some(('f', Off)));
612        assert_eq!(HashOnDefinition.portable_short_name(), Some(('h', On)));
613        assert_eq!(IgnoreEof.portable_short_name(), None);
614        assert_eq!(Interactive.portable_short_name(), Some(('i', On)));
615        assert_eq!(Log.portable_short_name(), None);
616        assert_eq!(Login.portable_short_name(), None);
617        assert_eq!(Monitor.portable_short_name(), Some(('m', On)));
618        assert_eq!(Notify.portable_short_name(), Some(('b', On)));
619        assert_eq!(PipeFail.portable_short_name(), None);
620        assert_eq!(Portable.portable_short_name(), None);
621        assert_eq!(PosixlyCorrect.portable_short_name(), None);
622        assert_eq!(Stdin.portable_short_name(), Some(('s', On)));
623        assert_eq!(Unset.portable_short_name(), Some(('u', Off)));
624        assert_eq!(Verbose.portable_short_name(), Some(('v', On)));
625        assert_eq!(Vi.portable_short_name(), None);
626        assert_eq!(XTrace.portable_short_name(), Some(('x', On)));
627    }
628
629    #[test]
630    fn portable_short_name_agrees_with_short_name() {
631        for option in EnumSet::<Option>::all() {
632            if let Some(name) = option.portable_short_name() {
633                assert_eq!(option.short_name(), Some(name), "{option}");
634            }
635        }
636    }
637
638    #[test]
639    fn portable_long_names() {
640        assert_eq!(AllExport.portable_long_name(), Some(("allexport", On)));
641        assert_eq!(Clobber.portable_long_name(), Some(("noclobber", Off)));
642        assert_eq!(CmdLine.portable_long_name(), None);
643        assert_eq!(ErrExit.portable_long_name(), Some(("errexit", On)));
644        assert_eq!(Exec.portable_long_name(), Some(("noexec", Off)));
645        assert_eq!(Glob.portable_long_name(), Some(("noglob", Off)));
646        assert_eq!(HashOnDefinition.portable_long_name(), None);
647        assert_eq!(IgnoreEof.portable_long_name(), Some(("ignoreeof", On)));
648        assert_eq!(Interactive.portable_long_name(), None);
649        assert_eq!(Log.portable_long_name(), Some(("nolog", Off)));
650        assert_eq!(Login.portable_long_name(), None);
651        assert_eq!(Monitor.portable_long_name(), Some(("monitor", On)));
652        assert_eq!(Notify.portable_long_name(), Some(("notify", On)));
653        assert_eq!(PipeFail.portable_long_name(), Some(("pipefail", On)));
654        assert_eq!(Portable.portable_long_name(), None);
655        assert_eq!(PosixlyCorrect.portable_long_name(), None);
656        assert_eq!(Stdin.portable_long_name(), None);
657        assert_eq!(Unset.portable_long_name(), Some(("nounset", Off)));
658        assert_eq!(Verbose.portable_long_name(), Some(("verbose", On)));
659        assert_eq!(Vi.portable_long_name(), Some(("vi", On)));
660        assert_eq!(XTrace.portable_long_name(), Some(("xtrace", On)));
661    }
662
663    #[test]
664    fn portable_long_name_parses_back_to_the_option() {
665        for option in EnumSet::<Option>::all() {
666            if let Some((name, state)) = option.portable_long_name() {
667                assert_eq!(parse_long(name), Ok((option, state)), "{option}");
668            }
669        }
670    }
671
672    #[test]
673    fn display_and_from_str_round_trip() {
674        for option in EnumSet::<Option>::all() {
675            let name = option.to_string();
676            assert_eq!(Option::from_str(&name), Ok(option));
677        }
678    }
679
680    #[test]
681    fn from_str_unambiguous_abbreviation() {
682        assert_eq!(Option::from_str("allexpor"), Ok(AllExport));
683        assert_eq!(Option::from_str("a"), Ok(AllExport));
684        assert_eq!(Option::from_str("n"), Ok(Notify));
685    }
686
687    #[test]
688    fn from_str_ambiguous_abbreviation() {
689        assert_eq!(Option::from_str(""), Err(Ambiguous));
690        assert_eq!(Option::from_str("c"), Err(Ambiguous));
691        assert_eq!(Option::from_str("lo"), Err(Ambiguous));
692    }
693
694    #[test]
695    fn from_str_no_match() {
696        assert_eq!(Option::from_str("vim"), Err(NoSuchOption));
697        assert_eq!(Option::from_str("0"), Err(NoSuchOption));
698        assert_eq!(Option::from_str("LOG"), Err(NoSuchOption));
699    }
700
701    #[test]
702    fn display_and_parse_round_trip() {
703        for option in EnumSet::<Option>::all() {
704            let name = option.to_string();
705            assert_eq!(parse_long(&name), Ok((option, On)));
706        }
707    }
708
709    #[test]
710    fn display_and_parse_negated_round_trip() {
711        for option in EnumSet::<Option>::all() {
712            let name = format!("no{option}");
713            assert_eq!(parse_long(&name), Ok((option, Off)));
714        }
715    }
716
717    #[test]
718    fn parse_unambiguous_abbreviation() {
719        assert_eq!(parse_long("allexpor"), Ok((AllExport, On)));
720        assert_eq!(parse_long("not"), Ok((Notify, On)));
721        assert_eq!(parse_long("non"), Ok((Notify, Off)));
722        assert_eq!(parse_long("un"), Ok((Unset, On)));
723        assert_eq!(parse_long("noun"), Ok((Unset, Off)));
724    }
725
726    #[test]
727    fn parse_ambiguous_abbreviation() {
728        assert_eq!(parse_long(""), Err(Ambiguous));
729        assert_eq!(parse_long("n"), Err(Ambiguous));
730        assert_eq!(parse_long("no"), Err(Ambiguous));
731        assert_eq!(parse_long("noe"), Err(Ambiguous));
732        assert_eq!(parse_long("e"), Err(Ambiguous));
733        assert_eq!(parse_long("nolo"), Err(Ambiguous));
734    }
735
736    #[test]
737    fn parse_no_match() {
738        assert_eq!(parse_long("vim"), Err(NoSuchOption));
739        assert_eq!(parse_long("0"), Err(NoSuchOption));
740        assert_eq!(parse_long("novim"), Err(NoSuchOption));
741        assert_eq!(parse_long("no0"), Err(NoSuchOption));
742        assert_eq!(parse_long("LOG"), Err(NoSuchOption));
743    }
744
745    #[test]
746    fn test_canonicalize() {
747        assert_eq!(canonicalize(""), "");
748        assert_eq!(canonicalize("POSIXlyCorrect"), "posixlycorrect");
749        assert_eq!(canonicalize(" log "), "log");
750        assert_eq!(canonicalize("gLoB"), "glob");
751        assert_eq!(canonicalize("no-notify"), "nonotify");
752        assert_eq!(canonicalize(" no  such_Option "), "nosuchoption");
753        assert_eq!(canonicalize("Abc"), "Abc");
754    }
755}