Bash’s trap command lets you intercept signals that would normally terminate your script and run custom code instead, giving you a chance to clean up.
Let’s see this in action. Imagine a script that downloads a large file and we want to ensure the partially downloaded file is removed if the user presses Ctrl+C.
#!/bin/bash
# The file we're "downloading"
DOWNLOAD_FILE="large_download.tmp"
URL="http://example.com/large_file.zip" # Replace with a real URL for testing
# Cleanup function
cleanup() {
echo "" # Newline after ^C
echo "Caught signal. Cleaning up..."
if [ -f "$DOWNLOAD_FILE" ]; then
echo "Removing partial file: $DOWNLOAD_FILE"
rm -f "$DOWNLOAD_FILE"
fi
exit 0 # Exit cleanly
}
# Trap SIGINT (Ctrl+C) and SIGTERM (sent by 'kill' command)
trap cleanup SIGINT SIGTERM
echo "Starting download of $URL to $DOWNLOAD_FILE..."
# Simulate a long download
# In a real scenario, this would be curl, wget, etc.
# Using 'sleep' to simulate long-running process
for i in {1..10}; do
echo -n "."
sleep 1
done
echo ""
echo "Download complete."
# Clean up the temporary file if download finished successfully
if [ -f "$DOWNLOAD_FILE" ]; then
echo "Removing temporary file: $DOWNLOAD_FILE"
rm -f "$DOWNLOAD_FILE"
fi
echo "Script finished successfully."
exit 0
When you run this script and press Ctrl+C, you’ll see:
Starting download of http://example.com/large_file.zip to large_download.tmp...
..........
Caught signal. Cleaning up...
Removing partial file: large_download.tmp
This trap command intercepts the SIGINT (the signal sent when you press Ctrl+C) and SIGTERM (the signal sent by the kill command by default) and redirects execution to the cleanup function. Inside cleanup, we check if the temporary download file exists and remove it before exiting gracefully. If the script completes without interruption, the cleanup function isn’t called, and the file is handled by the script’s normal exit logic.
The core problem trap solves is preventing abrupt termination from leaving the system in an inconsistent state. Without it, pressing Ctrl+C might leave temporary files, orphaned processes, or unclosed network connections. By defining a trap for relevant signals, you can ensure your script performs a controlled shutdown, freeing resources and leaving no messy artifacts. The trap command takes a command string or function name as its first argument and one or more signal names as subsequent arguments. When any of the specified signals are received by the script, the associated command or function is executed.
You can see the currently trapped signals and their associated commands by running trap with no arguments. This is invaluable for debugging or understanding what’s already in place, especially when dealing with complex scripts or functions that might themselves set traps.
trap - SIGNAL_NAME can be used to clear a trap for a specific signal. This is useful if you want to allow a signal to propagate normally for a certain section of your script, or if you’re inheriting traps from a parent process that you don’t want to be active anymore. For example, trap - SIGINT would disable the trap for Ctrl+C.
The EXIT signal is a special case. It’s not a signal in the traditional Unix sense but rather a pseudo-signal that trap can respond to. A trap set for EXIT will execute whenever the script exits, regardless of whether it’s a normal exit, an error exit, or due to a signal that was trapped and handled. This is the most universal cleanup mechanism.
trap "echo 'Something went wrong'" ERR is a common pattern to catch non-zero exit codes. The ERR signal is sent by bash whenever a command fails (returns a non-zero exit status), provided the errexit option (set -e) is active. This allows you to log the error, perform specific cleanup, or even re-throw the error.
When you trap a signal like SIGINT, the trap handler itself can be interrupted by another signal. If your cleanup function were to take a long time and you pressed Ctrl+C again while cleanup was running, the default action for SIGINT (termination) would occur unless you also trapped SIGINT within the cleanup function, or if the cleanup function itself exits cleanly.
The order of traps matters if multiple signals are trapped. If SIGINT and SIGTERM are trapped by the same handler, and both signals are sent in rapid succession, the handler will execute once. However, if you trap them separately, the first one to arrive triggers its handler.
The trap command operates on the current shell process. If you have subshells or call other scripts, traps set in the parent script might not be inherited or might be overridden. You need to be mindful of the scope of your traps.
You can also use trap '' SIGNAL_NAME to ignore a signal, effectively setting the trap to an empty command. This is different from trap - SIGNAL_NAME which removes the trap; trap '' SIGNAL_NAME sets the trap to do nothing. This is useful for preventing a signal from being handled by the shell’s default behavior or by any other traps.
The next concept you’ll likely encounter is managing job control and background processes within scripts that utilize trap, as signals can behave differently when processes are running in the background.