Skip to main content

yash_builtin/unalias/
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//! Command line argument parsing for the `unalias` built-in
18
19use super::Command;
20use crate::common::syntax::Mode;
21use crate::common::syntax::OptionSpec;
22use crate::common::syntax::parse_arguments;
23use thiserror::Error;
24use yash_env::Env;
25use yash_env::semantics::Field;
26use yash_env::source::Location;
27use yash_env::source::pretty::{Report, ReportType, Snippet, Span, SpanRole, add_span};
28
29/// List of all options supported by the `unalias` built-in
30pub const OPTION_SPECS: &[OptionSpec] = &[OptionSpec::new().short('a')];
31
32/// Errors that can occur while parsing command line arguments
33#[derive(Clone, Debug, Eq, Error, PartialEq)]
34pub enum Error {
35    /// An error occurred while parsing arguments.
36    #[error(transparent)]
37    CommonError(#[from] crate::common::syntax::ParseError<'static>),
38
39    /// The `-a` option was specified with other operands.
40    #[error("`-a` cannot be used with operands")]
41    ConflictingOptionAndOperand {
42        option_location: Location,
43        operand_location: Location,
44    },
45
46    /// No option or operand was specified.
47    #[error("no option or operand specified")]
48    MissingArgument,
49}
50
51impl Error {
52    /// Converts the error into a [`Report`].
53    #[must_use]
54    pub fn to_report(&self) -> Report<'_> {
55        let (title, snippets) = match self {
56            Self::CommonError(e) => return e.to_report(),
57
58            Self::ConflictingOptionAndOperand {
59                option_location,
60                operand_location,
61            } => ("`-a` cannot be used with operands", {
62                let mut snippets =
63                    Snippet::with_primary_span(option_location, "`-a` specified here".into());
64                add_span(
65                    &operand_location.code,
66                    Span {
67                        range: operand_location.byte_range(),
68                        role: SpanRole::Primary {
69                            label: "operand specified here".into(),
70                        },
71                    },
72                    &mut snippets,
73                );
74                snippets
75            }),
76
77            Self::MissingArgument => ("no option or operand specified", vec![]),
78        };
79
80        let mut report = Report::new();
81        report.r#type = ReportType::Error;
82        report.title = title.into();
83        report.snippets = snippets;
84        report
85    }
86}
87
88impl<'a> From<&'a Error> for Report<'a> {
89    #[inline]
90    fn from(error: &'a Error) -> Self {
91        error.to_report()
92    }
93}
94
95/// Parses command line arguments for the `unalias` built-in.
96pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
97    let mode = Mode::with_env(env);
98    let (mut options, operands) = parse_arguments(OPTION_SPECS, mode, args)?;
99
100    for option in &options {
101        debug_assert_eq!(option.spec.get_short(), Some('a'));
102    }
103
104    match (options.pop(), operands.is_empty()) {
105        (None, true) => Err(Error::MissingArgument),
106        (None, false) => Ok(Command::Remove(operands)),
107        (Some(_), true) => Ok(Command::RemoveAll),
108        (Some(option), false) => Err(Error::ConflictingOptionAndOperand {
109            option_location: option.location,
110            operand_location: { operands }.swap_remove(0).origin,
111        }),
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn all_option() {
121        let env = Env::new_virtual();
122        let result = parse(&env, Field::dummies(["-a"]));
123        assert_eq!(result, Ok(Command::RemoveAll));
124    }
125
126    #[test]
127    fn operands() {
128        let env = Env::new_virtual();
129        let operands = Field::dummies(["foo", "bar"]);
130        let result = parse(&env, operands.clone());
131        assert_eq!(result, Ok(Command::Remove(operands)));
132    }
133
134    #[test]
135    fn missing_arguments() {
136        let env = Env::new_virtual();
137        let result = parse(&env, vec![]);
138        assert_eq!(result, Err(Error::MissingArgument));
139    }
140
141    #[test]
142    fn option_and_operand() {
143        let env = Env::new_virtual();
144        let args = Field::dummies(["-a", "foo"]);
145        let result = parse(&env, args);
146        assert_eq!(
147            result,
148            Err(Error::ConflictingOptionAndOperand {
149                option_location: Location::dummy("-a"),
150                operand_location: Location::dummy("foo"),
151            }),
152        );
153    }
154}