1use super::Command;
20use crate::common::syntax::{OptionOccurrence, OptionSpec};
21use itertools::Itertools;
22use thiserror::Error;
23use yash_env::semantics::Field;
24use yash_env::signal::RawNumber;
25use yash_env::source::pretty::{Footnote, FootnoteType, Report, ReportType, Snippet};
26use yash_env::system::Signals;
27use yash_env::trap::{Action, Condition};
28
29pub const OPTION_SPECS: &[OptionSpec] = &[OptionSpec::new().short('p').long("print")];
31
32#[derive(Clone, Debug, Error, Eq, PartialEq)]
34#[non_exhaustive]
35pub enum Error {
36 #[error("unknown condition: {0}")]
38 UnknownCondition(Field),
39
40 #[error("missing condition")]
42 MissingCondition { action: Field },
43}
44
45impl Error {
46 #[must_use]
48 pub fn to_report(&self) -> Report<'_> {
49 let mut report = Report::new();
50 report.r#type = ReportType::Error;
51 match self {
52 Self::UnknownCondition(field) => {
53 report.title = "unknown condition".into();
54 report.snippets = Snippet::with_primary_span(
55 &field.origin,
56 format!("unknown condition `{field}`").into(),
57 );
58 }
59 Self::MissingCondition { action } => {
60 report.title = "trap condition is missing".into();
61 report.snippets = Snippet::with_primary_span(
62 &action.origin,
63 "trap action specified without condition".into(),
64 );
65 report.footnotes.push(Footnote {
66 r#type: FootnoteType::Note,
67 label: format!(
68 "the first operand `{action}` was not regarded as a condition \
69 because it was not an unsigned integer"
70 )
71 .into(),
72 });
73 }
74 }
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
86fn parse_condition<S: Signals>(field: Field, system: &S) -> Result<(Condition, Field), Error> {
94 match field.value.parse::<RawNumber>() {
97 Ok(0) => Ok((Condition::Exit, field)),
98 Ok(number) => match system.to_signal_number(number) {
99 Some(number) => Ok((Condition::Signal(number), field)),
100 None => Err(Error::UnknownCondition(field)),
101 },
102 Err(_) if field.value == "EXIT" => Ok((Condition::Exit, field)),
103 Err(_) => match system.str2sig(&field.value) {
104 Some(number) => Ok((Condition::Signal(number), field)),
105 None => Err(Error::UnknownCondition(field)),
106 },
107 }
108}
109
110pub fn interpret<S: Signals>(
119 options: Vec<OptionOccurrence>,
120 operands: Vec<Field>,
121 system: &S,
122) -> Result<Command, Vec<Error>> {
123 let mut print = false;
124 let mut operands = operands.into_iter().peekable();
125
126 for option in options {
128 if option.spec.get_short() == Some('p') {
129 print = true;
130 }
131 }
132
133 let action_field = operands
135 .next_if(|field| !print && !is_non_negative_integer(&field.value))
136 .map(|field| {
137 let action = match field.value.as_str() {
138 "-" => Action::Default,
139 "" => Action::Ignore,
140 command => Action::Command(command.into()),
141 };
142 (action, field)
143 });
144
145 let (conditions, errors): (Vec<_>, Vec<_>) = operands
147 .map(|operand| parse_condition(operand, system))
148 .partition_result();
149
150 if !errors.is_empty() {
151 Err(errors)
152 } else if print {
153 if conditions.is_empty() {
154 Ok(Command::PrintAll {
155 include_default: true,
156 })
157 } else {
158 Ok(Command::Print { conditions })
159 }
160 } else {
161 match (conditions.is_empty(), action_field) {
162 (true, None) => Ok(Command::PrintAll {
163 include_default: false,
164 }),
165 (true, Some((_, action))) => Err(vec![Error::MissingCondition { action }]),
166 (false, action) => {
167 let action = action.map(|(action, _)| action).unwrap_or_default();
168 Ok(Command::SetAction { action, conditions })
169 }
170 }
171 }
172}
173
174fn is_non_negative_integer(s: &str) -> bool {
175 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::num::NonZero;
182 use yash_env::signal::Number;
183 use yash_env::source::Location;
184 use yash_env::system::r#virtual::VirtualSystem;
185
186 #[test]
187 fn parse_condition_exit_numeric() {
188 let system = VirtualSystem::new();
189 let field = Field::dummy("0");
190 let result = parse_condition(field.clone(), &system);
191 assert_eq!(result, Ok((Condition::Exit, field)));
192 }
193
194 #[test]
195 fn parse_condition_exit_named() {
196 let system = VirtualSystem::new();
197 let field = Field::dummy("EXIT");
198 let result = parse_condition(field.clone(), &system);
199 assert_eq!(result, Ok((Condition::Exit, field)));
200 }
201
202 #[test]
203 fn parse_condition_signal_by_name() {
204 let system = VirtualSystem::new();
205 let field = Field::dummy("INT");
206 let result = parse_condition(field.clone(), &system);
207 assert_eq!(
208 result,
209 Ok((Condition::Signal(VirtualSystem::SIGINT), field))
210 );
211 }
212
213 #[test]
214 fn parse_condition_signal_by_number() {
215 let system = VirtualSystem::new();
216 let field = Field::dummy("2");
217 let result = parse_condition(field.clone(), &system);
218 assert_eq!(
219 result,
220 Ok((Condition::Signal(VirtualSystem::SIGINT), field))
221 );
222 }
223
224 #[test]
225 fn parse_condition_unknown_name() {
226 let system = VirtualSystem::new();
227 let field = Field::dummy("FOOBAR");
228 let result = parse_condition(field.clone(), &system);
229 assert_eq!(result, Err(Error::UnknownCondition(field)));
230 }
231
232 #[test]
233 fn parse_condition_invalid_signal_number() {
234 let system = VirtualSystem::new();
235 let field = Field::dummy("9999999999");
236 let result = parse_condition(field.clone(), &system);
237 assert_eq!(result, Err(Error::UnknownCondition(field)));
238 }
239
240 #[test]
241 fn parse_condition_negative_number() {
242 let system = VirtualSystem::new();
243 let field = Field::dummy("-1");
244 let result = parse_condition(field.clone(), &system);
245 assert_eq!(result, Err(Error::UnknownCondition(field)));
246 }
247
248 #[test]
249 fn print_all_not_including_default() {
250 let system = VirtualSystem::new();
251 let result = interpret(vec![], vec![], &system);
252 assert_eq!(
253 result,
254 Ok(Command::PrintAll {
255 include_default: false
256 })
257 );
258 }
259
260 #[test]
261 fn print_all_including_default() {
262 let system = VirtualSystem::new();
263 let print = OptionOccurrence {
264 spec: &OptionSpec::new().short('p').long("print"),
265 location: Location::dummy("-p"),
266 argument: None,
267 };
268 let result = interpret(vec![print], vec![], &system);
269 assert_eq!(
270 result,
271 Ok(Command::PrintAll {
272 include_default: true
273 })
274 );
275 }
276
277 #[test]
278 fn print_one_condition() {
279 let system = VirtualSystem::new();
280 let print = OptionOccurrence {
281 spec: &OptionSpec::new().short('p').long("print"),
282 location: Location::dummy("-p"),
283 argument: None,
284 };
285 let result = interpret(vec![print], Field::dummies(["INT"]), &system);
286 assert_eq!(
287 result,
288 Ok(Command::Print {
289 conditions: vec![(
290 Condition::Signal(VirtualSystem::SIGINT),
291 Field::dummy("INT")
292 )]
293 })
294 )
295 }
296
297 #[test]
298 fn print_multiple_conditions() {
299 let system = VirtualSystem::new();
300 let print = OptionOccurrence {
301 spec: &OptionSpec::new().short('p').long("print"),
302 location: Location::dummy("-p"),
303 argument: None,
304 };
305 let result = interpret(
306 vec![print],
307 Field::dummies(["HUP", "EXIT", "QUIT"]),
308 &system,
309 );
310 assert_eq!(
311 result,
312 Ok(Command::Print {
313 conditions: vec![
314 (
315 Condition::Signal(VirtualSystem::SIGHUP),
316 Field::dummy("HUP")
317 ),
318 (Condition::Exit, Field::dummy("EXIT")),
319 (
320 Condition::Signal(VirtualSystem::SIGQUIT),
321 Field::dummy("QUIT")
322 ),
323 ]
324 })
325 )
326 }
327
328 #[test]
329 fn default_action_with_one_condition() {
330 let system = VirtualSystem::new();
331 let result = interpret(vec![], Field::dummies(["-", "INT"]), &system);
332 assert_eq!(
333 result,
334 Ok(Command::SetAction {
335 action: Action::Default,
336 conditions: vec![(
337 Condition::Signal(VirtualSystem::SIGINT),
338 Field::dummy("INT")
339 )]
340 })
341 );
342 }
343
344 #[test]
345 fn ignore_action() {
346 let system = VirtualSystem::new();
347 let result = interpret(vec![], Field::dummies(["", "INT"]), &system);
348 assert_eq!(
349 result,
350 Ok(Command::SetAction {
351 action: Action::Ignore,
352 conditions: vec![(
353 Condition::Signal(VirtualSystem::SIGINT),
354 Field::dummy("INT")
355 )]
356 })
357 );
358 }
359
360 #[test]
361 fn command_action() {
362 let system = VirtualSystem::new();
363 let result = interpret(vec![], Field::dummies(["echo", "INT"]), &system);
364 assert_eq!(
365 result,
366 Ok(Command::SetAction {
367 action: Action::Command("echo".into()),
368 conditions: vec![(
369 Condition::Signal(VirtualSystem::SIGINT),
370 Field::dummy("INT")
371 )]
372 })
373 );
374 }
375
376 #[test]
377 fn action_with_multiple_conditions() {
378 let system = VirtualSystem::new();
379 let result = interpret(vec![], Field::dummies(["-", "HUP", "2", "TERM"]), &system);
380 assert_eq!(
381 result,
382 Ok(Command::SetAction {
383 action: Action::Default,
384 conditions: vec![
385 (
386 Condition::Signal(VirtualSystem::SIGHUP),
387 Field::dummy("HUP")
388 ),
389 (
390 Condition::Signal(Number::from_raw_unchecked(NonZero::new(2).unwrap())),
391 Field::dummy("2")
392 ),
393 (
394 Condition::Signal(VirtualSystem::SIGTERM),
395 Field::dummy("TERM")
396 ),
397 ]
398 })
399 );
400 }
401
402 #[test]
403 fn action_with_different_signal_name_conditions() {
404 let system = VirtualSystem::new();
405 let result = interpret(vec![], Field::dummies(["", "HUP"]), &system);
406 assert_eq!(
407 result,
408 Ok(Command::SetAction {
409 action: Action::Ignore,
410 conditions: vec![(
411 Condition::Signal(VirtualSystem::SIGHUP),
412 Field::dummy("HUP")
413 )]
414 })
415 );
416
417 let result = interpret(vec![], Field::dummies(["", "QUIT"]), &system);
418 assert_eq!(
419 result,
420 Ok(Command::SetAction {
421 action: Action::Ignore,
422 conditions: vec![(
423 Condition::Signal(VirtualSystem::SIGQUIT),
424 Field::dummy("QUIT")
425 )]
426 })
427 );
428 }
429
430 #[test]
431 fn action_with_signal_number_condition() {
432 let system = VirtualSystem::new();
433 let result = interpret(vec![], Field::dummies(["-", "1"]), &system);
434 assert_eq!(
435 result,
436 Ok(Command::SetAction {
437 action: Action::Default,
438 conditions: vec![(
439 Condition::Signal(Number::from_raw_unchecked(NonZero::new(1).unwrap())),
440 Field::dummy("1")
441 )]
442 })
443 );
444 }
445
446 #[test]
447 fn action_with_named_exit_condition() {
448 let system = VirtualSystem::new();
449 let result = interpret(vec![], Field::dummies(["-", "EXIT"]), &system);
450 assert_eq!(
451 result,
452 Ok(Command::SetAction {
453 action: Action::Default,
454 conditions: vec![(Condition::Exit, Field::dummy("EXIT"))]
455 })
456 );
457 }
458
459 #[test]
460 fn action_with_numeric_exit_condition() {
461 let system = VirtualSystem::new();
462 let result = interpret(vec![], Field::dummies(["-", "0"]), &system);
463 assert_eq!(
464 result,
465 Ok(Command::SetAction {
466 action: Action::Default,
467 conditions: vec![(Condition::Exit, Field::dummy("0"))]
468 })
469 );
470 }
471
472 #[test]
473 fn action_with_unknown_conditions() {
474 let system = VirtualSystem::new();
475 let result = interpret(
476 vec![],
477 Field::dummies(["-", "FOOBAR", "INT", "9999999999"]),
478 &system,
479 );
480 assert_eq!(
481 result,
482 Err(vec![
483 Error::UnknownCondition(Field::dummy("FOOBAR")),
484 Error::UnknownCondition(Field::dummy("9999999999")),
485 ])
486 );
487 }
488
489 #[test]
490 fn signal_number_condition_without_action() {
491 let system = VirtualSystem::new();
492 let result = interpret(vec![], Field::dummies(["1"]), &system);
493 assert_eq!(
494 result,
495 Ok(Command::SetAction {
496 action: Action::Default,
497 conditions: vec![(
498 Condition::Signal(Number::from_raw_unchecked(NonZero::new(1).unwrap())),
499 Field::dummy("1")
500 )]
501 })
502 );
503 }
504
505 #[test]
506 fn numeric_exit_condition_without_action() {
507 let system = VirtualSystem::new();
508 let result = interpret(vec![], Field::dummies(["0"]), &system);
509 assert_eq!(
510 result,
511 Ok(Command::SetAction {
512 action: Action::Default,
513 conditions: vec![(Condition::Exit, Field::dummy("0"))]
514 })
515 );
516 }
517
518 #[test]
519 fn action_that_looks_like_negative_number() {
520 let system = VirtualSystem::new();
521 let result = interpret(vec![], Field::dummies(["-1", "0"]), &system);
522 assert_eq!(
523 result,
524 Ok(Command::SetAction {
525 action: Action::Command("-1".into()),
526 conditions: vec![(Condition::Exit, Field::dummy("0"))]
527 })
528 );
529 }
530
531 #[test]
532 fn missing_condition() {
533 let system = VirtualSystem::new();
534 let result = interpret(vec![], Field::dummies(["echo"]), &system);
535 assert_eq!(
536 result,
537 Err(vec![Error::MissingCondition {
538 action: Field::dummy("echo")
539 }])
540 );
541 }
542}