Welcome to the sixth part of our technical series on Linux and Bash programming!
In the previous modules, we have learned how to structure scripts, work with variables, use control structures, modularize functions, and control I/O data streams.
In a perfect world, every command runs error-free. In reality of server operations, however, network connections drop, disks fill up, users press Ctrl + C in the middle of an update, or permissions are missing. A script that runs uncontrolled in such moments or leaves temporary files as junk endangers system stability.
In this lesson, you will learn how to protect your scripts against failures: We cover signal traps (trap) for automatic cleanup, the Bash Strict Mode, error catches with ERR trap, advanced debugging techniques with set -x and $PS4, and professional logging via Syslog.
Signal Handling & Cleanup Routines with trap
When a script creates temporary directories, starts background processes, or sets locks, these must be reliably cleaned up on termination – even if the script is aborted early due to an error or by the user pressing Ctrl + C (SIGINT).
The trap command catches operating system signals and executes a defined cleanup function (cleanup handler):
┌─────────────────────────────────────────────────────────────┐
│ SIGNAL HANDLING WITH TRAP │
├─────────────────────────────────────────────────────────────┤
│ │
│ Script runs normally ─────────────────────────────┐ │
│ │ │ │
│ [ User presses Ctrl+C (SIGINT) ] │ │
│ or [ Script terminates normally (EXIT) ] │ │
│ or [ Kill signal received (SIGTERM) ] ▼ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ TRAP HANDLER (cleanup function): │ │
│ │ - Removes /tmp/scratch.XXXXXX │ │
│ │ - Stops background processes │ │
│ │ - Releases lock files │ │
│ └──────────────────────────┬────────────────────────────┘ │
│ │ │
│ ▼ │
│ [ Script terminates cleanly with no remaining residues ] │
│ │
└─────────────────────────────────────────────────────────────┘
The Most Important Signals for trap:
| Signal | Name | Trigger & Meaning |
|---|---|---|
EXIT |
Pseudo-signal | Fires always when the script is terminated (regularly or via exit) |
INT (2) |
SIGINT |
Keyboard abort by the user via Ctrl + C |
TERM (15) |
SIGTERM |
Default termination signal (e.g. by kill or systemd) |
HUP (1) |
SIGHUP |
Terminal window closed or SSH connection dropped |
ERR |
Bash signal | Fires immediately when a command returns an error code ($\neq 0$) |
Practical Example: Safely Working with Temporary Directories
#!/usr/bin/env bash
set -euo pipefail
# 1. Create secure temporary directory
TEMP_DIR=$(mktemp -d /tmp/backup_job.XXXXXX)
# 2. Define cleanup function
cleanup() {
local exit_code=$?
echo "Running cleanup tasks..."
# Remove temporary directory completely
rm -rf "$TEMP_DIR"
echo "Temporary files deleted. Exit code: $exit_code"
exit "$exit_code"
}
# 3. Register trap on EXIT, INT and TERM
trap cleanup EXIT INT TERM
# Main program
echo "Working in directory: $TEMP_DIR"
tar -czf "$TEMP_DIR/data.tar.gz" /etc/hosts
sleep 2
# Regardless of whether the script ends regularly or is aborted here:
# The 'cleanup' function is GUARANTEED to execute!
Precise Error Localization: ERR Trap
With set -e, Bash aborts immediately on errors. However, by default it does not tell you which line the error occurred on.
With an ERR trap and the Bash special variables $LINENO and $BASH_COMMAND, you build a precise crash reporter:
#!/usr/bin/env bash
set -euo pipefail
error_handler() {
local exit_code=$?
local error_line=$1
local error_command=$2
echo "=========================================" >&2
echo "CRITICAL ERROR IN SCRIPT!" >&2
echo "Error line : $error_line" >&2
echo "Command : $error_command" >&2
echo "Exit code : $exit_code" >&2
echo "=========================================" >&2
exit "$exit_code"
}
# Register trap: Pass line number and command to handler
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
echo "Step 1: Initialization..."
echo "Step 2: Attempting invalid command..."
# This command will fail:
ls /path/that_definitely_does_not_exist
echo "Step 3: Will never be reached."
Output on crash:
Step 1: Initialization...
Step 2: Attempting invalid command...
ls: cannot access '/path/that_definitely_does_not_exist': No such file or directory
=========================================
CRITICAL ERROR IN SCRIPT!
Error line : 23
Command : ls /path/that_definitely_does_not_exist
Exit code : 2
=========================================
Debugging Techniques: Tracing the Error
When a script produces unexpected results, powerful analysis tools are at your disposal:
1. Trace Mode (-x) and Fine-tuning with $PS4
By default, set -x displays a simple + before each command. By adjusting the special variable $PS4, Bash displays file names, exact line numbers, and function names at each step:
#!/usr/bin/env bash
set -euo pipefail
# Extended trace prompt for professional debugging:
export PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
# Enable debugging selectively for a block:
set -x
a=10
b=20
sum=$(( a + b ))
set +x # Disable debugging again
echo "Calculation complete: $sum"
2. Syntax Check Without Execution (bash -n)
Checks scripts purely for syntactic correctness (e.g. unclosed quotes, forgotten done or fi keywords), without executing dangerous commands:
bash -n deploy_production.sh
3. Static Analysis with ShellCheck
# Check script for best practices and common pitfalls:
shellcheck deploy_production.sh
Structured Logging: File and Syslog Integration
Professional administration scripts do not output status messages unformatted with echo, but use uniform log levels (INFO, WARN, ERROR) and integrate with the Linux system journal (syslog):
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="/tmp/app_maintenance.log"
APP_NAME="BackupService"
log_msg() {
local level="$1"
shift
local msg="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 1. Write to log file
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
# 2. Send to system journal / syslog (visible via journalctl):
logger -t "$APP_NAME" "[$level] $msg"
# 3. Display colored in terminal
case "$level" in
INFO) echo -e "\e[1;32m[INFO]\e[0m $msg" ;;
WARN) echo -e "\e[1;33m[WARN]\e[0m $msg" >&2 ;;
ERROR) echo -e "\e[1;31m[ERROR]\e[0m $msg" >&2 ;;
esac
}
log_msg "INFO" "Maintenance task started."
log_msg "WARN" "High CPU usage detected (88%)."
log_msg "ERROR" "Could not establish database connection!"
Messages sent with logger can be filtered system-wide with journalctl -t BackupService.
Exercises & Practice Check
❗ Practice tasks for lesson #6:
- Task 1 (Signal Trap & Lockfile):
Write a script
single_instance.shthat creates a lockfile/tmp/my_app.lockon startup and ensures viatrapthat this file is deleted on termination (whether regular or viaCtrl + C).
- Task 2 (Crash Handler with Alert):
Implement an
ERRtrap that stores the exact line number in/tmp/crash.logon error and displays a structured warning to the user.
- Task 3 (Custom PS4 Debugger):
Adjust
$PS4so that the current time in format[HH:MM:SS]is displayed before each command.
Sample Solution for Task 1:
#!/usr/bin/env bash
set -euo pipefail
LOCK_FILE="/tmp/single_instance.lock"
# Check if an instance is already running
if [[ -f "$LOCK_FILE" ]]; then
echo "Error: Script already running (lockfile $LOCK_FILE exists)!" >&2
exit 1
fi
# Create lockfile
touch "$LOCK_FILE"
# Guarantee cleanup
cleanup() {
rm -f "$LOCK_FILE"
echo "Lockfile released."
}
trap cleanup EXIT INT TERM
echo "Script running exclusively. Press Ctrl+C to test the trap..."
sleep 5
echo "Work completed successfully."
Command Reference (Cheatsheet)
| Command / Syntax | Function & Description |
|---|---|
trap 'cleanup' EXIT INT TERM |
Executes cleanup on termination or signal interruption |
trap 'error_func $LINENO' ERR |
Catches command errors and passes the error line number |
set -euo pipefail |
Activates the full Bash Strict Mode |
set -x / set +x |
Enables / disables the interactive execution trace |
export PS4='+ [${LINENO}] ' |
Defines the format of the debugging trace output |
bash -n script.sh |
Checks script for syntax errors without execution |
logger -t TAG "text" |
Writes a log message directly to the Linux system journal |
$BASH_COMMAND |
Contains the currently failed command in the ERR trap |
Further Resources
| Resource | Description |
|---|---|
| GNU Bash Trap Builtin Reference | Official GNU documentation on signal handling with trap |
| Bash Basics #5: Input and Output in Bash | The previous part: Data streams, pipes, and here-docs |
| Linux Command Line Processor Guide | Fundamental knowledge on shells, I/O streams, and pipes |
| chmod and File Permissions | Linux permission concepts in detail |
Conclusion
Robust scripts differ from simple hobby scripts through predictable error handling and clean resource management. Through deliberate use of signal traps (trap), the Bash Strict Mode, and detailed logging, you protect your servers from orphaned lockfiles, uncleaned data residue, and unnoticed errors in the background.
💡 Tip: For all scripts that create temporary files in
/tmpor lockfiles, immediately add atrap cleanup EXIT INT TERMafter creation. This prevents aborted cron jobs from filling the disk with leftover junk.
In the seventh and final part of our series, we combine our knowledge and focus on advanced professional techniques: 👉 Next up: Bash Basics #7: Advanced Bash Techniques
👉 Course overview: All lessons of the Bash basics course