use std::collections::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> {
let num = Self::signal_name_to_number(condition)
.ok_or_else(|| format!("invalid signal name: {}", condition))?;
if num == 0 {
self.exit_trap = Some(action);
} else {
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) {
if let Some(num) = Self::signal_name_to_number(condition) {
if num == 0 {
self.exit_trap = None;
} else {
self.signal_traps.remove(&num);
}
}
}
pub 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_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: Vec<i32> = signal_traps.keys().copied().collect();
keys.sort();
for num in keys {
if let Some(action) = signal_traps.get(&num) {
let name = Self::signal_number_to_name(num);
match action {
TrapAction::Command(cmd) => println!("trap -- '{}' SIG{}", cmd, name),
TrapAction::Ignore => println!("trap -- '' SIG{}", name),
TrapAction::Default => {}
}
}
}
}
}
#[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.get(&2).is_none());
assert_eq!(store.signal_traps.get(&1), Some(&TrapAction::Ignore));
assert!(store.signal_traps.get(&15).is_none());
}
#[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());
}
}