yash_builtin/cd/
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 parser for the cd built-in
18
19use super::Command;
20use super::Mode;
21use crate::common::syntax::OptionSpec;
22use crate::common::syntax::parse_arguments;
23use std::collections::VecDeque;
24use thiserror::Error;
25use yash_env::Env;
26use yash_env::semantics::Field;
27use yash_env::source::Location;
28use yash_env::source::pretty::{Report, ReportType, Snippet};
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 `-e` option is used without the `-P` option.
39    ///
40    /// The `Location` indicates the argument containing the `-e` option.
41    #[error("-e option must be used with -P (and not -L)")]
42    EnsurePwdNotPhysical(Location),
43
44    /// The operand is an empty string.
45    #[error("empty operand")]
46    EmptyOperand(Field),
47
48    /// More than one operand is given.
49    ///
50    /// The `Vec` contains the extra operands.
51    #[error("unexpected operand")]
52    UnexpectedOperands(Vec<Field>),
53}
54
55impl Error {
56    /// Converts this error to a [`Report`].
57    #[must_use]
58    pub fn to_report(&self) -> Report<'_> {
59        let (location, label) = match self {
60            Self::CommonError(e) => return e.to_report(),
61            Self::EnsurePwdNotPhysical(location) => {
62                (location, "-e option must be used with -P".into())
63            }
64            Self::EmptyOperand(operand) => (&operand.origin, "empty operand".into()),
65            Self::UnexpectedOperands(operands) => (
66                &operands[0].origin,
67                format!("{}: unexpected operand", operands[0].value).into(),
68            ),
69        };
70
71        let mut report = Report::new();
72        report.r#type = ReportType::Error;
73        report.title = self.to_string().into();
74        report.snippets = Snippet::with_primary_span(location, label);
75        report
76    }
77}
78
79impl<'a> From<&'a Error> for Report<'a> {
80    #[inline]
81    fn from(error: &'a Error) -> Self {
82        error.to_report()
83    }
84}
85
86/// Result of parsing command line arguments
87pub type Result = std::result::Result<Command, Error>;
88
89const OPTION_SPECS: &[OptionSpec] = &[
90    OptionSpec::new().short('e').long("ensure-pwd"),
91    OptionSpec::new().short('L').long("logical"),
92    OptionSpec::new().short('P').long("physical"),
93];
94
95/// Parses command line arguments for the cd built-in.
96pub fn parse(env: &Env, args: Vec<Field>) -> Result {
97    let parser_mode = crate::common::syntax::Mode::with_env(env);
98    let (options, operands) = parse_arguments(OPTION_SPECS, parser_mode, args)?;
99
100    let mut ensure_pwd_option_location = None;
101    let mut mode = Mode::default();
102    for option in options {
103        match option.spec.get_short() {
104            Some('e') => ensure_pwd_option_location = Some(option.location),
105            Some('L') => mode = Mode::Logical,
106            Some('P') => mode = Mode::Physical,
107            _ => unreachable!(),
108        }
109    }
110
111    let ensure_pwd = match (ensure_pwd_option_location, mode) {
112        (Some(_), Mode::Physical) => true,
113        (Some(location), _) => return Err(Error::EnsurePwdNotPhysical(location)),
114        (None, _) => false,
115    };
116
117    let mut operands = VecDeque::from(operands);
118    let operand = operands.pop_front();
119    if !operands.is_empty() {
120        return Err(Error::UnexpectedOperands(operands.into()));
121    }
122
123    let operand = match operand {
124        Some(operand) if operand.value.is_empty() => return Err(Error::EmptyOperand(operand)),
125        operand => operand,
126    };
127    Ok(Command {
128        mode,
129        ensure_pwd,
130        operand,
131    })
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn no_arguments() {
140        let env = Env::new_virtual();
141        let result = parse(&env, vec![]);
142        assert_eq!(
143            result,
144            Ok(Command {
145                mode: Mode::Logical,
146                ensure_pwd: false,
147                operand: None,
148            })
149        );
150    }
151
152    #[test]
153    fn logical_option() {
154        let env = Env::new_virtual();
155        let result = parse(&env, Field::dummies(["-L"]));
156        assert_eq!(
157            result,
158            Ok(Command {
159                mode: Mode::Logical,
160                ensure_pwd: false,
161                operand: None,
162            })
163        );
164    }
165
166    #[test]
167    fn physical_option() {
168        let env = Env::new_virtual();
169        let result = parse(&env, Field::dummies(["-P"]));
170        assert_eq!(
171            result,
172            Ok(Command {
173                mode: Mode::Physical,
174                ensure_pwd: false,
175                operand: None,
176            })
177        );
178    }
179
180    #[test]
181    fn last_option_wins() {
182        let env = Env::new_virtual();
183
184        let result = parse(&env, Field::dummies(["-L", "-P"]));
185        assert_eq!(result.unwrap().mode, Mode::Physical);
186
187        let result = parse(&env, Field::dummies(["-P", "-L"]));
188        assert_eq!(result.unwrap().mode, Mode::Logical);
189
190        let result = parse(&env, Field::dummies(["-L", "-P", "-L"]));
191        assert_eq!(result.unwrap().mode, Mode::Logical);
192
193        let result = parse(&env, Field::dummies(["-PLP"]));
194        assert_eq!(result.unwrap().mode, Mode::Physical);
195    }
196
197    #[test]
198    fn ensure_pwd_option_with_physical_option() {
199        let env = Env::new_virtual();
200
201        let result = parse(&env, Field::dummies(["-e", "-P"]));
202        assert!(result.unwrap().ensure_pwd);
203
204        let result = parse(&env, Field::dummies(["-P", "-e"]));
205        assert!(result.unwrap().ensure_pwd);
206
207        let result = parse(&env, Field::dummies(["-eLP"]));
208        assert!(result.unwrap().ensure_pwd);
209    }
210
211    #[test]
212    fn with_operand() {
213        let env = Env::new_virtual();
214        let operand = Field::dummy("foo/bar");
215        let result = parse(&env, vec![operand.clone()]);
216        assert_eq!(
217            result,
218            Ok(Command {
219                mode: Mode::default(),
220                ensure_pwd: false,
221                operand: Some(operand),
222            })
223        );
224    }
225
226    #[test]
227    fn option_and_operand() {
228        let env = Env::new_virtual();
229        let operand = Field::dummy("foo/bar");
230        let args = vec![Field::dummy("-L"), Field::dummy("--"), operand.clone()];
231        let result = parse(&env, args);
232        assert_eq!(
233            result,
234            Ok(Command {
235                mode: Mode::Logical,
236                ensure_pwd: false,
237                operand: Some(operand),
238            })
239        );
240    }
241
242    #[test]
243    fn ensure_pwd_option_with_logical_option() {
244        let env = Env::new_virtual();
245        let e = Field::dummy("-e");
246
247        let result = parse(&env, vec![Field::dummy("-L"), e.clone()]);
248        assert_eq!(result, Err(Error::EnsurePwdNotPhysical(e.origin.clone())));
249
250        let result = parse(&env, vec![e.clone()]);
251        assert_eq!(result, Err(Error::EnsurePwdNotPhysical(e.origin)));
252    }
253
254    #[test]
255    fn empty_operand() {
256        let env = Env::new_virtual();
257        let operand = Field::dummy("");
258        let result = parse(&env, vec![operand.clone()]);
259        assert_eq!(result, Err(Error::EmptyOperand(operand)));
260    }
261
262    #[test]
263    fn unexpected_operand() {
264        let env = Env::new_virtual();
265        let operand1 = Field::dummy("foo");
266        let operand2 = Field::dummy("bar");
267        let result = parse(&env, vec![operand1, operand2.clone()]);
268        assert_eq!(result, Err(Error::UnexpectedOperands(vec![operand2])));
269    }
270
271    #[test]
272    fn unexpected_operands_after_options() {
273        let env = Env::new_virtual();
274        let args = Field::dummies(["-LP", "-L", "--", "one", "two", "three"]);
275        let extra_operands = args[4..].to_vec();
276        let result = parse(&env, args);
277        assert_eq!(result, Err(Error::UnexpectedOperands(extra_operands)));
278    }
279}