Logging and Error Checking in Bash Scripts

L

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 with logrotate.

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() and on_error() using a testing framework like bats.
  • 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.

Add Comment

Recent Posts

Archives

About

middle aged linux nerd. likes coding and pizza. owner of this particular site.