Sagar.BlogArticle
All posts
All posts
Bash

Safer Scripts with set -e, -u, -o pipefail, and -x

Four Bash options that instantly make your scripts safer: exit on error, error on unset variables, propagate pipe failures, and trace execution.

January 28, 20266 min read
BashsetError HandlingSafetyScripting

By default, Bash keeps running even after a command fails. It also ignores unset variables (treating them as empty) and ignores failures in the middle of pipes. Adding four options to the top of every script fixes all three problems.

#!/usr/bin/env bash
set -euo pipefail
# ↑ This single line dramatically reduces silent failures in scripts

set -e — Exit on Error

#!/usr/bin/env bash
set -e    # exit immediately if any command exits with a non-zero code

echo "Before"
ls /nonexistent      # ← This fails; script STOPS here
echo "This line never runs"

Nuances of set -e

set -e does NOT exit on all failures. It's skipped when:

  • The command is in an if, while, or until condition
  • The command is followed by || ... or && ...
  • The command is in a pipeline (the last command's exit code is checked)

For example: if grep -q pattern file; then — grep failure here won't exit the script.

set -u — Error on Unset Variables

#!/usr/bin/env bash
set -u    # treat unset variables as an error

name="Alice"
echo "$name"          # Alice
echo "$undefined"     # ← Error: unbound variable — script exits

# Use ${var:-default} to safely access potentially-unset variables
echo "${undefined:-fallback}"   # fallback — no error with set -u

set -o pipefail — Propagate Pipe Failures

# Without pipefail:
cat nonexistent_file | sort | head -5
echo $?    # 0 — only head's exit code matters!

# With pipefail:
set -o pipefail
cat nonexistent_file | sort | head -5
echo $?    # non-zero — cat's failure propagates

# Common gotcha: grep returning 1 (no match) fails with pipefail
# Fix: use grep || true to allow no-match
grep "pattern" file | head -5 || true    # won't fail script if no match

set -x — Debug Trace

#!/usr/bin/env bash
set -x    # print each command before executing it (prefixed with +)

name="Alice"
echo "Hello, $name"

# Output:
# + name=Alice
# + echo 'Hello, Alice'
# Hello, Alice

# Turn off tracing for part of the script
set +x
# quiet section
set -x
# tracing resumes

Combining Options and ERR Traps

#!/usr/bin/env bash
set -euo pipefail

# Custom error handler
function on_error {
    local line=$1
    echo "Script failed at line $line" >&2
}
trap 'on_error $LINENO' ERR

# Temporarily disable set -e for expected failures
set +e
grep "maybe" file.txt
found=$?
set -e

if [[ $found -eq 0 ]]; then
    echo "Pattern found"
else
    echo "Pattern not found (that's OK)"
fi
Quick Check

With `set -o pipefail`, which command's exit code determines if `cmd1 | cmd2 | cmd3` fails?

Exercise

Take this risky script and make it safe:

#!/usr/bin/env bash
# RISKY version
dir=$HOME/important
cd $dir
rm -rf *
echo "Done: $(pwd)"

Add set -euo pipefail, handle the case where $dir doesn't exist gracefully, and add a safety check before the rm.