I have packed everything I know about Bash traps into 80 lines of code (fairly well commented). It shows how to trap signals, how to take action on them, and how to deal with deferring the exit from the script.
It throws test signals at itself via child processes, but you can also send your own signals from another terminal. You might find parts of it helpful in addressing your own problem. It is a heck of a lot to unpack, though: ask if you get stuck.
Don't worry about the wdog
part: that is just a thing that synchronises the script to a minute, so the various delays are easier to read.
This is the Bash script.
EDIT: I found the bug that produces runaway timestamps. I use two read -t
commands to slow down the loops, and as a hook for getting user input. If stdin is at EOF (redirected from /dev/null, Ctrl-D at terminal, etc), the timeout fails, and the loop runs about 6000 iterations per second. Fix is to check status from the read -t
, and unless -gt 128
do the equivalent sleep (as applied below). Probably SIGTTIN would work now, too.
#! /bin/bash --
#.. trapPlay: an investigation into Bash signals.
intCount=0 #.. GLOBAL: Three Ctrl-C will exit the script.
#.. This function is called from every trap, with the trap name as an argument.
GlobalTrap () {
#.. Report all trap calls.
printf 'GlobalTrap: %-6s at %(%T)T\n' "${1}" -1
case "${1}" in
(INT) (( ++intCount )) #.. Console Ctrl-C.
printf '%(%T)T (Ctrl-C) (%s)\n' -1 "${intCount}"
#.. If exiting via Ctrl-C, disable trap for EXIT.
(( intCount >= 3 )) && { trap - EXIT; exit; }
;;
(WINCH) local Wr Wc #.. Window change size.
IFS='' read -r Wr < <( tput lines )
IFS='' read -r Wc < <( tput cols )
printf 'Window now %s lines %s cols\n' "${Wr}" "${Wc}"
;;
(EXIT) printf "\nBash time expired -- ready to exit\n"
Finishing;;
esac
}
#.. This function can be used to do things after Bash has seen "exit".
#.. When you leave this function, Bash will terminate your process.
#.. You get 20 more seconds to do stuff, or type 'Y'.
Finishing () {
local X
while (( SECONDS <= Expired + 20 )); do
printf 1>&2 "[Y] > "
read -r -t 2.0 X #-- Using 'read' as a loop delay.
[[ "${?}" -gt 128 ]] || sleep 2.0
[[ "${X}" = "Y" ]] && { printf "Exit confirmed\n"; exit; }
done
printf "\nExit overdue !!\n"
#.. Returning from this function ends processing of the EXIT trap,
#.. so Bash terminates the process.
}
#### Script Body Starts Here.
#.. Make an array of all the non-realtime signal numbers.
for c in {1..31}; do Cd[$c]="$c"; done
#.. Give usual names to all the important ones.
Cd[ 0]="EXIT"; Cd[ 1]="HUP"; Cd[ 2]="INT"; Cd[ 3]="QUIT";
Cd[ 5]="TRAP"; Cd[ 6]="ABRT"; Cd[10]="USR1"; Cd[12]="USR2";
Cd[13]="PIPE"; Cd[15]="TERM"; Cd[28]="WINCH";
#.. Destroy any problem ones.
for s in 5 21 22; do unset Cd["${s}"]; done
#.. Make a lot of traps like: trap 'GlobalTrap TERM' TERM
for s in "${Cd[@]}"; do
trap "GlobalTrap ${s}" "${s}"
done
printf '#### Send signals to me from another terminal ####\n'
printf '#### Like: kill -HUP %s\n\n' ${$}
Expired="$(( SECONDS + 60 ))"
printf 'Tick: %(%T)T\n' -1
#.. Set up some self-test shots.
for s in {20..58..9}; do
( sleep "${s}" && kill -SIGUSR2 $$ ) &
done
( sleep 12 && kill -SIGPIPE $$ ) &
( sleep 36 && kill -SIGTERM $$ ) &
#.. Do useful work here: we just show ticks using 'read' as a loop delay.
while (( SECONDS <= Expired )); do
read -r -t 10.0 Discard
[[ "${?}" -gt 128 ]] || sleep 10.0
printf 'Tick: %(%T)T\n' -1
done
#.. Dropping through here triggers the EXIT trap, which runs Finishing.
There are three executions.
(1) This run shows both the 60 seconds and the 20 seconds expiring, and the self-tests. The additional HUP signal at 20:59:30 was manually entered from another terminal.
$ wdog -d 0 -w 1m; ./trapPlay
wdog 24.172| Wed Mar 20 20:59:00.000 2024
wdog 20:59:00.023| Tick
#### Send signals to me from another terminal ####
#### Like: kill -HUP 18610
Tick: 20:59:00
Tick: 20:59:10
GlobalTrap: PIPE at 20:59:12
Tick: 20:59:20
GlobalTrap: USR2 at 20:59:20
GlobalTrap: USR2 at 20:59:29
Tick: 20:59:30
GlobalTrap: HUP at 20:59:30
GlobalTrap: TERM at 20:59:36
GlobalTrap: USR2 at 20:59:38
Tick: 20:59:40
GlobalTrap: USR2 at 20:59:47
Tick: 20:59:50
GlobalTrap: USR2 at 20:59:56
Tick: 21:00:00
Tick: 21:00:10
GlobalTrap: EXIT at 21:00:10
Bash time expired -- ready to exit
[Y] > [Y] > [Y] > [Y] > [Y] > [Y] >
Exit overdue !!
$
(2) This run show processing of dragging the terminal window to change the size, and then invoking the three Ctrl-C signals to terminate the script, bypassing the EXIT trap which would run Finishing
.
$ wdog -d 0 -w 1m; ./trapPlay
wdog 13.380| Wed Mar 20 21:01:00.000 2024
wdog 21:01:00.014| Tick
#### Send signals to me from another terminal ####
#### Like: kill -HUP 18631
Tick: 21:01:00
GlobalTrap: WINCH at 21:01:05
Window now 58 lines 97 cols
GlobalTrap: WINCH at 21:01:06
Window now 58 lines 92 cols
GlobalTrap: WINCH at 21:01:06
Window now 58 lines 88 cols
Tick: 21:01:10
GlobalTrap: PIPE at 21:01:12
^CGlobalTrap: INT at 21:01:15
21:01:15 (Ctrl-C) (1)
^CGlobalTrap: INT at 21:01:16
21:01:16 (Ctrl-C) (2)
Tick: 21:01:20
^CGlobalTrap: INT at 21:01:20
21:01:20 (Ctrl-C) (3)
$
(3) This run has no extra interrupts, and it shows me entering the Y
at the second [Y] >
prompt, to terminate the "Finishing" timeout early.
paul@paul-RV415-RV515 ~ $ wdog -d 0 -w 1m; ./trapPlay
wdog 6.047| Wed Mar 20 21:02:00.000 2024
wdog 21:02:00.006| Tick
#### Send signals to me from another terminal ####
#### Like: kill -HUP 18662
Tick: 21:02:00
Tick: 21:02:10
GlobalTrap: PIPE at 21:02:12
GlobalTrap: USR2 at 21:02:20
Tick: 21:02:20
GlobalTrap: USR2 at 21:02:29
Tick: 21:02:30
GlobalTrap: TERM at 21:02:36
GlobalTrap: USR2 at 21:02:38
Tick: 21:02:40
GlobalTrap: USR2 at 21:02:47
Tick: 21:02:50
GlobalTrap: USR2 at 21:02:56
Tick: 21:03:00
Tick: 21:03:10
GlobalTrap: EXIT at 21:03:10
Bash time expired -- ready to exit
[Y] > [Y] > Y[Y] >
Exit confirmed
$