Files
2025-11-30 08:28:57 +08:00

20 KiB

Common Bash Patterns and Anti-Patterns

Collection of proven patterns and common mistakes in bash scripting with explanations and solutions.


Table of Contents

  1. Variable Handling
  2. Command Execution
  3. File Operations
  4. String Processing
  5. Arrays and Loops
  6. Conditionals and Tests
  7. Functions
  8. Error Handling
  9. Process Management
  10. Security Patterns

🚨 CRITICAL GUIDELINES

Windows File Path Requirements

MANDATORY: Always Use Backslashes on Windows for File Paths

When using Edit or Write tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/).

Examples:

  • WRONG: D:/repos/project/file.tsx
  • CORRECT: D:\repos\project\file.tsx

This applies to:

  • Edit tool file_path parameter
  • Write tool file_path parameter
  • All file operations on Windows systems

Documentation Guidelines

NEVER create new documentation files unless explicitly requested by the user.

  • Priority: Update existing README.md files rather than creating new documentation
  • Repository cleanliness: Keep repository root clean - only README.md unless user requests otherwise
  • Style: Documentation should be concise, direct, and professional - avoid AI-generated tone
  • User preference: Only create additional .md files when user specifically asks for documentation

Variable Handling

Pattern: Safe Variable Expansion

# ✓ GOOD: Always quote variables
echo "$variable"
cp "$source" "$destination"
rm -rf "$directory"

# ✗ BAD: Unquoted variables
echo $variable           # Word splitting and globbing
cp $source $destination  # Breaks with spaces
rm -rf $directory        # VERY DANGEROUS unquoted

Why: Unquoted variables undergo word splitting and pathname expansion, leading to unexpected behavior.

Pattern: Default Values

# ✓ GOOD: Use parameter expansion for defaults
timeout="${TIMEOUT:-30}"
config="${CONFIG_FILE:-$HOME/.config/app.conf}"

# ✗ BAD: Manual check
if [ -z "$TIMEOUT" ]; then
    timeout=30
else
    timeout="$TIMEOUT"
fi

Why: Parameter expansion is concise, readable, and handles edge cases correctly.

Anti-Pattern: Confusing Assignment and Comparison

# ✗ VERY BAD: Using = instead of ==
if [ "$var" = "value" ]; then  # Assignment in POSIX test!
    echo "Match"
fi

# ✓ GOOD: Use == or = correctly
if [[ "$var" == "value" ]]; then  # Comparison in bash
    echo "Match"
fi

# ✓ GOOD: POSIX-compliant
if [ "$var" = "value" ]; then  # Single = is correct in [ ]
    echo "Match"
fi

Why: In [[ ]], both = and == work. In [ ], only = is POSIX-compliant.

Anti-Pattern: Unset Variable Access

# ✗ BAD: Accessing undefined variables
echo "Value: $undefined_variable"  # Silent error, prints "Value: "

# ✓ GOOD: Use set -u
set -u
echo "Value: $undefined_variable"  # Error: undefined_variable: unbound variable

# ✓ GOOD: Provide default
echo "Value: ${undefined_variable:-default}"

Why: set -u catches typos and logic errors early.


Command Execution

Pattern: Check Command Existence

# ✓ GOOD: Use command -v
if command -v jq &> /dev/null; then
    echo "jq is installed"
else
    echo "jq is not installed" >&2
    exit 1
fi

# ✗ BAD: Using which
if which jq; then  # Deprecated, not POSIX
    echo "jq is installed"
fi

# ✗ BAD: Using type
if type jq; then  # Verbose output
    echo "jq is installed"
fi

Why: command -v is POSIX-compliant, silent, and reliable.

Pattern: Command Substitution

# ✓ GOOD: Modern syntax with $()
result=$(command arg1 arg2)
timestamp=$(date +%s)

# ✗ BAD: Backticks (hard to nest)
result=`command arg1 arg2`
timestamp=`date +%s`

# ✓ GOOD: Nested substitution
result=$(echo "Outer: $(echo "Inner")")

# ✗ BAD: Nested backticks (requires escaping)
result=`echo "Outer: \`echo \"Inner\"\`"`

Why: $() is easier to read, nest, and maintain.

Anti-Pattern: Useless Use of Cat

# ✗ BAD: UUOC (Useless Use of Cat)
cat file.txt | grep "pattern"

# ✓ GOOD: Direct input
grep "pattern" file.txt

