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    #[must_use]
68    fn not(self) -> Self {
69        match self {
70            On => Off,
71            Off => On,
72        }
73    }
74}
75
76/// Converts a Boolean to a state
77impl From<bool> for State {
78    fn from(is_on: bool) -> Self {
79        if is_on {
80            On
81        } else {
82            Off
83        }
84    }
85}
86
87/// Converts a state to a Boolean
88impl From<State> for bool {
89    fn from(state: State) -> Self {
90        match state {
91            On => true,
92            Off => false,
93        }
94    }
95}
96
97/// Shell option
98#[derive(Clone, Copy, Debug, EnumSetType, Eq, Hash, PartialEq)]
99#[enumset(no_super_impls)]
100#[non_exhaustive]
101pub enum Option {
102    /// Makes all variables exported when they are assigned.
103    AllExport,
104    /// Allows overwriting and truncating an existing file with the `>`
105    /// redirection.
106    Clobber,
107    /// Executes a command string specified as a command line argument.
108    CmdLine,
109    /// Makes the shell to exit when a command returns a non-zero exit status.
110    ErrExit,
111    /// Makes the shell to actually run commands.
112    Exec,
113    /// Enables pathname expansion.
114    Glob,
115    /// Performs command search for each command in a function on its
116    /// definition.
117    HashOnDefinition,
118    /// Prevents the interactive shell from exiting when the user enters an
119    /// end-of-file.
120    IgnoreEof,
121    /// Enables features for interactive use.
122    Interactive,
123    /// Allows function definition commands to be recorded in the command
124    /// history.
125    Log,
126    /// Sources the profile file on startup.
127    Login,
128    /// Enables job control.
129    Monitor,
130    /// Automatically reports the results of asynchronous jobs.
131    Notify,
132    /// Disables most non-POSIX extensions.
133    PosixlyCorrect,
134    /// Reads commands from the standard input.
135    Stdin,
136    /// Expands unset variables to an empty string rather than erroring out.
137    Unset,
138    /// Echos the input before parsing and executing.
139    Verbose,
140    /// Enables vi-like command line editing.
141    Vi,
142    /// Prints expanded words during command execution.
143    XTrace,
144}
145
146pub use self::Option::*;
147
148impl Option {
149    /// Whether this option can be modified by the set built-in.
150    ///
151    /// Unmodifiable options can be set only on shell startup.
152    #[must_use]
153    pub const fn is_modifiable(self) -> bool {
154        !matches!(self, CmdLine | Interactive | Stdin)
155    }
156
157    /// Returns the single-character option name.
158    ///
159    /// This function returns a short name for the option and the state rendered
160    /// by the name.
161    /// The name can be converted back to `Option` with [`parse_short`].
162    /// Note that the result is `None` for options that do not have a short
163    /// name.
164    #[must_use]
165    pub const fn short_name(self) -> std::option::Option<(char, State)> {
166        match self {
167            AllExport => Some(('a', On)),
168            Clobber => Some(('C', Off)),
169            CmdLine => Some(('c', On)),
170            ErrExit => Some(('e', On)),
171            Exec => Some(('n', Off)),
172            Glob => Some(('f', Off)),
173            HashOnDefinition => Some(('h', On)),
174            IgnoreEof => None,
175            Interactive => Some(('i', On)),
176            Log => None,
177            Login => Some(('l', On)),
178            Monitor => Some(('m', On)),
179            Notify => Some(('b', On)),
180            PosixlyCorrect => None,
181            Stdin => Some(('s', On)),
182            Unset => Some(('u', Off)),
183            Verbose => Some(('v', On)),
184            Vi => None,
185            XTrace => Some(('x', On)),
186        }
187    }
188
189    /// Returns the option name, all in lower case without punctuations.
190    ///
191    /// This function returns a string like `"allexport"` and `"exec"`.
192    /// The name can be converted back to `Option` with [`parse_long`].
193    #[must_use]
194    pub const fn long_name(self) -> &'static str {
195        match self {
196            AllExport => "allexport",
197            Clobber => "clobber",
198            CmdLine => "cmdline",
199            ErrExit => "errexit",
200            Exec => "exec",
201            Glob => "glob",
202            HashOnDefinition => "hashondefinition",
203            IgnoreEof => "ignoreeof",
204            Interactive => "interactive",
205            Log => "log",
206            Login => "login",
207            Monitor => "monitor",
208            Notify => "notify",
209            PosixlyCorrect => "posixlycorrect",
210            Stdin => "stdin",
211            Unset => "unset",
212            Verbose => "verbose",
213            Vi => "vi",
214            XTrace => "xtrace",
215        }
216    }
217}
218
219/// Prints the option name, all in lower case without punctuations.
220impl Display for Option {
221    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
222        self.long_name().fmt(f)
223    }
224}
225
226/// Error type indicating that the input string does not name a valid option.
227#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
228pub enum FromStrError {
229    /// The input string does not match any option name.
230    #[error("no such option")]
231    NoSuchOption,
232
233    /// The input string is a prefix of more than one valid option name.
234    #[error("ambiguous option name")]
235    Ambiguous,
236}
237
238pub use FromStrError::*;
239
240/// Parses an option name.
241///
242/// The input string should be a canonical option name, that is, all the
243/// characters should be lowercase and there should be no punctuations or other
244/// irrelevant characters. You can [canonicalize] the name before parsing it.
245///
246/// The option name may be abbreviated as long as it is an unambiguous prefix of
247/// a valid option name. For example, `Option::from_str("clob")` will return
248/// `Ok(Clobber)` like `Option::from_str("clobber")`. If the name is ambiguous,
249/// `from_str` returns `Err(Ambiguous)`. A full option name is never considered
250/// ambiguous. For example, `"log"` is not ambiguous even though it is also a
251/// prefix of another valid option `"login"`.
252///
253/// Note that new options may be added in the future, which can turn an
254/// unambiguous option name into an ambiguous one. You should use full option
255/// names for maximum compatibility.
256impl FromStr for Option {
257    type Err = FromStrError;
258    fn from_str(name: &str) -> Result<Self, FromStrError> {
259        const OPTIONS: &[(&str, Option)] = &[
260            ("allexport", AllExport),
261            ("clobber", Clobber),
262            ("cmdline", CmdLine),
263            ("errexit", ErrExit),
264            ("exec", Exec),
265            ("glob", Glob),
266            ("hashondefinition", HashOnDefinition),
267            ("ignoreeof", IgnoreEof),
268            ("interactive", Interactive),
269            ("log", Log),
270            ("login", Login),
271            ("monitor", Monitor),
272            ("notify", Notify),
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}