Introduction
When your automation grows beyond a handful of lines, ad-hoc echo’s and unchecked failures become a maintenance headache. In this guide, you’ll learn how to build:
- a configurable logging function with levels, colors, and optional file output
- a robust error-checking framework using exit codes and traps
- real-world examples to tie it all together
1. Why Abstract Logging & Error Checking?
- Consistency: uniform timestamps & formats across scripts.
- Configurability: control verbosity, output destinations, and colors via env vars.
- Readability: core logic stays clean; logging & error logic lives in functions.
- Maintainability: tweak behavior in one place, not dozens of scripts.
2. Building a Robust Logging Function
Here’s a bash snippet that supports levels, colors, and an optional log file:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# Configuration (override via environment)
LOG_LEVEL="${LOG_LEVEL:-INFO}" # DEBUG, INFO, WARN, ERROR
LOG_FILE="${LOG_FILE:-}" # e.g. "/var/log/myscript.log"
COLORIZE="${COLORIZE:-true}" # set to "false" to disable ANSI colors
# Define ANSI codes
declare -A LEVEL_COLOR=(
[DEBUG]='\e[36m' # cyan
[INFO]='\e[32m' # green
[WARN]='\e[33m' # yellow
[ERROR]='\e[31m' # red
)
RESET_COLOR='\e[0m'
# Function: log
log() {
local level="$1"; shift
local msg="$*"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
# Filter out messages below the configured LOG_LEVEL
local levels=(DEBUG INFO WARN ERROR)
if (( ${levels[@]/$level//} < ${levels[@]/$LOG_LEVEL//} )); then
return
fi
# Build formatted message
if [[ "$COLORIZE" == "true" && -t 1 ]]; then
printf "%b [%s] [%s] %s%b\n" \
"${LEVEL_COLOR[$level]}" "$ts" "$level" "$msg" "$RESET_COLOR"
else
printf "[%s] [%s] %s\n" "$ts" "$level" "$msg"
fi
# Append to file if set
if [[ -n "$LOG_FILE" ]]; then
printf "[%s] [%s] %s\n" "$ts" "$level" "$msg" >>"$LOG_FILE"
fi
}
Tips:
- Use
LOG_LEVEL=DEBUG
for verbose output in development. - Disable color in CRON jobs by exporting
COLORIZE=false
. - Rotate
LOG_FILE
withlogrotate
.
3. Advanced Error Checking with Traps
Instead of sprinkling if…exit
after every command, leverage trap ERR
and a shared handler:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# Import or define log() from previous section…
# on_error: called when any command fails
on_error() {
local exit_code=$?
local last_cmd="${BASH_COMMAND:-unknown}"
local line_no="${BASH_LINENO[0]:-unknown}"
log ERROR "Command '${last_cmd}' failed at line ${line_no} (exit code: ${exit_code})"
cleanup
exit "$exit_code"
}
# cleanup: optional teardown logic
cleanup() {
log INFO "Performing cleanup before exit"
# e.g. remove temp files, unmount drives, etc.
}
# Register the trap
trap 'on_error' ERR
# Example function that may fail
download_archive() {
curl -fSL "https://example.com/archive.tar.gz" -o /tmp/archive.tar.gz
}
main() {
log INFO "Starting deployment"
download_archive
log INFO "Extracting archive"
tar xzf /tmp/archive.tar.gz -C /opt/app
log INFO "Deployment complete"
}
main "$@"
How it works:
trap 'on_error' ERR
catches any non-zero exit (even in pipes).BASH_COMMAND
&BASH_LINENO
help pinpoint the failure.- cleanup() gives you a chance to undo partial work.
4. Real-World Script Example
Putting it all together in a single deploy script:
#!/usr/bin/env bash
# [Insert set-o options here]
# Load logging & error-handling functions (or source "lib.sh")
# Example: override defaults
export LOG_LEVEL="${LOG_LEVEL:-DEBUG}"
export LOG_FILE="/var/log/deploy.log"
trap 'on_error' ERR
main() {
log INFO "Deployment started at $(date)"
mkdir -p /opt/app/releases/"$(date +%Y%m%d_%H%M%S)"
download_archive
check_error $? "download_archive failed"
log INFO "Setting up symlink"
ln -sfn /opt/app/releases/"$(date +%Y%m%d_%H%M%S)" /opt/app/current
check_error $? "symlink creation failed"
log INFO "Restarting service"
systemctl restart myapp
check_error $? "service restart failed"
log INFO "Deployment succeeded"
}
main "$@"
5. Additional Best Practices
- Use separate config file or
--config
flag for environment-specific settings. - Send critical ERROR logs to external systems (syslog, ELK, Slack notifications).
- Write unit tests for
log()
andon_error()
using a testing framework likebats
. - Don’t embed secrets in logs; respect
chmod 600
on sensitive files. - Document functions with header comments (purpose, inputs, outputs).
Conclusion
By centralizing logging and error-handling into reusable functions and leveraging traps you’ll dramatically improve the reliability and maintainability of your Bash scripts.
Start small: extract your next script’s echo
and if
checks into log()
and on_error()
, then iterate from there.