# ✗ BAD: Multiple cats
cat file1 | grep pattern | cat | sort | cat

# ✓ GOOD: Direct pipeline
grep pattern file1 | sort

Why: Unnecessary cat wastes resources and adds extra processes.

Anti-Pattern: Using ls in Scripts

# ✗ BAD: Parsing ls output
for file in $(ls *.txt); do
    echo "$file"
done

# ✓ GOOD: Use globbing
for file in *.txt; do
    [[ -f "$file" ]] || continue  # Skip if no matches
    echo "$file"
done

# ✗ BAD: Counting files with ls
count=$(ls -1 | wc -l)

# ✓ GOOD: Use array
files=(*)
count=${#files[@]}

Why: ls output is meant for humans, not scripts. Parsing it breaks with spaces, newlines, etc.


File Operations

Pattern: Safe File Reading

# ✓ GOOD: Preserve leading/trailing whitespace and backslashes
while IFS= read -r line; do
    echo "Line: $line"
done < file.txt

# ✗ BAD: Without IFS= (strips leading/trailing whitespace)
while read -r line; do
    echo "Line: $line"
done < file.txt

# ✗ BAD: Without -r (interprets backslashes)
while IFS= read line; do
    echo "Line: $line"
done < file.txt

Why: IFS= prevents trimming, -r prevents backslash interpretation.

Pattern: Null-Delimited Files

# ✓ GOOD: For filenames with special characters
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
    echo "Processing: $file"
done

# Or with mapfile (bash 4+)
mapfile -d '' -t files < <(find . -name "*.txt" -print0)
for file in "${files[@]}"; do
    echo "Processing: $file"
done

# ✗ BAD: Newline-delimited (breaks with newlines in filenames)
find . -name "*.txt" | while IFS= read -r file; do
    echo "Processing: $file"
done

Why: Filenames can contain any character except null and slash.

Anti-Pattern: Testing File Existence Incorrectly

# ✗ BAD: Using ls to test existence
if ls file.txt &> /dev/null; then
    echo "File exists"
fi

# ✓ GOOD: Use test operators
if [[ -f file.txt ]]; then
    echo "File exists"
fi

# ✓ GOOD: Different tests
[[ -e path ]]   # Exists (file or directory)
[[ -f file ]]   # Regular file
[[ -d dir ]]    # Directory
[[ -L link ]]   # Symbolic link
[[ -r file ]]   # Readable
[[ -w file ]]   # Writable
[[ -x file ]]   # Executable

Why: Test operators are the correct, efficient way to check file properties.

Pattern: Temporary Files

# ✓ GOOD: Secure temporary file
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT

# Use temp file
echo "data" > "$temp_file"

# ✗ BAD: Insecure temp file
temp_file="/tmp/myapp.$$"
echo "data" > "$temp_file"
# No cleanup!

# ✓ GOOD: Temporary directory
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT

Why: mktemp creates secure, unique files and prevents race conditions.


String Processing

Pattern: String Manipulation with Parameter Expansion

# ✓ GOOD: Use bash parameter expansion
filename="document.tar.gz"
basename="${filename%%.*}"      # document
extension="${filename##*.}"     # gz
name="${filename%.gz}"          # document.tar

# ✗ BAD: Using external commands
basename=$(echo "$filename" | sed 's/\..*$//')
extension=$(echo "$filename" | awk -F. '{print $NF}')

Why: Parameter expansion is faster and doesn't spawn processes.

Pattern: String Comparison

# ✓ GOOD: Use [[ ]] for strings
if [[ "$string1" == "$string2" ]]; then
    echo "Equal"
fi

# ✓ GOOD: Pattern matching
if [[ "$filename" == *.txt ]]; then
    echo "Text file"
fi

# ✓ GOOD: Regex matching
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "Valid email"
fi

# ✗ BAD: Using grep for simple string check
if echo "$string" | grep -q "substring"; then
    echo "Found"
fi

# ✓ GOOD: Use substring matching
if [[ "$string" == *"substring"* ]]; then
    echo "Found"
fi

Why: [[ ]] is bash-native, faster, and more readable.

Anti-Pattern: Word Splitting Issues

# ✗ BAD: Unquoted expansion with spaces
var="file1.txt file2.txt"
for file in $var; do  # Splits on spaces!
    echo "$file"      # file1.txt, then file2.txt
done

# ✓ GOOD: Use array
files=("file1.txt" "file2.txt")
for file in "${files[@]}"; do
    echo "$file"
done

# ✗ BAD: Word splitting in command arguments
file="my file.txt"
rm $file  # Tries to remove "my" and "file.txt"!

# ✓ GOOD: Quote variables
rm "$file"

Why: Word splitting on spaces is a major source of bugs.


Arrays and Loops

Pattern: Array Declaration and Use

# ✓ GOOD: Array declaration
files=("file1.txt" "file2.txt" "file 3.txt")

# ✓ GOOD: Array expansion (each element quoted)
for file in "${files[@]}"; do
    echo "$file"
done

# ✗ BAD: Unquoted array expansion
for file in ${files[@]}; do  # Word splitting!
    echo "$file"
done

# ✓ GOOD: Add to array
files+=("file4.txt")

# ✓ GOOD: Array length
echo "Count: ${#files[@]}"

# ✓ GOOD: Array indices
for i in "${!files[@]}"; do
    echo "File $i: ${files[$i]}"
done

Why: Proper array handling prevents word splitting and globbing issues.

Pattern: Reading Command Output into Array

# ✓ GOOD: mapfile/readarray (bash 4+)
mapfile -t lines < file.txt

# ✓ GOOD: With command substitution
mapfile -t files < <(find . -name "*.txt")

# ✗ BAD: Word splitting
files=($(find . -name "*.txt"))  # Breaks with spaces in filenames!

# ✓ GOOD: Alternative (POSIX-compatible)
while IFS= read -r file; do
    files+=("$file")
done < <(find . -name "*.txt")

Why: mapfile is efficient and handles special characters correctly.

Anti-Pattern: C-Style For Loops for Arrays

# ✗ BAD: C-style loop for arrays
for ((i=0; i<${#files[@]}; i++)); do
    echo "${files[$i]}"
done

# ✓ GOOD: For-in loop
for file in "${files[@]}"; do
    echo "$file"
done

# ✓ ACCEPTABLE: When you need the index
for i in "${!files[@]}"; do
    echo "Index $i: ${files[$i]}"
done

Why: For-in loops are simpler and less error-prone.

Pattern: Loop over Range

# ✓ GOOD: Brace expansion
for i in {1..10}; do
    echo "$i"
done

# ✓ GOOD: With variables (bash 4+)
start=1
end=10
for i in $(seq $start $end); do
    echo "$i"
done

# ✓ GOOD: C-style (arithmetic)
for ((i=1; i<=10; i++)); do
    echo "$i"
done

# ✗ BAD: Using seq in a loop unnecessarily
for i in $(seq 1 1000000); do  # Creates huge string in memory!
    echo "$i"
done

# ✓ GOOD: Use C-style for large ranges
for ((i=1; i<=1000000; i++)); do
    echo "$i"
done

Why: Choose the right loop construct based on the use case.


Conditionals and Tests

Pattern: File Tests

# ✓ GOOD: Use appropriate test
if [[ -f "$file" ]]; then         # Regular file
if [[ -d "$dir" ]]; then          # Directory
if [[ -e "$path" ]]; then         # Exists (any type)
if [[ -L "$link" ]]; then         # Symbolic link
if [[ -r "$file" ]]; then         # Readable
if [[ -w "$file" ]]; then         # Writable
if [[ -x "$file" ]]; then         # Executable
if [[ -s "$file" ]]; then         # Non-empty file

# ✗ BAD: Incorrect test
if [[ -e "$file" ]]; then         # Exists, but could be directory!
    cat "$file"                   # Fails if directory
fi

# ✓ GOOD: Specific test
if [[ -f "$file" ]]; then
    cat "$file"
fi

Why: Use the most specific test for your use case.

Pattern: Numeric Comparison

# ✓ GOOD: Arithmetic context
if (( num > 10 )); then
    echo "Greater than 10"
fi

# ✓ GOOD: Test operator
if [[ $num -gt 10 ]]; then
    echo "Greater than 10"
fi

# ✗ BAD: String comparison for numbers
if [[ "$num" > "10" ]]; then  # Lexicographic comparison!
    echo "Greater than 10"    # "9" > "10" is true!
fi

Why: Use numeric comparison operators for numbers.

Anti-Pattern: Testing Boolean Strings

# ✗ BAD: Comparing to string "true"
if [[ "$flag" == "true" ]]; then
    do_something
fi

# ✓ GOOD: Use boolean variable directly
flag=false  # or true

if $flag; then
    do_something
fi

# ✓ BETTER: Use integers for flags
flag=0  # false
flag=1  # true

if (( flag )); then
    do_something
fi

# ✓ GOOD: For command success/failure
if command; then
    echo "Success"
fi

Why: Boolean strings are error-prone; use actual booleans or return codes.

Pattern: Multiple Conditions

# ✓ GOOD: Logical operators
if [[ condition1 && condition2 ]]; then
    echo "Both true"
fi

if [[ condition1 || condition2 ]]; then
    echo "At least one true"
fi

if [[ ! condition ]]; then
    echo "False"
fi

# ✗ BAD: Separate tests
if [ condition1 -a condition2 ]; then  # Deprecated
    echo "Both true"
fi

# ✗ BAD: Nested ifs for AND
if [[ condition1 ]]; then
    if [[ condition2 ]]; then
        echo "Both true"
    fi
fi

Why: && and || in [[ ]] are clearer and recommended.


Functions

Pattern: Function Return Values

# ✓ GOOD: Return status, output to stdout
get_value() {
    local value="result"

    if [[ -n "$value" ]]; then
        echo "$value"
        return 0
    else
        return 1
    fi
}

# Usage
if result=$(get_value); then
    echo "Got: $result"
else
    echo "Failed"
fi

# ✗ BAD: Using return for data
get_value() {
    return 42  # Can only return 0-255!
}
result=$?  # Gets 42, but limited range

Why: return is for exit status (0-255), not data. Output to stdout for data.

Pattern: Local Variables in Functions

# ✓ GOOD: Declare local variables
my_function() {
    local arg="$1"
    local result=""

    result=$(process "$arg")
    echo "$result"
}

# ✗ BAD: Global variables
my_function() {
    arg="$1"        # Pollutes global namespace!
    result=""       # Global variable!

    result=$(process "$arg")
    echo "$result"
}

Why: Local variables prevent unexpected side effects.

Anti-Pattern: Capturing Local Command Failure

# ✗ BAD: Local declaration masks command failure
my_function() {
    local result=$(command_that_fails)  # $? is from 'local', not 'command'!
    echo "$result"
}

# ✓ GOOD: Separate declaration and assignment
my_function() {
    local result
    result=$(command_that_fails) || return 1
    echo "$result"
}

# ✓ GOOD: Check command separately
my_function() {
    local result

    if ! result=$(command_that_fails); then
        return 1
    fi

    echo "$result"
}

Why: Combining local and command substitution hides command failure.


Error Handling

Pattern: Check Command Success

# ✓ GOOD: Direct check
if ! command; then
    echo "Command failed" >&2
    exit 1
fi

# ✓ GOOD: With logical operator
command || {
    echo "Command failed" >&2
    exit 1
}

# ✓ GOOD: Capture output and check
if ! output=$(command 2>&1); then
    echo "Command failed: $output" >&2
    exit 1
fi

# ✗ BAD: Not checking status
command  # What if it fails?
next_command

Why: Always check if commands succeed unless failure is acceptable.

Pattern: Error Messages to stderr

# ✓ GOOD: Errors to stderr
echo "Error: Invalid argument" >&2

# ✗ BAD: Errors to stdout
echo "Error: Invalid argument"

# ✓ GOOD: Error function
error() {
    echo "ERROR: $*" >&2
}

error "Something went wrong"

Why: stderr is for errors, stdout is for data output.

Pattern: Cleanup on Exit

# ✓ GOOD: Trap for cleanup
temp_file=$(mktemp)

cleanup() {
    rm -f "$temp_file"
}

trap cleanup EXIT

# Do work with temp_file

# ✗ BAD: Manual cleanup (might not run)
temp_file=$(mktemp)

# Do work

rm -f "$temp_file"  # Doesn't run if script exits early!

Why: Trap ensures cleanup runs on exit, even on errors.

Anti-Pattern: Silencing Errors

# ✗ BAD: Silencing errors
command 2>/dev/null  # What if it fails?
next_command

# ✓ GOOD: Check status even if silencing output
if ! command 2>/dev/null; then
    echo "Command failed" >&2
    exit 1
fi

# ✓ ACCEPTABLE: When failure is expected and acceptable
if command 2>/dev/null; then
    echo "Command succeeded"
else
    echo "Command failed (expected)"
fi

Why: Silencing errors without checking status leads to silent failures.


Process Management

Pattern: Background Jobs

# ✓ GOOD: Track background jobs
long_running_task &
pid=$!

# Wait for completion
if wait "$pid"; then
    echo "Task completed successfully"
else
    echo "Task failed" >&2
fi

# ✓ GOOD: Multiple background jobs
job1 &
pid1=$!
job2 &
pid2=$!

wait "$pid1" "$pid2"

Why: Proper job management prevents zombie processes.

Pattern: Timeout for Commands

# ✓ GOOD: Use timeout command (if available)
if timeout 30 long_running_command; then
    echo "Completed within timeout"
else
    echo "Timed out or failed" >&2
fi

# ✓ GOOD: Manual timeout implementation
timeout_command() {
    local timeout=$1
    shift

    "$@" &
    local pid=$!

    ( sleep "$timeout"; kill "$pid" 2>/dev/null ) &
    local killer=$!

    if wait "$pid" 2>/dev/null; then
        kill "$killer" 2>/dev/null
        wait "$killer" 2>/dev/null
        return 0
    else
        return 1
    fi
}

timeout_command 30 long_running_command

Why: Prevents scripts from hanging indefinitely.

Anti-Pattern: Killing Processes Unsafely

# ✗ BAD: kill -9 immediately
kill -9 "$pid"

# ✓ GOOD: Graceful shutdown first
kill -TERM "$pid"
sleep 2

if kill -0 "$pid" 2>/dev/null; then
    echo "Process still running, forcing..." >&2
    kill -KILL "$pid"
fi

# ✓ GOOD: With timeout
graceful_kill() {
    local pid=$1
    local timeout=${2:-10}

    kill -TERM "$pid" 2>/dev/null || return 0

    for ((i=0; i<timeout; i++)); do
        if ! kill -0 "$pid" 2>/dev/null; then
            return 0
        fi
        sleep 1
    done

    echo "Forcing kill of $pid" >&2
    kill -KILL "$pid" 2>/dev/null
}

Why: SIGTERM allows graceful shutdown; SIGKILL should be last resort.


Security Patterns

Pattern: Input Validation

# ✓ GOOD: Whitelist validation
validate_action() {
    local action=$1
    case "$action" in
        start|stop|restart|status)
            return 0
            ;;
        *)
            echo "Error: Invalid action: $action" >&2
            return 1
            ;;
    esac
}

# ✗ BAD: No validation
action="$1"
systemctl "$action" myservice  # User can pass arbitrary commands!

# ✓ GOOD: Validate first
if validate_action "$1"; then
    systemctl "$1" myservice
else
    exit 1
fi

Why: Whitelist validation prevents command injection.

Pattern: Avoid eval

# ✗ BAD: eval with user input
eval "$user_command"  # DANGEROUS!

# ✓ GOOD: Use arrays
command_args=("$arg1" "$arg2" "$arg3")
command "${command_args[@]}"

# ✗ BAD: Dynamic variable names
eval "var_$name=value"

# ✓ GOOD: Associative arrays (bash 4+)
declare -A vars
vars[$name]="value"

Why: eval with user input is a security vulnerability.

Pattern: Safe PATH

# ✓ GOOD: Set explicit PATH
export PATH="/usr/local/bin:/usr/bin:/bin"

# ✓ GOOD: Use absolute paths for critical commands
/usr/bin/rm -rf "$directory"

# ✗ BAD: Trusting user's PATH
rm -rf "$directory"  # What if there's a malicious 'rm' in PATH?

Why: Prevents PATH injection attacks.


Summary

Most Critical Patterns:

  1. Always quote variable expansions: "$var"
  2. Use set -euo pipefail for safety
  3. Prefer [[ ]] over [ ] in bash
  4. Use arrays for lists: "${array[@]}"
  5. Check command success: if ! command; then
  6. Use local variables in functions
  7. Errors to stderr: echo "Error" >&2
  8. Use mktemp for temporary files
  9. Cleanup with traps: trap cleanup EXIT
  10. Validate all user input

Most Dangerous Anti-Patterns:

  1. Unquoted variables: $var
  2. Parsing ls output
  3. Using eval with user input
  4. Silencing errors without checking
  5. Not using set -u or defaults
  6. Global variables in functions
  7. Word splitting on filenames
  8. Testing strings with > for numbers
  9. kill -9 without trying graceful shutdown
  10. Trusting user PATH

Following these patterns and avoiding anti-patterns will result in robust, secure, and maintainable bash scripts.