1use 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#[derive(Clone, Debug, Eq, PartialEq)]
53#[non_exhaustive]
54pub enum Command {
55 PrintAll {
57 include_default: bool,
59 },
60
61 Print {
63 conditions: Vec<(Condition, Field)>,
65 },
66
67 SetAction {
69 action: Action,
71 conditions: Vec<(Condition, Field)>,
73 },
74}
75
76pub mod syntax;
77
78fn 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#[must_use]
111pub fn display_traps<S: SignalSystem>(traps: &mut TrapSet, system: &S) -> String {
112 display_all_traps(traps, system, false)
113}
114
115#[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#[derive(Clone, Debug, Eq, Error, PartialEq)]
142#[non_exhaustive]
143pub enum ErrorCause {
144 #[error("signal not supported on this system")]
149 UnsupportedSignal,
150 #[error(transparent)]
152 SetAction(#[from] SetActionError),
153}
154
155#[derive(Clone, Debug, Eq, Error, PartialEq)]
157pub struct Error {
158 pub cause: ErrorCause,
160 pub cond: Condition,
162 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 #[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
195async 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 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
274pub 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 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}