4. Signals and traps โ
๐ฏ What you will learn in this chapter:
- Unix signals โ SIGINT, SIGTERM, SIGKILL, SIGHUP, and others
- Catching signals with
trap- The
EXITpseudo-signal โ the most important cleanup mechanism- Cleanup pattern โ temp files, lock files, connections
- Graceful shutdown โ clean termination even when Ctrl+C is pressed
- Background processes โ
&,wait, signal propagationโฑ Time: ~25 minutes ๐งช Exercises:
bashlings watchโ 5 interactive exercises ready (exercises/09_traps/)
4.1. Why do you need this? โ
Imagine the following situations:
Your script was writing data to a 100MB temporary file. The user pressed
Ctrl+C. The file was left behind โ it cluttered the disk.A database backup script was running. The terminal was closed. A half-finished backup and an open connection were left hanging.
A server-monitoring script was running under
cron. The system rebooted. The PID file remained, and the next run incorrectly reported "already running."
All of these can be prevented with trap and signals.
Core idea
Never hope that things "finished successfully." Your script can be stopped at any time โ Ctrl+C, the terminal closing, the OOM killer, kill -9. You must guarantee cleanup.
4.2. Unix signals โ a brief model โ
A signal is an asynchronous message from one process to another (or from the kernel). The user's Ctrl+C is a signal. The kill command sends a signal. Controlling how the system runs โ done through signals.
Signals โ each has a number and a name โ
kill -l
# 1) SIGHUP 2) SIGINT 3) SIGQUIT ...
# 9) SIGKILL 15) SIGTERM ...Signals you'll need โ
| Signal | Number | Meaning | Can it be trapped? |
|---|---|---|---|
SIGINT | 2 | Ctrl+C โ a polite request to stop | โ |
SIGTERM | 15 | "Please stop" (default kill PID) | โ |
SIGHUP | 1 | Terminal closed (hangup) | โ |
SIGQUIT | 3 | Ctrl+\ โ stop with a core dump | โ |
SIGUSR1 | 10/30 | User-defined #1 | โ |
SIGUSR2 | 12/31 | User-defined #2 | โ |
SIGCHLD | 17/20 | A child process changed state | โ |
SIGPIPE | 13 | The pipe being written to was closed | โ |
SIGALRM | 14 | A timer fired | โ |
SIGKILL | 9 | Kill immediately โ you cannot resist it | โ NO |
SIGSTOP | 19/17 | Pause immediately โ you cannot resist it | โ NO |
EXIT | (0) | Bash pseudo-signal โ on any kind of exit | โ (most important) |
SIGKILL can never be caught
kill -9 PID kills the process immediately. Cleanup code does not run. That is why, for critical situations, you need "external guardian" processes.
4.3. kill โ sending a signal โ
# Default โ sends SIGTERM (15)
kill 1234
# A specific signal
kill -SIGINT 1234
kill -INT 1234 # you can drop the SIG prefix
kill -2 1234 # by number
# The strongest โ cannot be resisted
kill -9 1234
kill -KILL 1234
# By name (process name)
killall my-script.sh
pkill -f "long pattern"
# PID of the current process
echo $$ # for bashkill -0 โ check only โ
# Is the process alive?
if kill -0 1234 2>/dev/null; then
echo "Hali ishlayapti"
fi-0 does not send a signal; it only checks permissions.
4.4. trap basics โ
trap defines what to do when a signal is received.
Syntax โ
trap '<code to run>' SIGNAL [SIGNAL ...]First example โ
#!/usr/bin/env bash
trap 'echo "โ Ctrl+C bosildi, lekin men tirikman!"' SIGINT
echo "5 soniya kutaman..."
for i in {1..5}; do
echo " $i"
sleep 1
done
echo "Yakunlandi"Run it and press Ctrl+C โ the script warns you and keeps going.
Viewing traps โ
trap -p # list of currently set traps
trap -p SIGINT # only for SIGINTRemoving a trap (restoring the default) โ
trap - SIGINT # SIGINT default behavior (kill)
trap - EXIT INT TERM # several togetherIgnoring a trap entirely โ
trap '' SIGINT # Ctrl+C does nothing at allIgnoring is dangerous
trap '' SIGINT cancels the user's Ctrl+C. This is questionable UX. Use it only in special, documented situations.
4.5. EXIT โ the most important pseudo-signal โ
EXIT is not a real Unix signal; it is bash's special pseudo-signal. It runs whenever the script ends by any means:
- Normal termination (the last command ran)
- The
exitcommand was called - It stopped with an error (under
set -e) - A signal was received (
SIGINT,SIGTERM)
A win for cleanup
This very property makes EXIT the perfect place for cleanup. You write it once โ and it runs in every case.
The classic pattern โ
#!/usr/bin/env bash
set -euo pipefail
tmpfile=$(mktemp)
cleanup() {
rm -f "$tmpfile"
echo "๐งน Tozalandi"
}
trap cleanup EXIT
# Asosiy ish
echo "ma'lumot" > "$tmpfile"
# ... other code ...
# `cleanup` is called no matter whatTry it:
- Finish normally โ cleanup runs
- Press
Ctrl+Cโ cleanup runs - Add
exit 1โ cleanup runs
4.6. The best cleanup pattern โ
Most scripts catch Ctrl+C with a separate message and use EXIT for cleanup. This is the cleanest model:
#!/usr/bin/env bash
set -euo pipefail
tmpfile=$(mktemp)
lockfile="/tmp/myapp.lock"
cleanup() {
local rc=$?
rm -f "$tmpfile"
rm -f "$lockfile"
if [[ $rc -ne 0 ]]; then
echo "โ Skript $rc kodi bilan tugadi"
fi
}
# 1) EXIT โ cleanup in every case
trap cleanup EXIT
# 2) INT / TERM โ graceful exit (the EXIT trap also runs)
trap 'echo "โ to`xtatish so`rovi qabul qilindi"; exit 130' INT TERM
# Asosiy ish
echo $$ > "$lockfile"
echo "ma'lumot" > "$tmpfile"
for i in {1..30}; do
echo "Qadam $i"
sleep 1
done
echo "โ
Yakunlandi"What runs when? โ
| Event | INT trap | EXIT trap | Cleanup called? |
|---|---|---|---|
| Normal termination | โ | โ | Yes |
Ctrl+C | โ
โ exit 130 | โ | Yes |
kill PID | TERM โ exit 130 | โ | Yes |
kill -9 PID | โ not caught | โ | No โ |
exit 1 inside script | โ | โ | Yes |
Exit code convention
130=128 + SIGINT(2)โ the result of Ctrl+C143=128 + SIGTERM(15)โ the result of kill
This is a Unix convention. It is useful for programmatic checks.
4.7. Multiple cleanups and the done flag โ
If you need to guard against cleanup being called twice (for example, when Ctrl+C is followed by exit 1 as well):
__cleaned=0
cleanup() {
[[ $__cleaned -eq 1 ]] && return
__cleaned=1
rm -f "$tmpfile"
rm -f "$lockfile"
}
trap cleanup EXITThis is the idempotent cleanup pattern.
4.8. SIGKILL โ you cannot resist it โ
trap 'echo "haa"' SIGKILL # โ bash xato beradiBash will not even allow you to write this. The reason: SIGKILL and SIGSTOP are kernel-level signals, and a user process cannot block them by any means.
The real rules โ
kill PID(SIGTERM) โ a polite request. The script can clean up and exit.kill -9 PID(SIGKILL) โ using force. Cleanup does not run.
Production behavior
- First approach: a polite
SIGTERM - Wait a few seconds (for cleanup)
- If nothing happens:
SIGKILL(last resort)
systemctl stop works exactly on this model.
4.9. Background processes and wait โ
In scripts that work asynchronously โ background processes launched with & โ special attention is needed.
& and $! โ
sleep 30 &
echo "Background PID: $!" # the most recent background PIDwait โ waiting โ
sleep 5 &
pid1=$!
sleep 3 &
pid2=$!
wait $pid1 $pid2
echo "Ikkalasi ham tugadi"Signal propagation โ
By default, if the parent script receives Ctrl+C, the child processes are not killed automatically. You have to direct it explicitly:
#!/usr/bin/env bash
cleanup() {
echo "Child'larni o'ldiraman..."
jobs -p | xargs -r kill 2>/dev/null
}
trap cleanup EXIT INT TERM
# Several workers
worker.sh &
worker.sh &
worker.sh &
waitjobs -p โ all background PIDs. xargs -r kill โ kills them.
wait -n (Bash 4.3+) โ
Wait for the first child to finish:
worker1 &
worker2 &
worker3 &
# We wait for any one of them to finish
wait -n
echo "Bittasi tugadi"4.10. The graceful shutdown pattern โ
For long-running scripts (server, monitor, daemon), you need to accept a stop request smoothly.
#!/usr/bin/env bash
#
# monitor.sh โ check disk status every 5 seconds
#
set -euo pipefail
running=1
shutdown() {
echo "Shutdown so'rovi qabul qilindi, joriy iteratsiyani yakunlayman..."
running=0
}
trap shutdown INT TERM
while [[ $running -eq 1 ]]; do
df -h / | tail -1
sleep 5
done
echo "โ
Toza yakunlanish"What does this do? โ
- When
Ctrl+Cis pressed โ it setsrunning=0 - The current iteration finishes (the
dfoutput is complete) - The next
whilecheck returnsfalse - The script terminates cleanly
exit is not called โ we finish whatever work is in progress and then exit.
4.11. The ERR and DEBUG traps for debugging โ
ERR โ on every error โ
#!/usr/bin/env bash
set -e
trap 'echo "โ Xato $LINENO qatorida: $BASH_COMMAND"' ERR
echo "Ish boshlandi"
ls /yoq-katalog # bu yerda xato bo'ladi
echo "Bu satrgacha yetmaydi"When run:
Ish boshlandi
ls: cannot access '/yoq-katalog': No such file or directory
โ Xato 6 qatorida: ls /yoq-katalogDEBUG โ before every command โ
trap 'echo ">> [LINE $LINENO] $BASH_COMMAND"' DEBUG
x=5
echo "salom"
lsResult:
>> [LINE 3] x=5
>> [LINE 4] echo "salom"
salom
>> [LINE 5] ls
...The difference from set -x
set -x also prints every command, but the DEBUG trap lets you format the output however you want.
4.12. A real example โ a robust backup script โ
A complete example that combines all the concepts:
#!/usr/bin/env bash
#
# backup.sh โ Ctrl+C, kill, or a normal finish โ it handles all of them cleanly
#
set -euo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOCKFILE="/tmp/${SCRIPT_NAME%.sh}.lock"
TMPDIR=""
# --- Lock check (single-instance pattern) ---
if [[ -f "$LOCKFILE" ]]; then
pid=$(cat "$LOCKFILE")
if kill -0 "$pid" 2>/dev/null; then
echo "โ Backup allaqachon ishlamoqda (PID: $pid)" >&2
exit 1
else
echo "โ Eski stale lock topildi, tozalanmoqda"
rm -f "$LOCKFILE"
fi
fi
echo $$ > "$LOCKFILE"
# --- Cleanup function ---
__cleaned=0
cleanup() {
local rc=$?
[[ $__cleaned -eq 1 ]] && return
__cleaned=1
echo ""
echo "๐งน Tozalanmoqda..."
[[ -n "$TMPDIR" && -d "$TMPDIR" ]] && rm -rf "$TMPDIR"
rm -f "$LOCKFILE"
if [[ $rc -eq 0 ]]; then
echo "โ
Muvaffaqiyatli yakunlandi"
elif [[ $rc -eq 130 ]]; then
echo "โ Foydalanuvchi to'xtatdi (Ctrl+C)"
else
echo "โ Xato bilan tugadi (exit=$rc)"
fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
# --- Main work ---
TMPDIR=$(mktemp -d)
echo "๐ Vaqtinchalik katalog: $TMPDIR"
echo "๐ฆ Backup boshlandi ($(date '+%T'))"
for i in {1..10}; do
echo " Qadam $i / 10"
sleep 1
done
archive="$HOME/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
echo "๐พ Arxivga yozilmoqda: $archive"
# tar -czf "$archive" -C "$TMPDIR" . # the real command
echo "๐ Hajmi: $(du -sh "$TMPDIR" 2>/dev/null | cut -f1)"This script:
- Guarantees a single instance via a lock file (checks for stale locks with
kill -0) trap cleanup EXITโ cleans up on any kind of exitINTโ exit 130 andTERMโ exit 143 โ the Unix convention- The
__cleanedflag โ prevents being called twice - A temporary directory with
mktemp -dโ safe and unique - Color/emoji โ clear feedback for the user
4.13. Common mistakes โ
Classic traps
Trying to trap
SIGKILL. Impossible.kill -9bypasses cleanup. Use an external guardian (systemd,supervisord).Raising a new error inside cleanup. Under
set -e, an error inside cleanup drops the following lines.cleanup() { rm -f "$tmpfile" || true; ... }.EXIT trap indicators. When
cleanupis called,$?is the exit code of the most recent command. Capture it on the trap's first line withlocal rc=$?.Background processes left without a trap. When the parent dies, the children become orphans. Protect against this with
trap 'kill $(jobs -p) 2>/dev/null' EXIT.Using a variable in a trap โ single vs. double quote.
bashtrap "echo $tmpfile" EXIT # โ substituted when the trap is SET trap 'echo $tmpfile' EXIT # โ when the trap is CALLEDThis is a subtle difference! Writing it through a
cleanupfunction is safer.Forgetting
exitinside cleanup.bashcleanup() { rm -f "$tmp"; } # OK โ in the EXIT trap case trap 'cleanup; exit 130' INT # โ exit is also needed, otherwise it continuesA lock file left in a stale, uncleaned state. If the script crashes, the lock remains. Always check with
kill -0 $pidand remove it if necessary.
4.14. Exercises โ
๐งช Bashlings โ interactive exercises
This chapter's 5 exercises are available through the bashlings CLI with automatic checking:
bashlings watch # start from the first pending exercise
bashlings run trap1 # check a single exercise
bashlings hint trap1 # step-by-step hintSource: exercises/09_traps/
And try the following conceptual exercises by hand on your own:
Countdown โ write a script that prints from 10 down to 1, one per second. If
Ctrl+Cis pressed, it should print "sorry, couldn't continue" and return exit 130.tmp-safeโ create a file withmktemp, write 5 random numbers into it, and delete it in every case. Cleanup should be done through an EXIT trap.Single instance โ a script that uses a lock file to forbid a second instance from starting. It should automatically detect a stale lock.
Parallel workers โ a parent script that launches 3 background workers. On
Ctrl+Cit should kill them all (jobs -p | xargs kill).ERR trace โ write a script that uses
set -eandtrap '... $LINENO $BASH_COMMAND ...' ERRto print the line number and the command when an error occurs.
4.15. Summary โ
| Concept | Key point |
|---|---|
| What a signal is | An asynchronous message between processes |
SIGINT (2) | Ctrl+C โ the most common |
SIGTERM (15) | Default kill โ a polite request to stop |
SIGKILL (9) | Cannot be trapped โ no cleanup either |
EXIT | Bash pseudo-signal โ the most important cleanup point |
trap code SIGNAL | Set up a reaction to a signal |
trap - SIGNAL | Restore the default |
trap '' SIGNAL | Ignore it |
trap -p | List of current traps |
| Exit code 130 | The result of Ctrl+C (128+SIGINT) |
| Exit code 143 | The result of kill (128+SIGTERM) |
kill -0 PID | Check whether a process is alive |
$! | The most recent background PID |
5 key ideas โ
- Never hope that things "succeeded." Guarantee cleanup with
trap EXIT. - The EXIT pseudo-signal is one of the most powerful features of the bash world. Use it.
SIGKILLcannot be caught. For critical situations you need an external guardian.- Single instance + lock + kill -0 is the standard pattern for production scripts.
- Graceful shutdown โ with the
while [[ $running -eq 1 ]]model, finish each iteration before exiting.
๐ Now your scripts will terminate cleanly in any situation. In the next and final chapter, we will learn to write production-grade scripts using set -euo pipefail, ShellCheck, and getopts.
Next page: 5. Robust scripting โ