Skip to main content

yash_builtin/unset/
syntax.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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//! Parses the unset built-in command arguments.
18
19use crate::common::syntax::ConflictingOptionError;
20use crate::common::syntax::OptionSpec;
21use crate::common::syntax::parse_arguments;
22use thiserror::Error;
23use yash_env::Env;
24use yash_env::semantics::Field;
25use yash_env::source::pretty::Report;
26
27use super::Command;
28use super::Mode;
29
30/// Error in parsing command line arguments
31#[derive(Clone, Debug, Eq, Error, PartialEq)]
32#[non_exhaustive]
33pub enum Error {
34    /// An error occurred in the common parser.
35    #[error(transparent)]
36    CommonError(#[from] crate::common::syntax::ParseError<'static>),
37
38    /// The `-f` and `-v` options are used together.
39    #[error(transparent)]
40    ConflictingOption(#[from] ConflictingOptionError<'static>),
41    // TODO MissingOperand
42}
43
44impl Error {
45    /// Converts the error to a report.
46    pub fn to_report(&self) -> Report<'_> {
47        match self {
48            Error::CommonError(inner) => inner.to_report(),
49            Error::ConflictingOption(inner) => inner.to_report(),
50        }
51    }
52}
53
54impl<'a> From<&'a Error> for Report<'a> {
55    #[inline]
56    fn from(error: &'a Error) -> Self {
57        error.to_report()
58    }
59}
60
61/// Result of parsing command line arguments
62pub type Result = std::result::Result<Command, Error>;
63
64const OPTION_SPECS: &[OptionSpec] = &[
65    OptionSpec::new().short('f').long("functions"),
66    OptionSpec::new().short('v').long("variables"),
67];
68
69/// Parses command line arguments for the unset built-in.
70pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result {
71    let parser_mode = crate::common::syntax::Mode::with_env(env);
72    let (options, operands) = parse_arguments(OPTION_SPECS, parser_mode, args)?;
73
74    // Decide which to unset: variables or functions.
75    let f_option = options.iter().position(|o| o.spec.get_short() == Some('f'));
76    let v_option = options.iter().position(|o| o.spec.get_short() == Some('v'));
77    let mode = match (f_option, v_option) {
78        (None, None) => Mode::default(),
79        (None, Some(_)) => Mode::Variables,
80        (Some(_), None) => Mode::Functions,
81        (Some(f_pos), Some(v_pos)) => {
82            return Err(ConflictingOptionError::pick_from_indexes(options, [f_pos, v_pos]).into());
83        }
84    };
85
86    let names = operands;
87    Ok(Command { mode, names })
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use assert_matches::assert_matches;
94
95    #[test]
96    fn no_arguments_non_posix() {
97        let env = Env::new_virtual();
98        let result = parse(&env, vec![]);
99        assert_eq!(
100            result,
101            Ok(Command {
102                mode: Mode::Variables,
103                names: vec![],
104            })
105        );
106    }
107
108    // TODO no_arguments_posix: In the POSIXly-correct mode, the built-in
109    // requires at least one operand.
110
111    #[test]
112    fn v_option() {
113        let env = Env::new_virtual();
114        let result = parse(&env, Field::dummies(["-v"]));
115        assert_eq!(
116            result,
117            Ok(Command {
118                mode: Mode::Variables,
119                names: vec![],
120            })
121        );
122
123        // The same option can be specified multiple times.
124        let result = parse(&env, Field::dummies(["-vv", "--variables"]));
125        assert_eq!(
126            result,
127            Ok(Command {
128                mode: Mode::Variables,
129                names: vec![],
130            })
131        );
132    }
133
134    #[test]
135    fn f_option() {
136        let env = Env::new_virtual();
137        let result = parse(&env, Field::dummies(["-f"]));
138        assert_eq!(
139            result,
140            Ok(Command {
141                mode: Mode::Functions,
142                names: vec![],
143            })
144        );
145
146        // The same option can be specified multiple times.
147        let result = parse(&env, Field::dummies(["-ff", "--functions"]));
148        assert_eq!(
149            result,
150            Ok(Command {
151                mode: Mode::Functions,
152                names: vec![],
153            })
154        );
155    }
156
157    #[test]
158    fn v_and_f_option() {
159        // Specifying both -v and -f is an error.
160        let env = Env::new_virtual();
161        let args = Field::dummies(["-fv"]);
162        let result = parse(&env, args.clone());
163        assert_matches!(result, Err(Error::ConflictingOption(error)) => {
164            let short_options = error
165                .options()
166                .iter()
167                .map(|o| o.spec.get_short())
168                .collect::<Vec<_>>();
169            assert_eq!(short_options, [Some('f'), Some('v')], "{error:?}");
170        });
171    }
172
173    #[test]
174    fn operands() {
175        let env = Env::new_virtual();
176        let args = Field::dummies(["foo", "bar"]);
177        let result = parse(&env, args.clone());
178        assert_eq!(
179            result,
180            Ok(Command {
181                mode: Mode::Variables,
182                names: args,
183            })
184        );
185    }
186}