yash_builtin/wait/
syntax.rs1use 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#[derive(Clone, Debug, Eq, Error, PartialEq)]
31pub enum Error {
32 #[error(transparent)]
34 CommonError(#[from] ParseError<'static>),
35
36 #[error("{0}: {1}")]
38 ParseInt(Field, ParseIntError),
39
40 #[error("{0}: non-positive process ID")]
42 NonPositive(Field),
43}
44
45impl Error {
46 #[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
86pub 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}