Skip to main content

yash_builtin/
trap.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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//! Trap built-in.
18//!
19//! This module implements the [`trap` built-in], which sets or prints traps.
20//!
21//! [`trap` built-in]: https://magicant.github.io/yash-rs/builtins/trap.html
22//!
23//! # Implementation notes
24//!
25//! The [`TrapSet`] remembers the traps that were configured in the parent shell
26//! so that the built-in can print them when invoked in a subshell. Those traps
27//! are cleared when the built-in modifies any trap in the subshell. See
28//! [`TrapSet::enter_subshell`] and [`TrapSet::set_action`] for details.
29
30use crate::common::report::merge_reports;
31use crate::common::report::report_error;
32use crate::common::report::report_failure;
33use crate::common::syntax::Mode;
34use crate::common::syntax::parse_arguments;
35use thiserror::Error;
36use yash_env::Env;
37use yash_env::option::Option::Interactive;
38use yash_env::option::State::On;
39use yash_env::semantics::ExitStatus;
40use yash_env::semantics::Field;
41use yash_env::source::pretty::{Report, ReportType, Snippet};
42use yash_env::system::{Fcntl, Isatty, Sigaction, Sigmask, Signals, Write};
43use yash_env::trap::Action;
44use yash_env::trap::Condition;
45use yash_env::trap::SetActionError;
46use yash_env::trap::SignalSystem;
47use yash_env::trap::TrapSet;
48use yash_quote::quoted;
49
50/// Interpretation of command line arguments that selects the behavior of the
51/// `trap` built-in
52#[derive(Clone, Debug, Eq, PartialEq)]
53#[non_exhaustive]
54pub enum Command {
55    /// Print all traps
56    PrintAll {
57        /// If true, print all traps including ones with the default action
58        include_default: bool,
59    },
60
61    /// Print traps for one or more conditions
62    Print {
63        /// The conditions for which to print traps
64        conditions: Vec<(Condition, Field)>,
65    },
66
67    /// Set an action for one or more conditions
68    SetAction {
69        /// The action to set
70        action: Action,
71        /// The conditions for which the action should be set
72        conditions: Vec<(Condition, Field)>,
73    },
74}
75
76pub mod syntax;
77
78/// Displays the current trap for a condition.
79///
80/// If the trap is not set and `include_default` is `false`, this function
81/// does nothing. Otherwise, it prints the trap in the format `trap -- command
82/// condition`. The result is written to `output`.
83fn display_trap<S: SignalSystem, W: std::fmt::Write>(
84    traps: &mut TrapSet,
85    system: &S,
86    cond: Condition,
87    include_default: bool,
88    output: &mut W,
89) -> Result<(), std::fmt::Error> {
90    let Ok(trap) = traps.peek_state(system, cond) else {
91        return Ok(());
92    };
93    let command = match &trap.action {
94        Action::Default if include_default => "-",
95        Action::Default => return Ok(()),
96        Action::Ignore => "",
97        Action::Command(command) => command,
98    };
99    let cond = cond.to_string(system);
100    writeln!(output, "trap -- {} {}", quoted(command), cond)
101}
102
103/// Returns a string that represents the currently configured traps.
104///
105/// The returned string is the whole output of the `trap` built-in
106/// used without options or operands, including the trailing newline.
107///
108/// This function is equivalent to [`display_all_traps`] with `include_default`
109/// set to `false`.
110#[must_use]
111pub fn display_traps<S: SignalSystem>(traps: &mut TrapSet, system: &S) -> String {
112    display_all_traps(traps, system, false)
113}
114
115/// Returns a string that represents the currently configured traps.
116///
117/// The returned string is the whole output of the `trap` built-in
118/// used without operands, including the trailing newline.
119///
120/// If `include_default` is `true`, the output includes traps with the default
121/// action. Otherwise, the output includes only traps with non-default actions.
122#[must_use]
123pub fn display_all_traps<S: SignalSystem>(
124    traps: &mut TrapSet,
125    system: &S,
126    include_default: bool,
127) -> String {
128    let mut output = String::new();
129    for cond in Condition::iter(system) {
130        if let Condition::Signal(number) = cond {
131            if number == S::SIGKILL || number == S::SIGSTOP {
132                continue;
133            }
134        }
135        display_trap(traps, system, cond, include_default, &mut output).unwrap()
136    }
137    output
138}
139
140/// Cause of an error that may occur while executing the `trap` built-in
141#[derive(Clone, Debug, Eq, Error, PartialEq)]
142#[non_exhaustive]
143pub enum ErrorCause {
144    /// The specified condition is not supported.
145    ///
146    /// **Note:** Unsupported signals are now detected in [`syntax::interpret`],
147    /// so this error cause no longer occurs.
148    #[error("signal not supported on this system")]
149    UnsupportedSignal,
150    /// An error occurred while [setting a trap](TrapSet::set_action).
151    #[error(transparent)]
152    SetAction(#[from] SetActionError),
153}
154
155/// Information of an error that occurred while executing the `trap` built-in
156#[derive(Clone, Debug, Eq, Error, PartialEq)]
157pub struct Error {
158    /// The cause of the error
159    pub cause: ErrorCause,
160    /// The condition on which the error occurred
161    pub cond: Condition,
162    /// The field that specifies the condition
163    pub field: Field,
164}
165
166impl std::fmt::Display for Error {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        self.cause.fmt(f)
169    }
170}
171
172impl Error {
173    /// Converts this error to a [`Report`].
174    #[must_use]
175    pub fn to_report(&self) -> Report<'_> {
176        let mut report = Report::new();
177        report.r#type = ReportType::Error;
178        report.title = match &self.cause {
179            ErrorCause::UnsupportedSignal => "invalid trap condition".into(),
180            ErrorCause::SetAction(_) => "cannot update trap".into(),
181        };
182        report.snippets =
183            Snippet::with_primary_span(&self.field.origin, self.cause.to_string().into());
184        report
185    }
186}
187
188impl<'a> From<&'a Error> for Report<'a> {
189    #[inline]
190    fn from(error: &'a Error) -> Self {
191        error.to_report()
192    }
193}
194
195/// Updates an action for a condition in the trap set.
196///
197/// This is a utility function for implementing [`Command::execute`].
198async fn set_action<S: SignalSystem>(
199    traps: &mut TrapSet,
200    system: &S,
201    cond: Condition,
202    field: Field,
203    action: Action,
204    override_ignore: bool,
205) -> Result<(), Error> {
206    traps
207        .set_action(
208            system,
209            cond,
210            action.clone(),
211            field.origin.clone(),
212            override_ignore,
213        )
214        .await
215        .map_err(|cause| {
216            let cause = cause.into();
217            Error { cause, cond, field }
218        })
219}
220
221impl Command {
222    /// Executes the trap built-in.
223    ///
224    /// If successful, returns a string that should be printed to the standard
225    /// output. On failure, returns a non-empty list of errors.
226    pub async fn execute<S>(self, env: &mut Env<S>) -> Result<String, Vec<Error>>
227    where
228        S: Signals + Sigmask + Sigaction,
229    {
230        match self {
231            Self::PrintAll { include_default } => Ok(display_all_traps(
232                &mut env.traps,
233                &env.system,
234                include_default,
235            )),
236
237            Self::Print { conditions } => {
238                let mut output = String::new();
239                for (cond, _field) in conditions {
240                    display_trap(&mut env.traps, &env.system, cond, true, &mut output).unwrap();
241                }
242                Ok(output)
243            }
244
245            Self::SetAction { action, conditions } => {
246                let override_ignore = env.options.get(Interactive) == On;
247
248                let mut errors = Vec::new();
249                for (cond, field) in conditions {
250                    if let Err(error) = set_action(
251                        &mut env.traps,
252                        &env.system,
253                        cond,
254                        field,
255                        action.clone(),
256                        override_ignore,
257                    )
258                    .await
259                    {
260                        errors.push(error);
261                    }
262                }
263
264                if errors.is_empty() {
265                    Ok(String::new())
266                } else {
267                    Err(errors)
268                }
269            }
270        }
271    }
272}
273
274/// Entry point for executing the `trap` built-in
275pub async fn main<S>(env: &mut Env<S>, args: Vec<Field>) -> crate::Result
276where
277    S: Fcntl + Isatty + Signals + Sigmask + Sigaction + Write,
278{
279    let (options, operands) = match parse_arguments(syntax::OPTION_SPECS, Mode::with_env(env), args)
280    {
281        Ok(result) => result,
282        Err(error) => return report_error(env, &error).await,
283    };
284
285    let command = match syntax::interpret(options, operands, &env.system) {
286        Ok(command) => command,
287        Err(errors) => {
288            let is_soft_failure = errors
289                .iter()
290                .all(|e| matches!(e, syntax::Error::UnknownCondition(_)));
291            let report = merge_reports(&errors).unwrap();
292            let mut result = report_error(env, report).await;
293            if is_soft_failure {
294                result = crate::Result::from(ExitStatus::FAILURE);
295            }
296            return result;
297        }
298    };
299
300    match command.execute(env).await {
301        Ok(output) => crate::common::output(env, &output).await,
302        Err(mut errors) => {
303            // For now, we ignore the InitiallyIgnored error since it is not
304            // required by POSIX.
305            errors.retain(|error| error.cause != SetActionError::InitiallyIgnored.into());
306
307            match merge_reports(&errors) {
308                None => crate::Result::default(),
309                Some(report) => report_failure(env, report).await,
310            }
311        }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::Result;
319    use futures_util::future::FutureExt;
320    use std::ops::ControlFlow::{Break, Continue};
321    use std::rc::Rc;
322    use yash_env::Env;
323    use yash_env::VirtualSystem;
324    use yash_env::io::Fd;
325    use yash_env::semantics::Divert;
326    use yash_env::stack::Builtin;
327    use yash_env::stack::Frame;
328    use yash_env::system::Disposition;
329    use yash_env::system::r#virtual::{SIGINT, SIGPIPE, SIGUSR1, SIGUSR2};
330    use yash_env::test_helper::assert_stderr;
331    use yash_env::test_helper::assert_stdout;
332
333    #[test]
334    fn setting_trap_to_ignore() {
335        let system = VirtualSystem::new();
336        let pid = system.process_id;
337        let state = Rc::clone(&system.state);
338        let mut env = Env::with_system(system);
339        let args = Field::dummies(["", "USR1"]);
340        let result = main(&mut env, args).now_or_never().unwrap();
341        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
342        let process = &state.borrow().processes[&pid];
343        assert_eq!(process.disposition(SIGUSR1), Disposition::Ignore);
344    }
345
346    #[test]
347    fn setting_trap_to_command() {
348        let system = VirtualSystem::new();
349        let pid = system.process_id;
350        let state = Rc::clone(&system.state);
351        let mut env = Env::with_system(system);
352        let args = Field::dummies(["echo", "USR2"]);
353        let result = main(&mut env, args).now_or_never().unwrap();
354        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
355        let process = &state.borrow().processes[&pid];
356        assert_eq!(process.disposition(SIGUSR2), Disposition::Catch);
357    }
358
359    #[test]
360    fn resetting_trap() {
361        let system = VirtualSystem::new();
362        let pid = system.process_id;
363        let state = Rc::clone(&system.state);
364        let mut env = Env::with_system(system);
365        let args = Field::dummies(["-", "PIPE"]);
366        let result = main(&mut env, args).now_or_never().unwrap();
367        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
368        let process = &state.borrow().processes[&pid];
369        assert_eq!(process.disposition(SIGPIPE), Disposition::Default);
370    }
371
372    #[test]
373    fn printing_no_trap() {
374        let system = VirtualSystem::new();
375        let state = Rc::clone(&system.state);
376        let mut env = Env::with_system(system);
377
378        let result = main(&mut env, vec![]).now_or_never().unwrap();
379        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
380        assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
381    }
382
383    #[test]
384    fn printing_some_trap() {
385        let system = VirtualSystem::new();
386        let state = Rc::clone(&system.state);
387        let mut env = Env::with_system(system);
388        let args = Field::dummies(["echo", "INT"]);
389        let _ = main(&mut env, args).now_or_never().unwrap();
390
391        let result = main(&mut env, vec![]).now_or_never().unwrap();
392        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
393        assert_stdout(&state, |stdout| assert_eq!(stdout, "trap -- echo INT\n"));
394    }
395
396    #[test]
397    fn printing_some_traps() {
398        let system = VirtualSystem::new();
399        let state = Rc::clone(&system.state);
400        let mut env = Env::with_system(system);
401        let args = Field::dummies(["echo", "EXIT"]);
402        let _ = main(&mut env, args).now_or_never().unwrap();
403        let args = Field::dummies(["echo t", "TERM"]);
404        let _ = main(&mut env, args).now_or_never().unwrap();
405
406        let result = main(&mut env, vec![]).now_or_never().unwrap();
407        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
408        assert_stdout(&state, |stdout| {
409            assert_eq!(stdout, "trap -- echo EXIT\ntrap -- 'echo t' TERM\n")
410        });
411    }
412
413    #[test]
414    fn printing_initially_ignored_trap() {
415        let system = VirtualSystem::new();
416        system
417            .current_process_mut()
418            .set_disposition(SIGINT, Disposition::Ignore);
419        let mut env = Env::with_system(system.clone());
420
421        let result = main(&mut env, vec![]).now_or_never().unwrap();
422        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
423        assert_stdout(&system.state, |stdout| {
424            assert_eq!(stdout, "trap -- '' INT\n")
425        });
426    }
427
428    #[test]
429    fn printing_specified_traps() {
430        let system = VirtualSystem::new();
431        let state = Rc::clone(&system.state);
432        let mut env = Env::with_system(system);
433        let args = Field::dummies(["echo", "EXIT"]);
434        let _ = main(&mut env, args).now_or_never().unwrap();
435        let args = Field::dummies(["echo t", "TERM"]);
436        let _ = main(&mut env, args).now_or_never().unwrap();
437
438        let result = main(&mut env, Field::dummies(["-p", "TERM", "INT"]))
439            .now_or_never()
440            .unwrap();
441        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
442        assert_stdout(&state, |stdout| {
443            assert_eq!(stdout, "trap -- 'echo t' TERM\ntrap -- - INT\n")
444        });
445    }
446
447    #[test]
448    fn error_printing_traps() {
449        let system = VirtualSystem::new();
450        system.current_process_mut().close_fd(Fd::STDOUT);
451        let state = Rc::clone(&system.state);
452        let mut env = Env::with_system(system);
453        let mut env = env.push_frame(Frame::Builtin(Builtin {
454            name: Field::dummy("trap"),
455            is_special: true,
456        }));
457        let args = Field::dummies(["echo", "INT"]);
458        let _ = main(&mut env, args).now_or_never().unwrap();
459
460        let actual_result = main(&mut env, vec![]).now_or_never().unwrap();
461        let expected_result = Result::with_exit_status_and_divert(
462            ExitStatus::FAILURE,
463            Break(Divert::Interrupt(None)),
464        );
465        assert_eq!(actual_result, expected_result);
466        assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
467    }
468
469    #[test]
470    fn unknown_condition() {
471        let system = VirtualSystem::new();
472        let state = Rc::clone(&system.state);
473        let mut env = Env::with_system(system);
474        let mut env = env.push_frame(Frame::Builtin(Builtin {
475            name: Field::dummy("trap"),
476            is_special: true,
477        }));
478        let args = Field::dummies(["echo", "FOOBAR"]);
479
480        let actual_result = main(&mut env, args).now_or_never().unwrap();
481        let expected_result =
482            Result::with_exit_status_and_divert(ExitStatus::FAILURE, Continue(()));
483        assert_eq!(actual_result, expected_result);
484        assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
485    }
486
487    #[test]
488    fn missing_condition() {
489        let system = VirtualSystem::new();
490        let state = Rc::clone(&system.state);
491        let mut env = Env::with_system(system);
492        let mut env = env.push_frame(Frame::Builtin(Builtin {
493            name: Field::dummy("trap"),
494            is_special: true,
495        }));
496        let args = Field::dummies(["echo"]);
497
498        let actual_result = main(&mut env, args).now_or_never().unwrap();
499        let expected_result =
500            Result::with_exit_status_and_divert(ExitStatus::ERROR, Break(Divert::Interrupt(None)));
501        assert_eq!(actual_result, expected_result);
502        assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
503    }
504
505    #[test]
506    fn initially_ignored_signal_not_modifiable_if_non_interactive() {
507        let system = VirtualSystem::new();
508        system
509            .current_process_mut()
510            .set_disposition(SIGINT, Disposition::Ignore);
511        let mut env = Env::with_system(system.clone());
512        let mut env = env.push_frame(Frame::Builtin(Builtin {
513            name: Field::dummy("trap"),
514            is_special: true,
515        }));
516        let args = Field::dummies(["echo", "INT"]);
517
518        let result = main(&mut env, args).now_or_never().unwrap();
519        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
520        assert_stderr(&system.state, |stderr| assert_eq!(stderr, ""));
521        assert_eq!(
522            system.current_process().disposition(SIGINT),
523            Disposition::Ignore
524        );
525    }
526
527    #[test]
528    fn modifying_initially_ignored_signal_in_interactive_mode() {
529        let system = VirtualSystem::new();
530        system
531            .current_process_mut()
532            .set_disposition(SIGINT, Disposition::Ignore);
533        let mut env = Env::with_system(system.clone());
534        env.options.set(Interactive, On);
535        let mut env = env.push_frame(Frame::Builtin(Builtin {
536            name: Field::dummy("trap"),
537            is_special: true,
538        }));
539        let args = Field::dummies(["echo", "INT"]);
540
541        let result = main(&mut env, args).now_or_never().unwrap();
542        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
543        assert_stderr(&system.state, |stderr| assert_eq!(stderr, ""));
544        assert_eq!(
545            system.current_process().disposition(SIGINT),
546            Disposition::Catch
547        );
548    }
549
550    #[test]
551    fn trying_to_trap_sigkill() {
552        let system = VirtualSystem::new();
553        let state = Rc::clone(&system.state);
554        let mut env = Env::with_system(system);
555        let mut env = env.push_frame(Frame::Builtin(Builtin {
556            name: Field::dummy("trap"),
557            is_special: true,
558        }));
559        let args = Field::dummies(["echo", "KILL"]);
560
561        let actual_result = main(&mut env, args).now_or_never().unwrap();
562        let expected_result = Result::with_exit_status_and_divert(
563            ExitStatus::FAILURE,
564            Break(Divert::Interrupt(None)),
565        );
566        assert_eq!(actual_result, expected_result);
567        assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
568    }
569
570    #[test]
571    fn printing_traps_in_subshell() {
572        let system = VirtualSystem::new();
573        let state = Rc::clone(&system.state);
574        let mut env = Env::with_system(system);
575        let args = Field::dummies(["echo", "INT"]);
576        let _ = main(&mut env, args).now_or_never().unwrap();
577        let args = Field::dummies(["", "TERM"]);
578        let _ = main(&mut env, args).now_or_never().unwrap();
579        env.traps
580            .enter_subshell(&env.system, false, false)
581            .now_or_never()
582            .unwrap();
583
584        let result = main(&mut env, vec![]).now_or_never().unwrap();
585        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
586        assert_stdout(&state, |stdout| {
587            assert_eq!(stdout, "trap -- echo INT\ntrap -- '' TERM\n")
588        });
589    }
590
591    #[test]
592    fn printing_traps_after_setting_in_subshell() {
593        let system = VirtualSystem::new();
594        let state = Rc::clone(&system.state);
595        let mut env = Env::with_system(system);
596        let args = Field::dummies(["echo", "INT"]);
597        let _ = main(&mut env, args).now_or_never().unwrap();
598        let args = Field::dummies(["", "TERM"]);
599        let _ = main(&mut env, args).now_or_never().unwrap();
600        env.traps
601            .enter_subshell(&env.system, false, false)
602            .now_or_never()
603            .unwrap();
604        let args = Field::dummies(["ls", "QUIT"]);
605        let _ = main(&mut env, args).now_or_never().unwrap();
606
607        let result = main(&mut env, vec![]).now_or_never().unwrap();
608        assert_eq!(result, Result::new(ExitStatus::SUCCESS));
609        assert_stdout(&state, |stdout| {
610            assert_eq!(stdout, "trap -- ls QUIT\ntrap -- '' TERM\n")
611        });
612    }
613}