Your script creates temporary files, opens connections, or starts background processes. Then something goes wrong: the user hits Ctrl+C, the system runs out of memory, or a command fails. Now you have orphaned files, dangling connections, and zombie processes. There’s a better way.
The Problem
Consider this common scenario:
#!/bin/bash
# Download and process data
wget https://example.com/large-file.zip -O /tmp/data.zip
unzip /tmp/data.zip -d /tmp/data
python process.py /tmp/data
# Clean up
rm -rf /tmp/data.zip /tmp/dataIf the user presses Ctrl+C during processing, the cleanup never runs. Your /tmp directory fills up with garbage. Multiply this by dozens of scripts running daily, and you’ve got a mess.
The Hack: The trap Command
The trap command catches signals (like Ctrl+C, script errors, or normal exits) and executes cleanup code no matter how the script ends:
#!/bin/bash
# Define cleanup function
cleanup() {
echo "Cleaning up..."
rm -rf /tmp/data.zip /tmp/data
echo "Cleanup complete!"
}
# Set trap to call cleanup on EXIT
trap cleanup EXIT
# Your script code
wget https://example.com/large-file.zip -O /tmp/data.zip
unzip /tmp/data.zip -d /tmp/data
python process.py /tmp/dataNow, whether the script completes successfully, fails, or gets interrupted, cleanup always runs!
Understanding Signals
Different signals represent different events:
- EXIT: Script ends (normal or error)
- INT: User presses Ctrl+C
- TERM: System sends termination signal
- ERR: Any command fails (with set -e)
- HUP: Terminal disconnects
Practical Examples
Database Connection Cleanup
#!/bin/bash
cleanup() {
echo "Closing database connection..."
mysql -e "CALL close_session('$SESSION_ID');"
}
trap cleanup EXIT INT TERM
SESSION_ID=$(mysql -e "CALL create_session();" | tail -1)
# Perform database operations
mysql -e "SELECT * FROM large_table WHERE session='$SESSION_ID';"Lock File Management
#!/bin/bash
LOCKFILE="/var/run/myscript.lock"
cleanup() {
rm -f "$LOCKFILE"
echo "Lock file removed"
}
# Check if already running
if [ -e "$LOCKFILE" ]; then
echo "Script is already running!"
exit 1
fi
# Create lock file
touch "$LOCKFILE"
trap cleanup EXIT
# Your script logic here
sleep 10
echo "Work done"Temporary Directory Management
#!/bin/bash
TMPDIR=$(mktemp -d)
cleanup() {
echo "Removing temporary directory: $TMPDIR"
rm -rf "$TMPDIR"
}
trap cleanup EXIT
# Work with temporary files
cd "$TMPDIR"
wget https://example.com/data.tar.gz
tar xzf data.tar.gz
# Process files...Background Process Cleanup
#!/bin/bash
cleanup() {
echo "Killing background processes..."
jobs -p | xargs -r kill
wait
echo "All background jobs terminated"
}
trap cleanup EXIT INT TERM
# Start background jobs
./monitor.sh &
./logger.sh &
./processor.sh &
# Main script work
echo "Running main tasks..."
sleep 30Advanced Techniques
Multiple Trap Handlers
#!/bin/bash
cleanup_exit() {
echo "Script exiting normally"
rm -f /tmp/*.tmp
}
cleanup_interrupt() {
echo "Script interrupted by user!"
rm -f /tmp/*.tmp
exit 130
}
trap cleanup_exit EXIT
trap cleanup_interrupt INT TERMPreserving Exit Status
#!/bin/bash
cleanup() {
local exit_code=$?
echo "Cleaning up... (exit code: $exit_code)"
rm -rf /tmp/workdir
exit $exit_code
}
trap cleanup EXIT
# Your code here
some_command_that_might_fail
another_commandStacking Cleanup Functions
#!/bin/bash
cleanup_stack=()
add_cleanup() {
cleanup_stack+=("$1")
}
run_cleanups() {
for ((i=${#cleanup_stack[@]}-1; i>=0; i--)); do
eval "${cleanup_stack[$i]}"
done
}
trap run_cleanups EXIT
# Add cleanup tasks
add_cleanup "rm -f /tmp/file1.tmp"
add_cleanup "docker stop mycontainer"
add_cleanup "echo 'All done'"
# Your script logicReal-World Use Case: API Testing Script
#!/bin/bash
SERVER_PID=""
DOCKER_CONTAINER=""
cleanup() {
echo "Cleaning up test environment..."
# Stop test server
if [ -n "$SERVER_PID" ]; then
kill $SERVER_PID 2>/dev/null
wait $SERVER_PID 2>/dev/null
fi
# Stop Docker container
if [ -n "$DOCKER_CONTAINER" ]; then
docker stop $DOCKER_CONTAINER
docker rm $DOCKER_CONTAINER
fi
# Remove test data
rm -rf /tmp/test-data
echo "Cleanup complete"
}
trap cleanup EXIT INT TERM
# Start test database
DOCKER_CONTAINER=$(docker run -d -p 5432:5432 postgres:13)
sleep 5
# Start test server
node server.js &
SERVER_PID=$!
sleep 3
# Run tests
npm test
echo "Tests completed!"Error Handling Integration
Combine trap with set -e for robust error handling:
#!/bin/bash
set -euo pipefail
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "ERROR: Script failed with exit code $exit_code"
echo "Performing emergency cleanup..."
fi
rm -rf /tmp/workdir
# Send notification
curl -X POST https://slack.com/api/chat.postMessage \
-d "Script failed: $0"
}
trap cleanup EXIT
# Your commands here
# Any failure will trigger cleanupCommon Patterns
Temporary File Pattern
#!/bin/bash
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
# Use tmpfile safely
echo "data" > $tmpfile
process_file $tmpfileDirectory Change Pattern
#!/bin/bash
original_dir=$(pwd)
trap "cd $original_dir" EXIT
cd /some/other/directory
# Do work hereResource Restoration Pattern
#!/bin/bash
original_umask=$(umask)
trap "umask $original_umask" EXIT
umask 077
# Create sensitive filesDebugging with trap
#!/bin/bash
# Debug every command
trap 'echo "[$BASH_SOURCE:$LINENO] $BASH_COMMAND"' DEBUG
# Your script here
echo "Starting work"
ls /tmp
echo "Done"Pro Tips
- Use EXIT for most cases: It covers all exit scenarios
- Make cleanup idempotent: Safe to run multiple times
- Test your traps: Send signals manually to verify
- Keep cleanup fast: Don’t perform complex operations
- Log cleanup actions: Helps debugging production issues
Common Mistakes to Avoid
# Wrong - trap doesn't work with pipes
some_command | trap cleanup EXIT
# Wrong - quoting issues
trap 'rm -rf $TMPDIR' EXIT # $TMPDIR evaluated when trap is set
# Correct - use double quotes or function
TMPDIR=/tmp/data
trap "rm -rf $TMPDIR" EXIT
# Or use a function
cleanup() { rm -rf "$TMPDIR"; }
trap cleanup EXITWhen to Use trap
- Scripts that create temporary files or directories
- Database or network connection management
- Lock file handling
- Background process management
- Resource allocation (memory, file handles)
- State restoration
Conclusion
The trap command is your safety net. It ensures your scripts clean up after themselves, no matter how they exit. This prevents resource leaks, orphaned processes, and cluttered filesystems.
Professional scripts always use trap. Make it a habit, and your system administrators will thank you!
