Skip to main content

yash_builtin/trap/
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 trap built-in
18
19use 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
29/// Command line options for the trap built-in
30pub const OPTION_SPECS: &[OptionSpec] = &[OptionSpec::new().short('p').long("print")];
31
32/// Error that may occur while [interpreting](interpret) command line arguments.
33#[derive(Clone, Debug, Error, Eq, PartialEq)]
34#[non_exhaustive]
35pub enum Error {
36    /// The specified condition is not supported.
37    #[error("unknown condition: {0}")]
38    UnknownCondition(Field),
39
40    /// An action is specified but no condition is specified.
41    #[error("missing condition")]
42    MissingCondition { action: Field },
43}
44
45impl Error {
46    /// Converts the error to a report.
47    #[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
86/// Parses a single condition from a command line operand.
87///
88/// On success, returns the parsed `Condition` and the original `Field`.
89/// On failure, returns `Error::UnknownCondition`.
90///
91/// A condition can be `0` or `EXIT` for [`Condition::Exit`], or a signal
92/// name/number for [`Condition::Signal`].
93fn parse_condition<S: Signals>(field: Field, system: &S) -> Result<(Condition, Field), Error> {
94    // TODO Case-insensitive parse
95    // TODO Allow SIG prefix
96    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
110/// Converts parsed command line arguments into a `Command`.
111///
112/// The result of [`parse_arguments`](crate::common::syntax::parse_arguments)
113/// should be passed to this function.
114///
115/// On failure, returns a non-empty list of errors.
116///
117/// If a given option occurrence is not recognized, it is ignored.
118pub 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    // Parse options
127    for option in options {
128        if option.spec.get_short() == Some('p') {
129            print = true;
130        }
131    }
132
133    // Parse the first operand as an action
134    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    // Parse the remaining operands as conditions
146    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}