Shell Script Hack #3: Bulletproof Cleanup with the trap Command

Shell Script Hack #3: Bulletproof Cleanup with the trap Command

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/data

If 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/data

Now, 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 30

Advanced 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 TERM

Preserving 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_command

Stacking 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 logic

Real-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 cleanup

Common Patterns

Temporary File Pattern

#!/bin/bash
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT

# Use tmpfile safely
echo "data" > $tmpfile
process_file $tmpfile

Directory Change Pattern

#!/bin/bash
original_dir=$(pwd)
trap "cd $original_dir" EXIT

cd /some/other/directory
# Do work here

Resource Restoration Pattern

#!/bin/bash
original_umask=$(umask)
trap "umask $original_umask" EXIT

umask 077
# Create sensitive files

Debugging 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 EXIT

When 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!

References

Written by:

472 Posts

View All Posts
Follow Me :