use std::collections::{BTreeSet, HashMap};
#[derive(Debug, Clone, PartialEq)]
pub enum TrapAction {
Default,
Ignore,
Command(String),
}
type SavedTraps = (Option<TrapAction>, HashMap<i32, TrapAction>);
#[derive(Debug, Clone, Default)]
pub struct TrapStore {
pub exit_trap: Option<TrapAction>,
pub signal_traps: HashMap<i32, TrapAction>,
saved_traps: Option<Box<SavedTraps>>,
}
impl TrapStore {
pub fn signal_name_to_number(name: &str) -> Option<i32> {
if let Ok(n) = name.parse::<i32>() {
return Some(n);
}
if name.eq_ignore_ascii_case("EXIT") {
return Some(0);
}
crate::signal::signal_name_to_number(name).ok()
}
fn signal_number_to_name(num: i32) -> &'static str {
if num == 0 {
return "EXIT";
}
crate::signal::signal_number_to_name(num).unwrap_or("UNKNOWN")
}
pub fn set_trap(&mut self, condition: &str, action: TrapAction) -> Result<(), String> {
self.set_trap_with(condition, action, &crate::signal::is_ignored_on_entry)
}
pub(crate) fn set_trap_with(
&mut self,
condition: &str,
action: TrapAction,
is_ignored: &dyn Fn(i32) -> bool,
) -> Result<(), String> {
let num = Self::signal_name_to_number(condition)
.ok_or_else(|| format!("invalid signal name: {}", condition))?;
if num == 0 {
self.exit_trap = Some(action);
return Ok(());
}
if is_ignored(num) {
return Ok(());
}
self.signal_traps.insert(num, action);
Ok(())
}
#[allow(dead_code)]
pub fn get_trap(&self, condition: &str) -> Option<&TrapAction> {
let num = Self::signal_name_to_number(condition)?;
if num == 0 {
self.exit_trap.as_ref()
} else {
self.signal_traps.get(&num)
}
}
pub fn remove_trap(&mut self, condition: &str) {
self.remove_trap_with(condition, &crate::signal::is_ignored_on_entry)
}
pub(crate) fn remove_trap_with(&mut self, condition: &str, is_ignored: &dyn Fn(i32) -> bool) {
let Some(num) = Self::signal_name_to_number(condition) else {
return;
};
if num == 0 {
self.exit_trap = None;
return;
}
if is_ignored(num) {
return;
}
self.signal_traps.remove(&num);
}
pub(crate) fn reset_non_ignored(&mut self) {
if matches!(self.exit_trap, Some(TrapAction::Command(_))) {
self.exit_trap = None;
}
self.signal_traps
.retain(|_, action| matches!(action, TrapAction::Ignore));
}
pub fn reset_for_subshell(&mut self) {
self.reset_non_ignored();
self.saved_traps = None;
}
#[cfg(test)]
pub(crate) fn saved_traps_is_some(&self) -> bool {
self.saved_traps.is_some()
}
pub fn reset_for_command_sub(&mut self) {
self.saved_traps = Some(Box::new((
self.exit_trap.clone(),
self.signal_traps.clone(),
)));
self.reset_non_ignored();
}
pub fn ignored_signals(&self) -> Vec<i32> {
self.signal_traps
.iter()
.filter(|(_, action)| matches!(action, TrapAction::Ignore))
.map(|(&num, _)| num)
.collect()
}
pub fn get_signal_trap(&self, sig: i32) -> Option<&TrapAction> {
self.signal_traps.get(&sig)
}
pub fn display_all(&self) {
let (exit_trap, signal_traps) = if let Some(saved) = &self.saved_traps {
(&saved.0, &saved.1)
} else {
(&self.exit_trap, &self.signal_traps)
};
if let Some(action) = exit_trap {
match action {
TrapAction::Command(cmd) => println!("trap -- '{}' EXIT", cmd),
TrapAction::Ignore => println!("trap -- '' EXIT"),
TrapAction::Default => {}
}
}
let mut keys: BTreeSet<i32> = signal_traps.keys().copied().collect();
if let Some(entry_set) = crate::signal::ignored_on_entry_set_opt() {
for &sig in entry_set {
keys.insert(sig);
}
}
for num in keys {
let name = Self::signal_number_to_name(num);
match signal_traps.get(&num) {
Some(TrapAction::Command(cmd)) => println!("trap -- '{}' SIG{}", cmd, name),
Some(TrapAction::Ignore) => println!("trap -- '' SIG{}", name),
Some(TrapAction::Default) => {}
None => {
println!("trap -- '' SIG{}", name);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trap_store_default() {
let store = TrapStore::default();
assert!(store.exit_trap.is_none());
assert!(store.signal_traps.is_empty());
}
#[test]
fn test_trap_store_set_exit() {
let mut store = TrapStore::default();
store
.set_trap("EXIT", TrapAction::Command("echo bye".to_string()))
.unwrap();
assert!(matches!(
store.get_trap("EXIT"),
Some(TrapAction::Command(_))
));
}
#[test]
fn test_trap_store_set_signal() {
let mut store = TrapStore::default();
store.set_trap("INT", TrapAction::Ignore).unwrap();
assert!(matches!(store.get_trap("INT"), Some(TrapAction::Ignore)));
store.set_trap("INT", TrapAction::Default).unwrap();
assert!(matches!(store.get_trap("INT"), Some(TrapAction::Default)));
}
#[test]
fn test_trap_store_signal_name_to_number() {
assert_eq!(TrapStore::signal_name_to_number("EXIT"), Some(0));
assert_eq!(TrapStore::signal_name_to_number("HUP"), Some(1));
assert_eq!(TrapStore::signal_name_to_number("INT"), Some(2));
assert_eq!(TrapStore::signal_name_to_number("QUIT"), Some(3));
assert_eq!(TrapStore::signal_name_to_number("TERM"), Some(15));
assert_eq!(TrapStore::signal_name_to_number("0"), Some(0));
assert_eq!(TrapStore::signal_name_to_number("2"), Some(2));
assert_eq!(TrapStore::signal_name_to_number("INVALID"), None);
}
#[test]
fn test_trap_store_remove() {
let mut store = TrapStore::default();
store
.set_trap("EXIT", TrapAction::Command("echo bye".to_string()))
.unwrap();
store.remove_trap("EXIT");
assert!(store.exit_trap.is_none());
}
#[test]
fn test_trap_store_reset_non_ignored() {
let mut store = TrapStore::default();
store
.set_trap("INT", TrapAction::Command("echo caught".to_string()))
.unwrap();
store.set_trap("HUP", TrapAction::Ignore).unwrap();
store
.set_trap("TERM", TrapAction::Command("echo term".to_string()))
.unwrap();
store.reset_non_ignored();
assert!(!store.signal_traps.contains_key(&2));
assert_eq!(store.signal_traps.get(&1), Some(&TrapAction::Ignore));
assert!(!store.signal_traps.contains_key(&15));
}
#[test]
fn test_set_trap_with_ignored_predicate_is_silent() {
let mut store = TrapStore::default();
let is_ignored = |sig: i32| sig == 2;
let result = store.set_trap_with(
"INT",
TrapAction::Command("echo caught".to_string()),
&is_ignored,
);
assert!(
result.is_ok(),
"silent-ignore must return Ok(()), got {:?}",
result
);
assert!(
store.signal_traps.is_empty(),
"signal_traps should remain empty when set on ignored-on-entry; got {:?}",
store.signal_traps
);
}
#[test]
fn test_set_trap_with_non_ignored_predicate_inserts_normally() {
let mut store = TrapStore::default();
let never_ignored = |_sig: i32| false;
let result = store.set_trap_with(
"INT",
TrapAction::Command("echo x".to_string()),
&never_ignored,
);
assert!(result.is_ok());
assert!(matches!(
store.signal_traps.get(&2),
Some(TrapAction::Command(_))
));
}
#[test]
fn test_set_trap_exit_signal_bypasses_ignored_check() {
let mut store = TrapStore::default();
let always_ignored = |_sig: i32| true;
let result = store.set_trap_with(
"EXIT",
TrapAction::Command("echo bye".to_string()),
&always_ignored,
);
assert!(result.is_ok());
assert!(matches!(store.exit_trap, Some(TrapAction::Command(_))));
}
#[test]
fn test_remove_trap_with_ignored_predicate_is_silent() {
let mut store = TrapStore::default();
store.signal_traps.insert(2, TrapAction::Ignore);
let is_ignored = |sig: i32| sig == 2;
store.remove_trap_with("INT", &is_ignored);
assert_eq!(
store.signal_traps.get(&2),
Some(&TrapAction::Ignore),
"remove_trap on ignored-on-entry signal must be silent no-op"
);
}
#[test]
fn test_trap_store_get_signal_trap() {
let mut store = TrapStore::default();
store
.set_trap("INT", TrapAction::Command("echo caught".to_string()))
.unwrap();
assert!(matches!(
store.get_signal_trap(2),
Some(TrapAction::Command(_))
));
assert!(store.get_signal_trap(15).is_none());
}
#[test]
fn test_reset_for_subshell_clears_saved_traps() {
let mut store = TrapStore::default();
store
.set_trap("INT", TrapAction::Command("echo parent".into()))
.unwrap();
store.reset_for_command_sub();
assert!(
store.saved_traps_is_some(),
"precondition: reset_for_command_sub must populate saved_traps"
);
store.reset_for_subshell();
assert!(
!store.saved_traps_is_some(),
"saved_traps must be cleared by reset_for_subshell"
);
assert!(
store.signal_traps.is_empty(),
"signal_traps must be reset by reset_for_subshell"
);
}
#[test]
fn test_reset_for_subshell_preserves_ignored() {
let mut store = TrapStore::default();
store.set_trap("HUP", TrapAction::Ignore).unwrap();
store
.set_trap("INT", TrapAction::Command("x".into()))
.unwrap();
store.reset_for_subshell();
assert_eq!(
store.signal_traps.get(&1),
Some(&TrapAction::Ignore),
"HUP Ignore must be preserved"
);
assert!(
!store.signal_traps.contains_key(&2),
"INT Command must be cleared"
);
}
#[test]
fn test_reset_for_subshell_with_no_saved_traps_is_safe() {
let mut store = TrapStore::default();
store
.set_trap("INT", TrapAction::Command("x".into()))
.unwrap();
store.reset_for_subshell();
assert!(!store.saved_traps_is_some());
assert!(store.signal_traps.is_empty());
}
}