Skip to main content

yash_builtin/read/
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 read built-in
18
19use super::Command;
20use crate::common::syntax::Mode;
21use crate::common::syntax::OptionArgumentSpec;
22use crate::common::syntax::OptionSpec;
23use crate::common::syntax::parse_arguments;
24use thiserror::Error;
25use yash_env::Env;
26use yash_env::semantics::Field;
27use yash_env::source::pretty::Snippet;
28use yash_env::source::pretty::{Report, ReportType};
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 delimiter specified by the `-d` option is multibyte.
39    #[error("multibyte delimiter is not supported")]
40    MultibyteDelimiter { delimiter: Field },
41
42    /// No operand is given.
43    #[error("missing operand")]
44    MissingOperand,
45
46    /// An operand is not a valid variable name.
47    #[error("invalid variable name")]
48    InvalidVariableName { name: Field },
49}
50
51impl Error {
52    /// Converts this error to a report.
53    #[must_use]
54    pub fn to_report(&self) -> Report<'_> {
55        let snippets = match self {
56            Self::CommonError(parse_error) => return parse_error.to_report(),
57
58            Self::MultibyteDelimiter { delimiter } => Snippet::with_primary_span(
59                &delimiter.origin,
60                format!(
61                    "delimiter {:?} is {}-byte long",
62                    delimiter.value,
63                    delimiter.value.len()
64                )
65                .into(),
66            ),
67
68            Self::MissingOperand => vec![],
69
70            Self::InvalidVariableName { name } => Snippet::with_primary_span(
71                &name.origin,
72                format!("variable name {name:?} is not valid").into(),
73            ),
74        };
75
76        let mut report = Report::new();
77        report.r#type = ReportType::Error;
78        report.title = self.to_string().into();
79        report.snippets = snippets;
80        report
81    }
82}
83
84impl<'a> From<&'a Error> for Report<'a> {
85    #[inline]
86    fn from(error: &'a Error) -> Self {
87        error.to_report()
88    }
89}
90
91const OPTION_SPECS: &[OptionSpec] = &[
92    OptionSpec::new()
93        .short('d')
94        .long("delimiter")
95        .argument(OptionArgumentSpec::Required),
96    OptionSpec::new().short('r').long("raw-mode"),
97];
98
99/// Parses command line arguments.
100pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
101    let mode = Mode::with_env(env);
102    let (options, operands) = parse_arguments(OPTION_SPECS, mode, args)?;
103
104    // Parse options
105    let mut delimiter = b'\n';
106    let mut is_raw = false;
107    for option in options {
108        match option.spec.get_short() {
109            Some('d') => {
110                let arg = option.argument.unwrap();
111                match arg.value.len() {
112                    0 => delimiter = b'\0',
113                    1 => delimiter = arg.value.as_bytes()[0],
114                    _ => return Err(Error::MultibyteDelimiter { delimiter: arg }),
115                }
116            }
117            Some('r') => is_raw = true,
118            _ => unreachable!(),
119        }
120    }
121
122    // Parse operands
123    let mut variables = validate_names(operands)?;
124    let last_variable = variables.pop().ok_or(Error::MissingOperand)?;
125
126    Ok(Command {
127        delimiter,
128        is_raw,
129        variables,
130        last_variable,
131    })
132}
133
134/// Tests if all the variable names are valid.
135///
136/// If all the variable names are valid, this function returns `names` as is.
137/// Otherwise, this function returns an `Error::InvalidVariableName`.
138fn validate_names(names: Vec<Field>) -> Result<Vec<Field>, Error> {
139    match names.iter().position(|name| name.value.contains('=')) {
140        None => Ok(names),
141        Some(i) => Err(Error::InvalidVariableName {
142            name: { names }.swap_remove(i),
143        }),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn one_operand() {
153        let env = Env::new_virtual();
154        assert_eq!(
155            parse(&env, Field::dummies(["var"])),
156            Ok(Command {
157                delimiter: b'\n',
158                is_raw: false,
159                variables: vec![],
160                last_variable: Field::dummy("var"),
161            })
162        );
163    }
164
165    #[test]
166    fn raw_mode() {
167        let env = Env::new_virtual();
168        assert_eq!(
169            parse(&env, Field::dummies(["-r", "var"])),
170            Ok(Command {
171                delimiter: b'\n',
172                is_raw: true,
173                variables: vec![],
174                last_variable: Field::dummy("var"),
175            })
176        );
177    }
178
179    #[test]
180    fn nul_delimiter() {
181        let env = Env::new_virtual();
182        assert_eq!(
183            parse(&env, Field::dummies(["-d", "", "var"])),
184            Ok(Command {
185                delimiter: b'\0',
186                is_raw: false,
187                variables: vec![],
188                last_variable: Field::dummy("var"),
189            })
190        );
191    }
192
193    #[test]
194    fn non_default_non_nul_delimiter() {
195        let env = Env::new_virtual();
196        assert_eq!(
197            parse(&env, Field::dummies(["-d", ":", "var"])),
198            Ok(Command {
199                delimiter: b':',
200                is_raw: false,
201                variables: vec![],
202                last_variable: Field::dummy("var"),
203            })
204        );
205    }
206
207    #[test]
208    fn multibyte_delimiter_is_not_supported() {
209        let env = Env::new_virtual();
210        assert_eq!(
211            parse(&env, Field::dummies(["-d", "!?", "var"])),
212            Err(Error::MultibyteDelimiter {
213                delimiter: Field::dummy("!?")
214            })
215        );
216
217        assert_eq!(
218            parse(&env, Field::dummies(["-d", "あ", "var"])),
219            Err(Error::MultibyteDelimiter {
220                delimiter: Field::dummy("あ")
221            })
222        );
223    }
224
225    #[test]
226    fn many_operands() {
227        let env = Env::new_virtual();
228        assert_eq!(
229            parse(&env, Field::dummies(["foo", "bar"])),
230            Ok(Command {
231                delimiter: b'\n',
232                is_raw: false,
233                variables: Field::dummies(["foo"]),
234                last_variable: Field::dummy("bar"),
235            })
236        );
237
238        assert_eq!(
239            parse(&env, Field::dummies(["first", "second", "third"])),
240            Ok(Command {
241                delimiter: b'\n',
242                is_raw: false,
243                variables: Field::dummies(["first", "second"]),
244                last_variable: Field::dummy("third"),
245            })
246        );
247    }
248
249    #[test]
250    fn missing_operand() {
251        let env = Env::new_virtual();
252        assert_eq!(parse(&env, vec![]), Err(Error::MissingOperand));
253    }
254
255    #[test]
256    fn operand_containing_equal() {
257        let env = Env::new_virtual();
258        assert_eq!(
259            parse(&env, Field::dummies(["="])),
260            Err(Error::InvalidVariableName {
261                name: Field::dummy("=")
262            })
263        );
264        assert_eq!(
265            parse(&env, Field::dummies(["foo", "bar=bar", "baz"])),
266            Err(Error::InvalidVariableName {
267                name: Field::dummy("bar=bar")
268            })
269        );
270    }
271}