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 most non-POSIX extensions.
130    PosixlyCorrect,
131    /// Reads commands from the standard input.
132    Stdin,
133    /// Expands unset variables to an empty string rather than erroring out.
134    Unset,
135    /// Echos the input before parsing and executing.
136    Verbose,
137    /// Enables vi-like command line editing.
138    Vi,
139    /// Prints expanded words during command execution.
140    XTrace,
141}
142
143pub use self::Option::*;
144
145impl Option {
146    /// Whether this option can be modified by the set built-in.
147    ///
148    /// Unmodifiable options can be set only on shell startup.
149    #[must_use]
150    pub const fn is_modifiable(self) -> bool {
151        !matches!(self, CmdLine | Interactive | Stdin)
152    }
153
154    /// Returns the single-character option name.
155    ///
156    /// This function returns a short name for the option and the state rendered
157    /// by the name.
158    /// The name can be converted back to `Option` with [`parse_short`].
159    /// Note that the result is `None` for options that do not have a short
160    /// name.
161    #[must_use]
162    pub const fn short_name(self) -> std::option::Option<(char, State)> {
163        match self {
164            AllExport => Some(('a', On)),
165            Clobber => Some(('C', Off)),
166            CmdLine => Some(('c', On)),
167            ErrExit => Some(('e', On)),
168            Exec => Some(('n', Off)),
169            Glob => Some(('f', Off)),
170            HashOnDefinition => Some(('h', On)),
171            IgnoreEof => None,
172            Interactive => Some(('i', On)),
173            Log => None,
174            Login => Some(('l', On)),
175            Monitor => Some(('m', On)),
176            Notify => Some(('b', On)),
177            PipeFail => None,
178            PosixlyCorrect => None,
179            Stdin => Some(('s', On)),
180            Unset => Some(('u', Off)),
181            Verbose => Some(('v', On)),
182            Vi => None,
183            XTrace => Some(('x', On)),
184        }
185    }
186
187    /// Returns the option name, all in lower case without punctuations.
188    ///
189    /// This function returns a string like `"allexport"` and `"exec"`.
190    /// The name can be converted back to `Option` with [`parse_long`].
191    #[must_use]
192    pub const fn long_name(self) -> &'static str {
193        match self {
194            AllExport => "allexport",
195            Clobber => "clobber",
196            CmdLine => "cmdline",
197            ErrExit => "errexit",
198            Exec => "exec",
199            Glob => "glob",
200            HashOnDefinition => "hashondefinition",
201            IgnoreEof => "ignoreeof",
202            Interactive => "interactive",
203            Log => "log",
204            Login => "login",
205            Monitor => "monitor",
206            Notify => "notify",
207            PipeFail => "pipefail",
208            PosixlyCorrect => "posixlycorrect",
209            Stdin => "stdin",
210            Unset => "unset",
211            Verbose => "verbose",
212            Vi => "vi",
213            XTrace => "xtrace",
214        }
215    }
216}
217
218/// Prints the option name, all in lower case without punctuations.
219impl Display for Option {
220    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
221        self.long_name().fmt(f)
222    }
223}
224
225/// Error type indicating that the input string does not name a valid option.
226#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
227pub enum FromStrError {
228    /// The input string does not match any option name.
229    #[error("no such option")]
230    NoSuchOption,
231
232    /// The input string is a prefix of more than one valid option name.
233    #[error("ambiguous option name")]
234    Ambiguous,
235}
236
237pub use FromStrError::*;
238
239/// Parses an option name.
240///
241/// The input string should be a canonical option name, that is, all the
242/// characters should be lowercase and there should be no punctuations or other
243/// irrelevant characters. You can [canonicalize] the name before parsing it.
244///
245/// The option name may be abbreviated as long as it is an unambiguous prefix of
246/// a valid option name. For example, `Option::from_str("clob")` will return
247/// `Ok(Clobber)` like `Option::from_str("clobber")`. If the name is ambiguous,
248/// `from_str` returns `Err(Ambiguous)`. A full option name is never considered
249/// ambiguous. For example, `"log"` is not ambiguous even though it is also a
250/// prefix of another valid option `"login"`.
251///
252/// Note that new options may be added in the future, which can turn an
253/// unambiguous option name into an ambiguous one. You should use full option
254/// names for maximum compatibility.
255impl FromStr for Option {
256    type Err = FromStrError;
257    fn from_str(name: &str) -> Result<Self, FromStrError> {
258        const OPTIONS: &[(&str, Option)] = &[
259            ("allexport", AllExport),
260            ("clobber", Clobber),
261            ("cmdline", CmdLine),
262            ("errexit", ErrExit),
263            ("exec", Exec),
264            ("glob", Glob),
265            ("hashondefinition", HashOnDefinition),
266            ("ignoreeof", IgnoreEof),
267            ("interactive", Interactive),
268            ("log", Log),
269            ("login", Login),
270            ("monitor", Monitor),
271            ("notify", Notify),
272            ("pipefail", PipeFail),
273            ("posixlycorrect", PosixlyCorrect),
274            ("stdin", Stdin),
275            ("unset", Unset),
276            ("verbose", Verbose),
277            ("vi", Vi),
278            ("xtrace", XTrace),
279        ];
280
281        match OPTIONS.binary_search_by_key(&name, |&(full_name, _option)| full_name) {
282            Ok(index) => Ok(OPTIONS[index].1),
283            Err(index) => {
284                let mut options = OPTIONS[index..]
285                    .iter()
286                    .filter(|&(full_name, _option)| full_name.starts_with(name));
287                match options.next() {
288                    Some(first) => match options.next() {
289                        Some(_second) => Err(Ambiguous),
290                        None => Ok(first.1),
291                    },
292                    None => Err(NoSuchOption),
293                }
294            }
295        }
296    }
297}
298
299/// Parses a short option name.
300///
301/// This function parses the following single-character option names.
302///
303/// ```
304/// # use yash_env::option::*;
305/// assert_eq!(parse_short('a'), Some((AllExport, On)));
306/// assert_eq!(parse_short('b'), Some((Notify, On)));
307/// assert_eq!(parse_short('C'), Some((Clobber, Off)));
308/// assert_eq!(parse_short('c'), Some((CmdLine, On)));
309/// assert_eq!(parse_short('e'), Some((ErrExit, On)));
310/// assert_eq!(parse_short('f'), Some((Glob, Off)));
311/// assert_eq!(parse_short('h'), Some((HashOnDefinition, On)));
312/// assert_eq!(parse_short('i'), Some((Interactive, On)));
313/// assert_eq!(parse_short('l'), Some((Login, On)));
314/// assert_eq!(parse_short('m'), Some((Monitor, On)));
315/// assert_eq!(parse_short('n'), Some((Exec, Off)));
316/// assert_eq!(parse_short('s'), Some((Stdin, On)));
317/// assert_eq!(parse_short('u'), Some((Unset, Off)));
318/// assert_eq!(parse_short('v'), Some((Verbose, On)));
319/// assert_eq!(parse_short('x'), Some((XTrace, On)));
320/// ```
321///
322/// The name argument is case-sensitive.
323///
324/// This function returns `None` if the argument does not match any of the short
325/// option names above. Note that new names may be added in the future and it is
326/// not considered a breaking API change.
327#[must_use]
328pub const fn parse_short(name: char) -> std::option::Option<(self::Option, State)> {
329    match name {
330        'a' => Some((AllExport, On)),
331        'b' => Some((Notify, On)),
332        'C' => Some((Clobber, Off)),
333        'c' => Some((CmdLine, On)),
334        'e' => Some((ErrExit, On)),
335        'f' => Some((Glob, Off)),
336        'h' => Some((HashOnDefinition, On)),
337        'i' => Some((Interactive, On)),
338        'l' => Some((Login, On)),
339        'm' => Some((Monitor, On)),
340        'n' => Some((Exec, Off)),
341        's' => Some((Stdin, On)),
342        'u' => Some((Unset, Off)),
343        'v' => Some((Verbose, On)),
344        'x' => Some((XTrace, On)),
345        _ => None,
346    }
347}
348
349/// Iterator of options
350///
351/// This iterator yields all available options in alphabetical order.
352///
353/// An `Iter` can be created by [`Option::iter()`].
354#[derive(Clone, Debug)]
355pub struct Iter {
356    inner: EnumSetIter<Option>,
357}
358
359impl Iterator for Iter {
360    type Item = Option;
361    fn next(&mut self) -> std::option::Option<self::Option> {
362        self.inner.next()
363    }
364    fn size_hint(&self) -> (usize, std::option::Option<usize>) {
365        self.inner.size_hint()
366    }
367}
368
369impl DoubleEndedIterator for Iter {
370    fn next_back(&mut self) -> std::option::Option<self::Option> {
371        self.inner.next_back()
372    }
373}
374
375impl ExactSizeIterator for Iter {}
376
377impl Option {
378    /// Creates an iterator that yields all available options in alphabetical
379    /// order.
380    pub fn iter() -> Iter {
381        Iter {
382            inner: EnumSet::<Option>::all().iter(),
383        }
384    }
385}
386
387/// Parses a long option name.
388///
389/// This function is similar to `impl FromStr for Option`, but allows prefixing
390/// the option name with `no` to negate the state.
391///
392/// ```
393/// # use yash_env::option::{parse_long, FromStrError::NoSuchOption, Option::*, State::*};
394/// assert_eq!(parse_long("notify"), Ok((Notify, On)));
395/// assert_eq!(parse_long("nonotify"), Ok((Notify, Off)));
396/// assert_eq!(parse_long("tify"), Err(NoSuchOption));
397/// ```
398///
399/// Note that new options may be added in the future, which can turn an
400/// unambiguous option name into an ambiguous one. You should use full option
401/// names for forward compatibility.
402///
403/// You cannot parse a short option name with this function. Use [`parse_short`]
404/// for that purpose.
405pub fn parse_long(name: &str) -> Result<(Option, State), FromStrError> {
406    if "no".starts_with(name) {
407        return Err(Ambiguous);
408    }
409
410    let intact = Option::from_str(name);
411    let without_no = name
412        .strip_prefix("no")
413        .ok_or(NoSuchOption)
414        .and_then(Option::from_str);
415
416    match (intact, without_no) {
417        (Ok(option), Err(NoSuchOption)) => Ok((option, On)),
418        (Err(NoSuchOption), Ok(option)) => Ok((option, Off)),
419        (Err(Ambiguous), _) | (_, Err(Ambiguous)) => Err(Ambiguous),
420        _ => Err(NoSuchOption),
421    }
422}
423
424/// Canonicalize an option name.
425///
426/// This function converts the string to lower case and removes non-alphanumeric
427/// characters. Exceptionally, this function does not convert non-ASCII
428/// uppercase characters because they will not constitute a valid option name
429/// anyway.
430pub fn canonicalize(name: &str) -> Cow<'_, str> {
431    if name
432        .chars()
433        .all(|c| c.is_alphanumeric() && !c.is_ascii_uppercase())
434    {
435        Cow::Borrowed(name)
436    } else {
437        Cow::Owned(
438            name.chars()
439                .filter(|c| c.is_alphanumeric())
440                .map(|c| c.to_ascii_lowercase())
441                .collect(),
442        )
443    }
444}
445
446/// Set of the shell options and their states.
447#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
448pub struct OptionSet {
449    enabled_options: EnumSet<Option>,
450}
451
452/// Defines the default option set.
453///
454/// Note that the default set is not empty. The following options are enabled by
455/// default: `Clobber`, `Exec`, `Glob`, `Log`, `Unset`
456impl Default for OptionSet {
457    fn default() -> Self {
458        let enabled_options = Clobber | Exec | Glob | Log | Unset;
459        OptionSet { enabled_options }
460    }
461}
462
463impl OptionSet {
464    /// Creates an option set with all options disabled.
465    pub fn empty() -> Self {
466        OptionSet {
467            enabled_options: EnumSet::empty(),
468        }
469    }
470
471    // Some options are mutually exclusive, so there is no "all" function that
472    // returns an option set with all options enabled.
473
474    /// Returns the current state of the option.
475    pub fn get(&self, option: Option) -> State {
476        if self.enabled_options.contains(option) {
477            On
478        } else {
479            Off
480        }
481    }
482
483    /// Changes an option's state.
484    ///
485    /// Some options should not be changed after the shell startup, but that
486    /// does not affect the behavior of this function.
487    ///
488    /// TODO: What if an option that is mutually exclusive with another is set?
489    pub fn set(&mut self, option: Option, state: State) {
490        match state {
491            On => self.enabled_options.insert(option),
492            Off => self.enabled_options.remove(option),
493        };
494    }
495}
496
497impl Extend<Option> for OptionSet {
498    fn extend<T: IntoIterator<Item = Option>>(&mut self, iter: T) {
499        self.enabled_options.extend(iter);
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[test]
508    fn short_name_round_trip() {
509        for option in EnumSet::<Option>::all() {
510            if let Some((name, state)) = option.short_name() {
511                assert_eq!(parse_short(name), Some((option, state)));
512            }
513        }
514        for name in 'A'..='z' {
515            if let Some((option, state)) = parse_short(name) {
516                assert_eq!(option.short_name(), Some((name, state)));
517            }
518        }
519    }
520
521    #[test]
522    fn display_and_from_str_round_trip() {
523        for option in EnumSet::<Option>::all() {
524            let name = option.to_string();
525            assert_eq!(Option::from_str(&name), Ok(option));
526        }
527    }
528
529    #[test]
530    fn from_str_unambiguous_abbreviation() {
531        assert_eq!(Option::from_str("allexpor"), Ok(AllExport));
532        assert_eq!(Option::from_str("a"), Ok(AllExport));
533        assert_eq!(Option::from_str("n"), Ok(Notify));
534    }
535
536    #[test]
537    fn from_str_ambiguous_abbreviation() {
538        assert_eq!(Option::from_str(""), Err(Ambiguous));
539        assert_eq!(Option::from_str("c"), Err(Ambiguous));
540        assert_eq!(Option::from_str("lo"), Err(Ambiguous));
541    }
542
543    #[test]
544    fn from_str_no_match() {
545        assert_eq!(Option::from_str("vim"), Err(NoSuchOption));
546        assert_eq!(Option::from_str("0"), Err(NoSuchOption));
547        assert_eq!(Option::from_str("LOG"), Err(NoSuchOption));
548    }
549
550    #[test]
551    fn display_and_parse_round_trip() {
552        for option in EnumSet::<Option>::all() {
553            let name = option.to_string();
554            assert_eq!(parse_long(&name), Ok((option, On)));
555        }
556    }
557
558    #[test]
559    fn display_and_parse_negated_round_trip() {
560        for option in EnumSet::<Option>::all() {
561            let name = format!("no{option}");
562            assert_eq!(parse_long(&name), Ok((option, Off)));
563        }
564    }
565
566    #[test]
567    fn parse_unambiguous_abbreviation() {
568        assert_eq!(parse_long("allexpor"), Ok((AllExport, On)));
569        assert_eq!(parse_long("not"), Ok((Notify, On)));
570        assert_eq!(parse_long("non"), Ok((Notify, Off)));
571        assert_eq!(parse_long("un"), Ok((Unset, On)));
572        assert_eq!(parse_long("noun"), Ok((Unset, Off)));
573    }
574
575    #[test]
576    fn parse_ambiguous_abbreviation() {
577        assert_eq!(parse_long(""), Err(Ambiguous));
578        assert_eq!(parse_long("n"), Err(Ambiguous));
579        assert_eq!(parse_long("no"), Err(Ambiguous));
580        assert_eq!(parse_long("noe"), Err(Ambiguous));
581        assert_eq!(parse_long("e"), Err(Ambiguous));
582        assert_eq!(parse_long("nolo"), Err(Ambiguous));
583    }
584
585    #[test]
586    fn parse_no_match() {
587        assert_eq!(parse_long("vim"), Err(NoSuchOption));
588        assert_eq!(parse_long("0"), Err(NoSuchOption));
589        assert_eq!(parse_long("novim"), Err(NoSuchOption));
590        assert_eq!(parse_long("no0"), Err(NoSuchOption));
591        assert_eq!(parse_long("LOG"), Err(NoSuchOption));
592    }
593
594    #[test]
595    fn test_canonicalize() {
596        assert_eq!(canonicalize(""), "");
597        assert_eq!(canonicalize("POSIXlyCorrect"), "posixlycorrect");
598        assert_eq!(canonicalize(" log "), "log");
599        assert_eq!(canonicalize("gLoB"), "glob");
600        assert_eq!(canonicalize("no-notify"), "nonotify");
601        assert_eq!(canonicalize(" no  such_Option "), "nosuchoption");
602        assert_eq!(canonicalize("Abc"), "Abc");
603    }
604}