Skip to main content

Lesson: Scripts You Can Trust

What you'll learn

  • How set -euo pipefail turns silent failures into loud, safe ones.
  • The quoting pitfalls that cause the worst real-world script bugs.
  • How to lint your scripts automatically with ShellCheck.
  • How to clean up reliably with trap, and what "idempotency" means.
  • How to recognize when Bash is the wrong tool for the job.

By the end you can write scripts that fail safely, are checked by a tool, clean up after themselves, and that you would trust to run unattended.

The lesson

A script that works once on your machine is easy. A script you trust to run on a server at 3 a.m. — and not delete the wrong thing — is the real skill. This lesson is the difference between a beginner's script and a professional one.

Run everything on the Jumpbox (10.100.100.254, user ubuntu). Because you will commit these scripts to Git soon, robustness and readability are part of the deliverable.

1. The safety header: set -euo pipefail

By default, Bash keeps going after errors and treats undefined variables as empty — a recipe for disaster. Put this near the top of every serious script:

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

What each part does:

  set -e            exit immediately if any command fails (non-zero)
  set -u            error on use of an UNSET variable (catches typos)
  set -o pipefail   a pipeline fails if ANY command in it fails,
                    not just the last one

Why it matters — the classic horror story:

# WITHOUT set -u, if $dir is unset/typo'd this becomes: rm -rf /
rm -rf "$dir/"

# WITH set -u, the script stops on the unset variable instead.

set -e is helpful but has sharp edges: a command that is expected to fail (like grep finding nothing) would stop the script. Handle those explicitly:

if grep -q pattern file; then ...; fi    # fine: the 'if' "uses" the exit code
count=$(grep -c pattern file || true)    # 'or true' stops -e from firing

Add the header, then test that your script still does the right thing.

2. Quoting pitfalls (the #1 source of bugs)

You learned to quote variables in Lesson 1. Here is why it bites in practice:

file="my report.txt"
rm $file          # WRONG: runs  rm my report.txt  -> deletes TWO files!
rm "$file"        # RIGHT: deletes the one file "my report.txt"

Rules to internalize:

  • Quote every variable expansion: "$var", "$1", "${arr[@]}".
  • Quote command substitution when you assign or pass it: x="$(cmd)".
  • Empty/unset variables vanish without quotes: [ -f $f ] becomes [ -f ] (broken) if $f is empty; inside [[ ]] this is safer, another reason to prefer [[ ]].
  • Use "${var}" braces when a variable is next to other text: "${name}_backup".

When in doubt, quote. There is rarely a downside.

3. ShellCheck — let a tool find the bugs

ShellCheck is a free static analyzer that reads your script and flags exactly these mistakes. Install and run it on the Jumpbox:

sudo apt-get install -y shellcheck
shellcheck myscript.sh

It produces messages like:

In myscript.sh line 7:
rm $file
   ^--^ SC2086: Double quote to prevent globbing and word splitting.

Each SCxxxx code links to a wiki page explaining the fix. Make "ShellCheck-clean" your standard before you commit a script to Git. You can even add a directive to silence a specific warning when you are sure: # shellcheck disable=SC2034.

4. Cleaning up with trap

A trap runs a command when your script exits or receives a signal (like Ctrl+C). Use it to remove temp files no matter how the script ends:

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

tmp="$(mktemp)"                 # safe unique temp file
trap 'rm -f "$tmp"' EXIT        # ALWAYS runs on exit, success or failure

echo "working..." > "$tmp"
# ... use $tmp ...
# no manual cleanup needed; the trap handles it

EXIT fires on normal end, on error (thanks to set -e), and on interruption. Common signals to trap: EXIT (always), INT (Ctrl+C), TERM. Use mktemp to make temp files — never hardcode /tmp/myfile, which is predictable and can be hijacked.

5. Idempotency

Idempotent means running the script twice has the same result as running it once — no errors, no duplicates, no harm. This is essential for automation that may re-run.

mkdir /backups          # FAILS the second time (already exists)
mkdir -p /backups       # idempotent: fine whether or not it exists

# Instead of blindly appending (which duplicates on re-run):
grep -qxF "$line" file || echo "$line" >> file   # add only if missing

Ask of every step: "what happens if this runs again?" Idempotent scripts are safe to retry, which is exactly what schedulers and config tools expect.

6. When Bash is the WRONG tool

Bash is excellent for gluing commands together, file shuffling, and short automation. It becomes painful — and you should reach for Python, Go, or a dedicated tool — when you have:

  - Complex data structures (nested JSON, real math, dates arithmetic)
  - Anything over ~100-200 lines or many functions
  - Heavy string parsing  -> use awk, or a real language
  - JSON/YAML             -> use jq / yq, not regex on text
  - Needs unit tests, libraries, or to run on Windows too

A good instinct: if you are fighting Bash to do something a real programming language makes easy, switch. Parsing JSON with grep and sed is a well-known trap — pipe it through jq instead. Knowing the boundary is itself a senior skill.

7. A robust template to start from

#!/usr/bin/env bash
#
# describe.sh — what this script does, in one line.
# Usage: ./describe.sh <arg>
#
set -euo pipefail

# --- config ---
readonly LOGDIR="/var/log"

# --- cleanup ---
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

# --- functions ---
log() { echo "[$(date +%T)] $*"; }

die() { echo "ERROR: $*" >&2; exit 1; }

# --- args ---
[[ $# -ge 1 ]] || die "Usage: $0 <arg>"
arg="$1"

# --- main ---
log "Starting with arg=$arg"
[[ -d "$LOGDIR" ]] || die "$LOGDIR not found"
log "Done"

This template has the safety header, a trap, helper functions (log, die), argument validation, and readonly constants. Run shellcheck describe.sh and fix anything it reports. Habits like these are exactly what your mentor will look for when you commit these scripts to Git.

Dig deeper

Search terms

  • bash set -euo pipefail explained
  • bash quoting word splitting SC2086
  • shellcheck install ubuntu run script
  • bash trap EXIT cleanup temp file mktemp
  • bash idempotent script mkdir -p
  • when not to use bash use python instead

Check yourself

  1. What does each of -e, -u, and -o pipefail do in set -euo pipefail?
  2. Show a one-line example where forgetting to quote a variable causes a real bug.
  3. What is ShellCheck, and at what point in your workflow should you run it?
  4. What does a trap '...' EXIT guarantee, and why use mktemp?
  5. Give two situations where you should choose Python or another tool over Bash.