mod cond;
pub use self::cond::CondSpec;
use crate::common::report::merge_reports;
use crate::common::report::report_error;
use crate::common::report::report_failure;
use crate::common::syntax::Mode;
use crate::common::syntax::parse_arguments;
use itertools::Itertools as _;
use std::borrow::Cow;
use std::fmt::Write;
use thiserror::Error;
use yash_env::Env;
use yash_env::System;
use yash_env::option::Option::Interactive;
use yash_env::option::State::On;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::signal::Name::{Kill, Stop};
#[allow(deprecated)]
use yash_env::source::pretty::{Annotation, AnnotationType, MessageBase};
use yash_env::source::pretty::{Report, ReportType, Snippet};
use yash_env::system::SharedSystem;
use yash_env::trap::Action;
use yash_env::trap::Condition;
use yash_env::trap::SetActionError;
use yash_env::trap::SignalSystem;
use yash_env::trap::TrapSet;
use yash_quote::quoted;
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Command {
PrintAll {
include_default: bool,
},
Print {
conditions: Vec<(CondSpec, Field)>,
},
SetAction {
action: Action,
conditions: Vec<(CondSpec, Field)>,
},
}
pub mod syntax;
fn display_trap<S: SignalSystem, W: Write>(
traps: &mut TrapSet,
system: &S,
cond: Condition,
include_default: bool,
output: &mut W,
) -> Result<(), std::fmt::Error> {
let Ok(trap) = traps.peek_state(system, cond) else {
return Ok(());
};
let command = match &trap.action {
Action::Default if include_default => "-",
Action::Default => return Ok(()),
Action::Ignore => "",
Action::Command(command) => command,
};
let cond = cond.to_string(system);
writeln!(output, "trap -- {} {}", quoted(command), cond)
}
#[must_use]
pub fn display_traps<S: SignalSystem>(traps: &mut TrapSet, system: &S) -> String {
display_all_traps(traps, system, false)
}
#[must_use]
pub fn display_all_traps<S: SignalSystem>(
traps: &mut TrapSet,
system: &S,
include_default: bool,
) -> String {
let mut output = String::new();
for cond in Condition::iter(system) {
if let Condition::Signal(number) = cond {
let name = system.signal_name_from_number(number);
if name == Kill || name == Stop {
continue;
}
}
display_trap(traps, system, cond, include_default, &mut output).unwrap()
}
output
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ErrorCause {
#[error("signal not supported on this system")]
UnsupportedSignal,
#[error(transparent)]
SetAction(#[from] SetActionError),
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub struct Error {
pub cause: ErrorCause,
pub cond: CondSpec,
pub field: Field,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.cause.fmt(f)
}
}
impl Error {
#[must_use]
pub fn to_report(&self) -> Report<'_> {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = match &self.cause {
ErrorCause::UnsupportedSignal => "invalid trap condition".into(),
ErrorCause::SetAction(_) => "cannot update trap".into(),
};
report.snippets =
Snippet::with_primary_span(&self.field.origin, self.cause.to_string().into());
report
}
}
impl<'a> From<&'a Error> for Report<'a> {
#[inline]
fn from(error: &'a Error) -> Self {
error.to_report()
}
}
#[allow(deprecated)]
impl MessageBase for Error {
fn message_title(&self) -> Cow<'_, str> {
match &self.cause {
ErrorCause::UnsupportedSignal => "invalid trap condition".into(),
ErrorCause::SetAction(_) => "cannot update trap".into(),
}
}
fn main_annotation(&self) -> Annotation<'_> {
Annotation::new(
AnnotationType::Error,
self.cause.to_string().into(),
&self.field.origin,
)
}
}
fn resolve<S: System>(cond: CondSpec, field: Field, system: &S) -> Result<Condition, Error> {
cond.to_condition(system).ok_or_else(|| {
let cause = ErrorCause::UnsupportedSignal;
Error { cause, cond, field }
})
}
fn set_action(
traps: &mut TrapSet,
system: &mut SharedSystem,
cond: CondSpec,
field: Field,
action: Action,
override_ignore: bool,
) -> Result<(), Error> {
let Some(cond2) = cond.to_condition(system) else {
let cause = ErrorCause::UnsupportedSignal;
return Err(Error { cause, cond, field });
};
traps
.set_action(
system,
cond2,
action.clone(),
field.origin.clone(),
override_ignore,
)
.map_err(|cause| {
let cause = cause.into();
Error { cause, cond, field }
})
}
impl Command {
pub fn execute(self, env: &mut Env) -> Result<String, Vec<Error>> {
match self {
Self::PrintAll { include_default } => Ok(display_all_traps(
&mut env.traps,
&env.system,
include_default,
)),
Self::Print { conditions } => {
let mut output = String::new();
let ((), errors): ((), Vec<Error>) = conditions
.into_iter()
.map(|(cond, field)| {
let cond = resolve(cond, field, &env.system)?;
display_trap(&mut env.traps, &env.system, cond, true, &mut output).unwrap();
Ok(())
})
.partition_result();
if errors.is_empty() {
Ok(output)
} else {
Err(errors)
}
}
Self::SetAction { action, conditions } => {
let override_ignore = env.options.get(Interactive) == On;
let ((), errors): ((), Vec<Error>) = conditions
.into_iter()
.map(|(cond, field)| {
set_action(
&mut env.traps,
&mut env.system,
cond,
field,
action.clone(),
override_ignore,
)
})
.partition_result();
if errors.is_empty() {
Ok(String::new())
} else {
Err(errors)
}
}
}
}
}
pub async fn main(env: &mut Env, args: Vec<Field>) -> crate::Result {
let (options, operands) = match parse_arguments(syntax::OPTION_SPECS, Mode::with_env(env), args)
{
Ok(result) => result,
Err(error) => return report_error(env, &error).await,
};
let command = match syntax::interpret(options, operands) {
Ok(command) => command,
Err(errors) => {
let is_soft_failure = errors
.iter()
.all(|e| matches!(e, syntax::Error::UnknownCondition(_)));
let report = merge_reports(&errors).unwrap();
let mut result = report_error(env, report).await;
if is_soft_failure {
result = crate::Result::from(ExitStatus::FAILURE);
}
return result;
}
};
match command.execute(env) {
Ok(output) => crate::common::output(env, &output).await,
Err(mut errors) => {
errors.retain(|error| error.cause != SetActionError::InitiallyIgnored.into());
match merge_reports(&errors) {
None => crate::Result::default(),
Some(report) => report_failure(env, report).await,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Result;
use futures_util::future::FutureExt;
use std::ops::ControlFlow::{Break, Continue};
use std::rc::Rc;
use yash_env::Env;
use yash_env::VirtualSystem;
use yash_env::io::Fd;
use yash_env::semantics::Divert;
use yash_env::stack::Builtin;
use yash_env::stack::Frame;
use yash_env::system::Disposition;
use yash_env::system::r#virtual::{SIGINT, SIGPIPE, SIGUSR1, SIGUSR2};
use yash_env_test_helper::assert_stderr;
use yash_env_test_helper::assert_stdout;
#[test]
fn setting_trap_to_ignore() {
let system = Box::new(VirtualSystem::new());
let pid = system.process_id;
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["", "USR1"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
let process = &state.borrow().processes[&pid];
assert_eq!(process.disposition(SIGUSR1), Disposition::Ignore);
}
#[test]
fn setting_trap_to_command() {
let system = Box::new(VirtualSystem::new());
let pid = system.process_id;
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["echo", "USR2"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
let process = &state.borrow().processes[&pid];
assert_eq!(process.disposition(SIGUSR2), Disposition::Catch);
}
#[test]
fn resetting_trap() {
let system = Box::new(VirtualSystem::new());
let pid = system.process_id;
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["-", "PIPE"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
let process = &state.borrow().processes[&pid];
assert_eq!(process.disposition(SIGPIPE), Disposition::Default);
}
#[test]
fn printing_no_trap() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn printing_some_trap() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["echo", "INT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&state, |stdout| assert_eq!(stdout, "trap -- echo INT\n"));
}
#[test]
fn printing_some_traps() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["echo", "EXIT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let args = Field::dummies(["echo t", "TERM"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&state, |stdout| {
assert_eq!(stdout, "trap -- echo EXIT\ntrap -- 'echo t' TERM\n")
});
}
#[test]
fn printing_initially_ignored_trap() {
let mut system = VirtualSystem::new();
system
.current_process_mut()
.set_disposition(SIGINT, Disposition::Ignore);
let mut env = Env::with_system(Box::new(system.clone()));
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&system.state, |stdout| {
assert_eq!(stdout, "trap -- '' INT\n")
});
}
#[test]
fn printing_specified_traps() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["echo", "EXIT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let args = Field::dummies(["echo t", "TERM"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let result = main(&mut env, Field::dummies(["-p", "TERM", "INT"]))
.now_or_never()
.unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&state, |stdout| {
assert_eq!(stdout, "trap -- 'echo t' TERM\ntrap -- - INT\n")
});
}
#[test]
fn error_printing_traps() {
let mut system = Box::new(VirtualSystem::new());
system.current_process_mut().close_fd(Fd::STDOUT);
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: Field::dummy("trap"),
is_special: true,
}));
let args = Field::dummies(["echo", "INT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let actual_result = main(&mut env, vec![]).now_or_never().unwrap();
let expected_result = Result::with_exit_status_and_divert(
ExitStatus::FAILURE,
Break(Divert::Interrupt(None)),
);
assert_eq!(actual_result, expected_result);
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn unknown_condition() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: Field::dummy("trap"),
is_special: true,
}));
let args = Field::dummies(["echo", "FOOBAR"]);
let actual_result = main(&mut env, args).now_or_never().unwrap();
let expected_result =
Result::with_exit_status_and_divert(ExitStatus::FAILURE, Continue(()));
assert_eq!(actual_result, expected_result);
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn missing_condition() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: Field::dummy("trap"),
is_special: true,
}));
let args = Field::dummies(["echo"]);
let actual_result = main(&mut env, args).now_or_never().unwrap();
let expected_result =
Result::with_exit_status_and_divert(ExitStatus::ERROR, Break(Divert::Interrupt(None)));
assert_eq!(actual_result, expected_result);
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn initially_ignored_signal_not_modifiable_if_non_interactive() {
let mut system = VirtualSystem::new();
system
.current_process_mut()
.set_disposition(SIGINT, Disposition::Ignore);
let mut env = Env::with_system(Box::new(system.clone()));
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: Field::dummy("trap"),
is_special: true,
}));
let args = Field::dummies(["echo", "INT"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stderr(&system.state, |stderr| assert_eq!(stderr, ""));
assert_eq!(
system.current_process().disposition(SIGINT),
Disposition::Ignore
);
}
#[test]
fn modifying_initially_ignored_signal_in_interactive_mode() {
let mut system = VirtualSystem::new();
system
.current_process_mut()
.set_disposition(SIGINT, Disposition::Ignore);
let mut env = Env::with_system(Box::new(system.clone()));
env.options.set(Interactive, On);
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: Field::dummy("trap"),
is_special: true,
}));
let args = Field::dummies(["echo", "INT"]);
let result = main(&mut env, args).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stderr(&system.state, |stderr| assert_eq!(stderr, ""));
assert_eq!(
system.current_process().disposition(SIGINT),
Disposition::Catch
);
}
#[test]
fn trying_to_trap_sigkill() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: Field::dummy("trap"),
is_special: true,
}));
let args = Field::dummies(["echo", "KILL"]);
let actual_result = main(&mut env, args).now_or_never().unwrap();
let expected_result = Result::with_exit_status_and_divert(
ExitStatus::FAILURE,
Break(Divert::Interrupt(None)),
);
assert_eq!(actual_result, expected_result);
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn printing_traps_in_subshell() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["echo", "INT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let args = Field::dummies(["", "TERM"]);
let _ = main(&mut env, args).now_or_never().unwrap();
env.traps.enter_subshell(&mut env.system, false, false);
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&state, |stdout| {
assert_eq!(stdout, "trap -- echo INT\ntrap -- '' TERM\n")
});
}
#[test]
fn printing_traps_after_setting_in_subshell() {
let system = Box::new(VirtualSystem::new());
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let args = Field::dummies(["echo", "INT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let args = Field::dummies(["", "TERM"]);
let _ = main(&mut env, args).now_or_never().unwrap();
env.traps.enter_subshell(&mut env.system, false, false);
let args = Field::dummies(["ls", "QUIT"]);
let _ = main(&mut env, args).now_or_never().unwrap();
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, Result::new(ExitStatus::SUCCESS));
assert_stdout(&state, |stdout| {
assert_eq!(stdout, "trap -- ls QUIT\ntrap -- '' TERM\n")
});
}
}