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