Skip to main content

yash_builtin/wait/
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 wait built-in
18
19use super::{Command, JobSpec};
20use std::num::ParseIntError;
21use thiserror::Error;
22use yash_env::Env;
23use yash_env::job::Pid;
24use yash_env::semantics::Field;
25use yash_env::source::pretty::{Report, ReportType, Snippet};
26
27use crate::common::syntax::{Mode, ParseError, parse_arguments};
28
29/// Errors that may occur while parsing command line arguments
30#[derive(Clone, Debug, Eq, Error, PartialEq)]
31pub enum Error {
32    /// Error in generic argument parsing
33    #[error(transparent)]
34    CommonError(#[from] ParseError<'static>),
35
36    /// An operand does not start with `%` and is not a decimal integer.
37    #[error("{0}: {1}")]
38    ParseInt(Field, ParseIntError),
39
40    /// An operand is a negative decimal integer.
41    #[error("{0}: non-positive process ID")]
42    NonPositive(Field),
43}
44
45impl Error {
46    /// Converts the error to a report.
47    #[must_use]
48    pub fn to_report(&self) -> Report<'_> {
49        let (title, snippets) = match self {
50            Self::CommonError(e) => return e.to_report(),
51            Self::ParseInt(field, _) | Self::NonPositive(field) => (
52                "invalid job specification".into(),
53                Snippet::with_primary_span(&field.origin, self.to_string().into()),
54            ),
55        };
56        let mut report = Report::new();
57        report.r#type = ReportType::Error;
58        report.title = title;
59        report.snippets = snippets;
60        report
61    }
62}
63
64impl<'a> From<&'a Error> for Report<'a> {
65    #[inline]
66    fn from(error: &'a Error) -> Self {
67        error.to_report()
68    }
69}
70
71impl TryFrom<Field> for JobSpec {
72    type Error = Error;
73
74    fn try_from(field: Field) -> Result<Self, Error> {
75        if field.value.starts_with('%') {
76            return Ok(Self::JobId(field));
77        }
78        match field.value.parse() {
79            Ok(int) if int >= 0 => Ok(Self::ProcessId(Pid(int))),
80            Ok(_) => Err(Error::NonPositive(field)),
81            Err(error) => Err(Error::ParseInt(field, error)),
82        }
83    }
84}
85
86/// Parses command line arguments for the wait built-in.
87pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
88    let (_, operands) = parse_arguments(&[], Mode::with_env(env), args)?;
89    let jobs = operands
90        .into_iter()
91        .map(JobSpec::try_from)
92        .collect::<Result<Vec<JobSpec>, Error>>()?;
93    Ok(Command { jobs })
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use assert_matches::assert_matches;
100    use std::num::IntErrorKind;
101
102    #[test]
103    fn non_negative_process_ids() {
104        let result = JobSpec::try_from(Field::dummy("123"));
105        assert_eq!(result, Ok(JobSpec::ProcessId(Pid(123))));
106
107        let result = JobSpec::try_from(Field::dummy("0"));
108        assert_eq!(result, Ok(JobSpec::ProcessId(Pid(0))));
109    }
110
111    #[test]
112    fn negative_process_ids() {
113        let result = JobSpec::try_from(Field::dummy("-1"));
114        assert_eq!(result, Err(Error::NonPositive(Field::dummy("-1"))));
115
116        let result = JobSpec::try_from(Field::dummy("-121"));
117        assert_eq!(result, Err(Error::NonPositive(Field::dummy("-121"))));
118    }
119
120    #[test]
121    fn unparsable_process_ids() {
122        let result = JobSpec::try_from(Field::dummy("abc"));
123        assert_matches!(result, Err(Error::ParseInt(field, error)) => {
124            assert_eq!(field, Field::dummy("abc"));
125            assert_eq!(error.kind(), &IntErrorKind::InvalidDigit);
126        });
127
128        let result = JobSpec::try_from(Field::dummy(""));
129        assert_matches!(result, Err(Error::ParseInt(field, error)) => {
130            assert_eq!(field, Field::dummy(""));
131            assert_eq!(error.kind(), &IntErrorKind::Empty);
132        });
133    }
134
135    #[test]
136    fn job_ids() {
137        let result = JobSpec::try_from(Field::dummy("%abc"));
138        assert_eq!(result, Ok(JobSpec::JobId(Field::dummy("%abc"))));
139
140        let result = JobSpec::try_from(Field::dummy("%"));
141        assert_eq!(result, Ok(JobSpec::JobId(Field::dummy("%"))));
142    }
143}