use crate::builtins::Builtin;
use crate::parser::ShellCommand;
use crate::state::ShellState;
use std::io::Write;
pub struct TrapBuiltin;
const SIGNAL_MAP: &[(&str, i32)] = &[
("HUP", 1), ("INT", 2), ("QUIT", 3), ("ILL", 4), ("TRAP", 5), ("ABRT", 6), ("BUS", 7), ("FPE", 8), ("KILL", 9), ("USR1", 10), ("SEGV", 11), ("USR2", 12), ("PIPE", 13), ("ALRM", 14), ("TERM", 15), ("CHLD", 17), ("CONT", 18), ("STOP", 19), ("TSTP", 20), ("TTIN", 21), ("TTOU", 22), ("URG", 23), ("XCPU", 24), ("XFSZ", 25), ("VTALRM", 26), ("PROF", 27), ("WINCH", 28), ("IO", 29), ("PWR", 30), ("SYS", 31), ("EXIT", 0), ];
impl TrapBuiltin {
fn normalize_signal_name(signal: &str) -> String {
let sig = signal.to_uppercase();
if sig.starts_with("SIG") && sig.len() > 3 {
sig[3..].to_string()
} else {
sig
}
}
fn signal_to_name(signal: &str) -> Result<String, String> {
let normalized = Self::normalize_signal_name(signal);
if let Ok(num) = normalized.parse::<i32>() {
for (name, sig_num) in SIGNAL_MAP {
if *sig_num == num {
return Ok(name.to_string());
}
}
return Err(format!("Invalid signal number: {}", num));
}
for (name, _) in SIGNAL_MAP {
if *name == normalized {
return Ok(name.to_string());
}
}
Err(format!("Invalid signal name: {}", signal))
}
fn is_trappable(signal_name: &str) -> bool {
!matches!(signal_name, "KILL" | "STOP")
}
fn list_signals(output: &mut dyn Write) -> i32 {
for (name, num) in SIGNAL_MAP {
if *num > 0 {
let _ = writeln!(output, "{}) SIG{}", num, name);
}
}
0
}
fn display_trap(signal_name: &str, shell_state: &ShellState, output: &mut dyn Write) -> i32 {
if let Some(command) = shell_state.get_trap(signal_name) {
let _ = writeln!(output, "trap -- '{}' {}", command, signal_name);
}
0
}
fn display_all_traps(shell_state: &ShellState, output: &mut dyn Write) -> i32 {
let traps = shell_state.get_all_traps();
if traps.is_empty() {
return 0;
}
let mut sorted_traps: Vec<_> = traps.iter().collect();
sorted_traps.sort_by_key(|(name, _)| *name);
for (signal_name, command) in sorted_traps {
let _ = writeln!(output, "trap -- '{}' {}", command, signal_name);
}
0
}
fn set_trap(
action: &str,
signals: &[String],
shell_state: &mut ShellState,
output: &mut dyn Write,
) -> i32 {
let mut exit_code = 0;
for signal in signals {
match Self::signal_to_name(signal) {
Ok(signal_name) => {
if !Self::is_trappable(&signal_name) {
let _ = writeln!(output, "trap: {}: cannot trap signal", signal);
exit_code = 1;
continue;
}
if action.is_empty() {
shell_state.set_trap(&signal_name, String::new());
} else {
shell_state.set_trap(&signal_name, action.to_string());
}
}
Err(e) => {
let _ = writeln!(output, "trap: {}", e);
exit_code = 1;
}
}
}
exit_code
}
fn reset_traps(
signals: &[String],
shell_state: &mut ShellState,
output: &mut dyn Write,
) -> i32 {
let mut exit_code = 0;
for signal in signals {
match Self::signal_to_name(signal) {
Ok(signal_name) => {
shell_state.remove_trap(&signal_name);
}
Err(e) => {
let _ = writeln!(output, "trap: {}", e);
exit_code = 1;
}
}
}
exit_code
}
}
impl Builtin for TrapBuiltin {
fn name(&self) -> &'static str {
"trap"
}
fn names(&self) -> Vec<&'static str> {
vec!["trap"]
}
fn description(&self) -> &'static str {
"Set or display signal handlers"
}
fn run(
&self,
cmd: &ShellCommand,
shell_state: &mut ShellState,
output_writer: &mut dyn Write,
) -> i32 {
let args = &cmd.args;
if args.len() == 1 {
return Self::display_all_traps(shell_state, output_writer);
}
if args.len() == 2 && args[1] == "-l" {
return Self::list_signals(output_writer);
}
if args.len() >= 2 && args[1] == "-p" {
if args.len() == 2 {
return Self::display_all_traps(shell_state, output_writer);
} else {
let mut exit_code = 0;
for signal in &args[2..] {
match Self::signal_to_name(signal) {
Ok(signal_name) => {
exit_code =
Self::display_trap(&signal_name, shell_state, output_writer);
}
Err(e) => {
let _ = writeln!(output_writer, "trap: {}", e);
exit_code = 1;
}
}
}
return exit_code;
}
}
if args.len() >= 3 && args[1] == "-" {
return Self::reset_traps(&args[2..], shell_state, output_writer);
}
if args.len() >= 3 {
let action = &args[1];
let signals: Vec<String> = args[2..].to_vec();
return Self::set_trap(action, &signals, shell_state, output_writer);
}
let _ = writeln!(
output_writer,
"trap: usage: trap [-lp] [[action] signal...]"
);
1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_signal_name() {
assert_eq!(TrapBuiltin::normalize_signal_name("int"), "INT");
assert_eq!(TrapBuiltin::normalize_signal_name("INT"), "INT");
assert_eq!(TrapBuiltin::normalize_signal_name("SIGINT"), "INT");
assert_eq!(TrapBuiltin::normalize_signal_name("sigint"), "INT");
assert_eq!(TrapBuiltin::normalize_signal_name("SigInt"), "INT");
}
#[test]
fn test_signal_to_name() {
assert_eq!(TrapBuiltin::signal_to_name("INT").unwrap(), "INT");
assert_eq!(TrapBuiltin::signal_to_name("SIGINT").unwrap(), "INT");
assert_eq!(TrapBuiltin::signal_to_name("2").unwrap(), "INT");
assert_eq!(TrapBuiltin::signal_to_name("15").unwrap(), "TERM");
assert_eq!(TrapBuiltin::signal_to_name("SIGTERM").unwrap(), "TERM");
assert!(TrapBuiltin::signal_to_name("INVALID").is_err());
assert!(TrapBuiltin::signal_to_name("999").is_err());
}
#[test]
fn test_is_trappable() {
assert!(TrapBuiltin::is_trappable("INT"));
assert!(TrapBuiltin::is_trappable("TERM"));
assert!(TrapBuiltin::is_trappable("HUP"));
assert!(!TrapBuiltin::is_trappable("KILL"));
assert!(!TrapBuiltin::is_trappable("STOP"));
}
#[test]
fn test_trap_set_handler() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec![
"trap".to_string(),
"echo hello".to_string(),
"INT".to_string(),
],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
assert_eq!(shell_state.get_trap("INT"), Some("echo hello".to_string()));
}
#[test]
fn test_trap_reset_handler() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
shell_state.set_trap("INT", "echo hello".to_string());
assert_eq!(shell_state.get_trap("INT"), Some("echo hello".to_string()));
let cmd = ShellCommand {
args: vec!["trap".to_string(), "-".to_string(), "INT".to_string()],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
assert_eq!(shell_state.get_trap("INT"), None);
}
#[test]
fn test_trap_invalid_signal() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec![
"trap".to_string(),
"echo hello".to_string(),
"INVALID".to_string(),
],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 1);
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("Invalid signal"));
}
#[test]
fn test_trap_uncatchable_signal() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec![
"trap".to_string(),
"echo hello".to_string(),
"KILL".to_string(),
],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 1);
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("cannot trap"));
}
#[test]
fn test_trap_multiple_signals() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec![
"trap".to_string(),
"echo signal".to_string(),
"INT".to_string(),
"TERM".to_string(),
"HUP".to_string(),
],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
assert_eq!(shell_state.get_trap("INT"), Some("echo signal".to_string()));
assert_eq!(
shell_state.get_trap("TERM"),
Some("echo signal".to_string())
);
assert_eq!(shell_state.get_trap("HUP"), Some("echo signal".to_string()));
}
#[test]
fn test_trap_display_all() {
let mut shell_state = ShellState::new();
shell_state.set_trap("INT", "echo int".to_string());
shell_state.set_trap("TERM", "echo term".to_string());
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec!["trap".to_string()],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("trap -- 'echo int' INT"));
assert!(output_str.contains("trap -- 'echo term' TERM"));
}
#[test]
fn test_trap_list_signals() {
let mut output = Vec::new();
let mut shell_state = ShellState::new();
let cmd = ShellCommand {
args: vec!["trap".to_string(), "-l".to_string()],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("2) SIGINT"));
assert!(output_str.contains("15) SIGTERM"));
}
#[test]
fn test_trap_empty_action() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec!["trap".to_string(), "".to_string(), "INT".to_string()],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
assert_eq!(shell_state.get_trap("INT"), Some("".to_string()));
}
#[test]
fn test_trap_signal_numbers() {
let mut shell_state = ShellState::new();
let mut output = Vec::new();
let cmd = ShellCommand {
args: vec![
"trap".to_string(),
"echo signal".to_string(),
"2".to_string(),
],
redirections: Vec::new(),
compound: None,
};
let builtin = TrapBuiltin;
let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
assert_eq!(exit_code, 0);
assert_eq!(shell_state.get_trap("INT"), Some("echo signal".to_string()));
}
}