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