The ERR trap fires on every non-zero exit status when set -e is active. In pipelines with pipefailThe trap fires for the failing command, not the pipeline as a whole. Avoid relying on ERR trap in complex pipelines — explicit error checking is more reliable.
pid_file="/var/run/myscript.pid"
echo "Caught signal, cleaning up..."
[[ -n "$temp_dir" ]] && rm -rf "$temp_dir"
[[ -f "$pid_file" ]] && rm -f "$pid_file"
trap cleanup EXIT INT TERM HUP QUIT
| Signal | Number | Description | Default Action |
|---|
| SIGHUP | 1 | Terminal hangup | Terminate |
| SIGINT | 2 | Interrupt (Ctrl+C) | Terminate |
| SIGQUIT | 3 | Quit (Ctrl+) | Core dump |
| SIGKILL | 9 | Kill (cannot be caught) | Terminate |
| SIGTERM | 15 | Termination signal | Terminate |
| SIGUSR1 | 10 | User-defined signal 1 | Terminate |
| SIGUSR2 | 12 | User-defined signal 2 | Terminate |
| SIGPIPE | 13 | Broken pipe | Terminate |
| SIGSTOP | 19 | Stop (cannot be caught) | Stop |
| SIGTSTP | 20 | Stop (Ctrl+Z) | Stop |
kill -9 12345 # SIGKILL — cannot be caught, blocked, or ignored
apt-get install shellcheck # Debian/Ubuntu
dnf install ShellCheck # Fedora/RHEL
brew install shellcheck # macOS
shellcheck -x script.sh # follow sourced files
# Ignore specific warnings
# shellcheck disable=SC2086
Common shellcheck warnings and fixes:
| Code | Issue | Fix |
|---|
| SC2086 | Double-quote to prevent globbing | "$VAR" instead of $VAR |
| SC2004 | `/{} is unnecessary on arithmetic vars | ((count++)) instead of $count |
| SC2181 | Check exit code directly | if mycmd; instead of if $? |
| SC2034 | Variable appears unused | Remove or use the variable |
| SC2155 | Declare and assign separately | Split local a=$(cmd) into two |
| SC1091 | Not following sourced file | Add # shellcheck source=... |
# Always quote variable expansions
"$@" # correct — preserves argument boundaries
"${arr[@]}" # correct for arrays
"${!arr[@]}" # correct for array keys
# Quote everything unless you explicitly want word splitting or globbing
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2; }
die() { log "FATAL: $*"; exit 1; }
log "Starting deployment"
validate_config || die "Invalid configuration"
run_tests || die "Tests failed"
deploy || die "Deployment failed"
log "Deployment complete"
[[ -f config.yaml ]] || return 1
kubectl apply -f manifests/ || return 1
# POSIX sh compatible — maximum portability
# No arrays, no associative arrays
# No process substitution
# No ${var^^} case conversion
# Bash — when you know the target has bash
# Can use [[ ]], (( )), arrays, process substitution
# Use bash 4.0+ features only when target is guaranteed
# Alpine Linux has bash 5.x by default
# macOS ships bash 3.2 (old license) — avoid bash 4+ features for macOS
Usage: deploy.sh [OPTIONS]
-e, --env ENV Deployment environment (default: staging)
-v, --version VER Version to deploy (default: latest)
-d, --dry-run Show what would be deployed
-h, --help Show this help message
-e|--env) env="$2"; shift 2 ;;
-v|--version) version="$2"; shift 2 ;;
-d|--dry-run) dry_run=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
echo "Remaining args: $*"
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/myscript/config"
# Load config with defaults
[output_dir]="/var/log/myscript"
if [[ -f "$CONFIG_FILE" ]]; then
while IFS='=' read -r key value; do
[[ "$key" =~ ^[[:space:]]*# ]] && continue # skip comments
[[ -z "$key" ]] && continue # skip blank lines
key="${key%%#*}" # strip inline comments
key="${key%"${key##*[![:space:]]}"}" # trim trailing whitespace
value="${value#"${value%%[![:space:]]*}"}" # trim leading whitespace
for key in "${!CONFIG[@]}"; do
printf 'CONFIG[%s] = %s\n' "$key" "${CONFIG[$key]}"
LOG_LEVEL="${LOG_LEVEL:-INFO}"
declare -A LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)
(( LEVELS[$level] >= LEVELS[$LOG_LEVEL] )) || return 0
printf '[%s] [%-5s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$level" "$*" >&2
log_debug() { log DEBUG "$@"; }
log_info() { log INFO "$@"; }
log_warn() { log WARN "$@"; }
log_error() { log ERROR "$@"; }
log_fatal() { log FATAL "$@"; exit 1; }
log_info "Starting process"
log_debug "Variable value: $variable"
log_warn "Retrying operation (attempt $attempt/$max_retries)"
log_error "Failed to connect to $host:$port"
log_fatal "Configuration file not found: $config_path"
for f in "${tmp_files[@]:-}"; do
if [[ -n "$tmp_dir" ]]; then
# Create a single temp file
tmp_file=$(mktemp /tmp/script.XXXXXX)
# Create a temp directory
tmp_dir=$(mktemp -d /tmp/script.XXXXXX)
# Use temp directory safely
echo "data" > "$tmp_dir/input.txt"
process < "$tmp_dir/input.txt" > "$tmp_dir/output.txt"
result=$(cat "$tmp_dir/output.txt")
arr=("file with spaces.txt" "another file.txt")
# WRONG — word splitting on spaces
# CORRECT — preserves array element boundaries
# This function returns non-zero as part of normal logic
# WRONG — script exits if package is not installed
# CORRECT — handle the return code explicitly
if is_installed nginx; then
echo "nginx is installed"
echo "nginx is not installed"
# CORRECT — or use || true
is_installed nginx || echo "nginx not installed"
# WRONG — SC2155: declare and assign separately
local result=$(some_command)
# These do NOT work on macOS bash 3.2:
declare -A assoc_array # associative arrays need bash 4.0+
${var^^} # case conversion needs bash 4.0+
${var:offset:length} # works in bash 3.2
read -a arr # works in bash 3.2
mapfile -t arr < <(cmd) # mapfile needs bash 4.0+
# For macOS portability, use POSIX constructs or install bash via Homebrew
/usr/local/bin/bash --version # bash 5.x
# WRONG — pipe creates a subshell, variables don't propagate
echo "hello" | read result
# CORRECT — use process substitution
read result < <(echo "hello")
# CORRECT — use here-string
# CORRECT — use lastpipe (requires job control disabled)
echo "hello" | read result
echo "$result" # hello (only works in non-interactive shell or with set +m)
# WRONG — variable expands, then glob matches files
ls $files # expands to ls *.txt, then globs
# CORRECT — quote to prevent globbing
ls "$files" # passes literal "*.txt" to ls
# CORRECT — use an array to store globs
This topic covers the core concepts of bash scripting, including underlying theory, practical implementation, and key applications.
Key concepts include:
- command-line fundamentals
- file permissions and ownership
- process management
- shell scripting with bash
- package management
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.