Great Stack to Doesn't Work Bonus: 10 Bash Scripting Golden Rules

Published: (June 13, 2026 at 05:00 AM EDT)
5 min read
Source: Dev.to

Source: Dev.to

Great Stack to Doesn’t Work — Bonus

10 Bash Scripting Golden Rules

Because your deployment script is production code whether you admit it or not.

1. Start every script with set -euo pipefail.

#!/usr/bin/env bash
set -euo pipefail
Enter fullscreen mode


Exit fullscreen mode

-e: Exit on any command failure. Without it, a failed rm or cp is silently ignored and the script continues with corrupted state.

-u: Treat undefined variables as errors. $UNSET_VAR expands to empty string by default. With -u, it’s a hard error. This catches typos ($DATABSE_URL instead of $DATABASE_URL) before they reach production.

-o pipefail: A pipeline fails if any command in it fails. Without it, bad_command | grep something returns grep’s exit code, hiding bad_command’s failure.

2. Quote your variables. Always.

# BAD: breaks if filename has spaces
rm $file

# GOOD: works with any filename
rm "$file"

# BAD: word splitting nightmare
for f in $files; do

# GOOD: preserves entries with spaces
for f in "${files[@]}"; do
Enter fullscreen mode


Exit fullscreen mode

Unquoted variables undergo word splitting and glob expansion. A filename with spaces becomes two arguments. A variable containing * expands to every file in the directory.

3. Never use eval.

eval takes a string and executes it as a command. It’s the rm -rf / of bash programming — it works until someone puts something unexpected in that string.

# DANGEROUS: if $user_input contains "; rm -rf /"
eval "echo $user_input"

# SAFE: use arrays for dynamic commands
cmd=("docker" "run" "--rm" "$image")
"${cmd[@]}"
Enter fullscreen mode


Exit fullscreen mode

If you think you need eval, you almost certainly need an array instead.

4. Use ShellCheck. Non-negotiable.

ShellCheck catches quoting errors, undefined variables, deprecated syntax, and common pitfalls statically. Run it in CI.

shellcheck myscript.sh
Enter fullscreen mode


Exit fullscreen mode

It finds bugs you’d never catch in code review. Enable it as a pre-commit hook and you’ll wonder how you lived without it.

5. Clean up with trap.

Temporary files, background processes, lock files — if your script creates them, it must clean them up, even on failure.

cleanup() {
    rm -f "$TEMP_FILE"
    kill "$BG_PID" 2>/dev/null || true
}
trap cleanup EXIT

TEMP_FILE=$(mktemp)
some_command > "$TEMP_FILE" &
BG_PID=$!
Enter fullscreen mode


Exit fullscreen mode

trap ... EXIT fires on normal exit, error exit, and most signals. No more orphaned temp files.

6. Use process substitution instead of temp files.

# OLD: write to temp, read from temp
command1 > /tmp/result.txt
command2 < /tmp/result.txt

# BETTER: no temp file needed
command2 < <(command1)

# COMPARE TWO COMMANDS:
diff <(sort file1) <(sort file2)
Enter fullscreen mode


Exit fullscreen mode

<(command) creates a virtual file descriptor. No temp files to clean up. No race conditions.

7. Use parameter expansion instead of external commands.

# SLOW: spawns a subprocess
filename=$(basename "$path")
extension=$(echo "$file" | sed 's/.*\.//')

# FAST: pure bash
filename="${path##*/}"
extension="${file##*.}"
dirname="${path%/*}"
without_ext="${file%.*}"

# Default values
db_host="${DB_HOST:-localhost}"
db_port="${DB_PORT:-5432}"
Enter fullscreen mode


Exit fullscreen mode

Each $(...) forks a subprocess. In a loop processing 10,000 items, the subprocess overhead dominates. Parameter expansion is instant.

8. Use arrays properly.

# WRONG: space-delimited string
files="file one.txt file two.txt"

# RIGHT: proper array
files=("file one.txt" "file two.txt")

# Iterate safely
for f in "${files[@]}"; do
    echo "Processing: $f"
done

# Pass as arguments
command "${files[@]}"

# Append
files+=("file three.txt")

# Length
echo "${#files[@]}"
Enter fullscreen mode


Exit fullscreen mode

Arrays preserve elements with spaces, newlines, and special characters. Strings don’t.

9. Use here-docs for multi-line strings.

# HERE-DOC: variables expanded
cat << EOF
Hello $USER,
Today is $(date).
Your home is $HOME.
EOF

# HERE-DOC with quotes: no expansion (literal)
cat << 'EOF'
This $variable is not expanded.
Neither is $(this command).
EOF

# HERE-STRING: one-liner
grep "pattern" <<< "$variable"
Enter fullscreen mode


Exit fullscreen mode

Here-docs are cleaner than escaped multi-line echo statements and more readable than concatenated strings.

10. Test with Bats.

Bats (Bash Automated Testing System) is a testing framework for bash scripts.

# test_deploy.bats
@test "deployment script requires ENVIRONMENT variable" {
    unset ENVIRONMENT
    run ./deploy.sh
    [ "$status" -eq 1 ]
    [[ "$output" == *"ENVIRONMENT is required"* ]]
}

@test "deployment script validates environment name" {
    ENVIRONMENT="invalid" run ./deploy.sh
    [ "$status" -eq 1 ]
    [[ "$output" == *"must be staging or production"* ]]
}
Enter fullscreen mode


Exit fullscreen mode

If your bash script is important enough to run in production, it’s important enough to test. Bats makes it simple.

Over to You

Which bash scripting mistake has bitten you the hardest? Do you test your bash scripts — and if so, how?

If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.

Follow me:

This is part of the Great Stack to Doesn’t Work series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.

0 views
Back to Blog

Related posts

Read more »

The spec is in the wrong place

My day job is at a large tech company. Hundreds of engineering teams, and every one of them is somewhere different on AI adoption. Some are still treating codin...

The Heuristics Say Don't

A culture that only records its disasters ends up with a biased archive. Wars documented, plagues chronicled, collapses catalogued. The quiet decades go unwritt...