DEV Community

Mehmet TURAÇ
Mehmet TURAÇ

Posted on

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

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.*

Top comments (1)

Collapse
 
merbayerp profile image
Mustafa ERBAY

One lesson production environments taught me is that bash scripts are often treated as “temporary glue code” right until they become mission-critical. I’ve seen outages caused by missing quotes, silent pipeline failures, and scripts that worked perfectly for years until a filename contained an unexpected space.I’d add one more rule: treat deployment and automation scripts as production software. Give them code reviews, testing, versioning, and observability just like application code.The cost of a bug in a deployment script is often much higher than the cost of a bug in the application itself.