Przejdź do treści

⚠️ Common Gotchas Reference

Even experienced shell scripters encounter subtle pitfalls and unexpected behaviors. This reference documents frequent mistakes and their solutions to help you write more robust scripts.


🎯 Quoting and Variable Expansion

Unquoted Variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ❌ Dangerous - unquoted variables
filename="my file.txt"
rm $filename  # Becomes: rm my file.txt (fails)

# ✅ Safe - quoted variables
rm "$filename"  # Becomes: rm "my file.txt" (works)

# ❌ Array expansion without quotes
files=("file 1.txt" "file 2.txt")
for file in ${files[@]}; do  # Splits on spaces!
    echo "Processing: $file"
done

# ✅ Proper array expansion
for file in "${files[@]}"; do  # Preserves spaces
    echo "Processing: $file"
done

Variable Assignment with Spaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ❌ Wrong - spaces around =
name = "John"  # Error: command 'name' not found

# ✅ Correct
name="John"

# ❌ Wrong - command substitution with spaces
result = $(date)  # Error: command 'result' not found

# ✅ Correct
result=$(date)

🔧 Command Substitution Issues

Subshell Variable Scope

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ❌ Variables set in pipes are lost
counter=0
echo -e "line1\nline2\nline3" | while read line; do
    counter=$((counter + 1))
done
echo "Counter: $counter"  # Still 0!

# ✅ Solution 1: Use here-string
counter=0
while read line; do
    counter=$((counter + 1))
done <<< "$(echo -e "line1\nline2\nline3")"
echo "Counter: $counter"  # Now 3

# ✅ Solution 2: Process in same shell
counter=0
while read line; do
    counter=$((counter + 1))
done << EOF
line1
line2
line3
EOF
echo "Counter: $counter"  # Now 3

Command Substitution Whitespace

1
2
3
4
5
6
7
# ❌ Leading/trailing whitespace removed
result=$(echo "  spaced  ")
echo "[$result]"  # [spaced] - spaces lost!

# ✅ Preserve whitespace
result="$(echo "  spaced  ")"
echo "[$result]"  # [  spaced  ] - spaces preserved

📋 Test and Conditional Gotchas

String Comparison Pitfalls

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ❌ Empty string test - dangerous
if [ $var = "test" ]; then  # Fails if $var is empty!
    echo "Match"
fi

# ✅ Safe empty string test
if [ "$var" = "test" ]; then  # Works even if $var is empty
    echo "Match"
fi

# ❌ Unquoted right side comparison
if [ "$var" = $pattern ]; then  # Fails if $pattern has spaces!
    echo "Match"
fi

# ✅ Both sides quoted
if [ "$var" = "$pattern" ]; then
    echo "Match"
fi

Numeric vs String Comparison

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# ❌ String comparison of numbers
if [ "10" \< "2" ]; then  # True! (lexicographic comparison)
    echo "10 is less than 2"
fi

# ✅ Numeric comparison
if [ 10 -lt 2 ]; then  # False (numeric comparison)
    echo "10 is less than 2"
fi

# ❌ Mixing types
count="05"
if [ $count -eq 5 ]; then  # May fail due to leading zero (octal)
    echo "Equal"
fi

# ✅ Force base 10
if [ $((10#$count)) -eq 5 ]; then
    echo "Equal"
fi

🔄 Loop and Iteration Issues

For Loop with Globbing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ❌ Dangerous - globbing with unquoted expansion
pattern="*.txt"
for file in $pattern; do  # Expands in current directory!
    echo "$file"
done

# ✅ Safe - disable globbing or quote pattern
set -f  # Disable globbing
for file in $pattern; do
    echo "$file"
done
set +f  # Re-enable globbing

# ✅ Or quote the pattern
for file in "$pattern"; do  # Treats as literal string
    echo "$file"
done

While Loop Infinite Conditions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ❌ Potential infinite loop
while [ true ]; do  # 'true' command, not boolean!
    # Some condition should break
    if [ some_condition ]; then
        break
    fi
done

# ✅ Clearer infinite loop
while true; do
    # Some condition should break
    if [ some_condition ]; then
        break
    fi
done

# ❌ Accidental assignment
while [ $count = 10 ]; do  # Should be -eq for numbers!
    # Loop body
done

# ✅ Correct comparison
while [ $count -eq 10 ]; do
    # Loop body
done

📊 Arithmetic Gotchas

Octal Numbers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# ❌ Leading zeros interpreted as octal
port=080  # Error: invalid octal number

# ✅ Force decimal interpretation
port=$((10#080))  # Base 10 interpretation

# ❌ Octal arithmetic
permissions=755
new_permissions=$((permissions + 100))  # Octal addition!

# ✅ Decimal arithmetic
permissions=755
new_permissions=$((10#$permissions + 100))  # Decimal addition

Floating Point Arithmetic

1
2
3
4
5
6
7
8
# ❌ Shell doesn't support floating point
result=$((10 / 3))  # Result is 3, not 3.333...

# ✅ Use bc for floating point
result=$(echo "scale=2; 10 / 3" | bc)  # 3.33

# ✅ Use awk for calculations
result=$(awk 'BEGIN { printf "%.2f", 10/3 }')  # 3.33

🛠️ File and Path Issues

Pathname Expansion in Variables

1
2
3
4
5
6
# ❌ Unintended glob expansion
wildcard="file*.txt"
echo $wildcard  # Expands to matching files!

# ✅ Quote to prevent expansion
echo "$wildcard"  # Prints literal "file*.txt"

Directory Change Side Effects

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ❌ Changing directory affects entire script
cd /some/directory
# ... rest of script runs in different directory

# ✅ Use subshell or save/restore
(
    cd /some/directory
    # Commands here run in subshell
    # Parent shell directory unchanged
)

# ✅ Or save and restore
OLD_PWD="$PWD"
cd /some/directory
# ... work in directory
cd "$OLD_PWD"  # Restore original directory

🔍 Pattern Matching Problems

Case Statement Fallthrough

1
2
3
4
5
6
7
8
9
# ❌ Missing semicolons causes fallthrough
case "$var" in
    pattern1)
        echo "First pattern"
        ;;  # This semicolon is crucial!
    pattern2)
        echo "Second pattern"
        ;;
esac

Regex vs Glob Patterns

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ❌ Confusing glob and regex
if [[ "$file" =~ *.txt ]]; then  # Wrong! =~ expects regex
    echo "Text file"
fi

# ✅ For regex
if [[ "$file" =~ \.txt$ ]]; then
    echo "Text file"
fi

# ✅ For glob patterns
if [[ "$file" == *.txt ]]; then
    echo "Text file"
fi

🎨 Advanced Gotchas

Exit Code Propagation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# ❌ Last command determines exit code
my_function() {
    command_that_fails  # Exit code 1
    echo "Cleanup"      # Exit code 0 - function returns 0!
}

# ✅ Preserve important exit codes
my_function() {
    command_that_fails
    local exit_code=$?
    echo "Cleanup"
    return $exit_code
}

# ✅ Or use set -e carefully
set -e
my_function() {
    command_that_fails || return $?  # Explicit error handling
    echo "Cleanup"
}

Signal Handling Traps

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ❌ Trap inheritance issues
trap 'echo "Caught signal"' INT

(
    # Subshell inherits trap
    kill -INT $$  # Will trigger trap
)

# ❌ Trap restoration
trap 'cleanup' EXIT
trap - EXIT  # Removes trap - be careful!

Function Return Values

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ❌ Confusing return vs echo
get_value() {
    echo "some value"
    return 0  # Return code, not value!
}

# ❌ Wrong way to capture
result=$(get_value)  # Gets echoed output
return_code=$?       # Gets return code (0)

# ✅ Sometimes both are needed
get_value_with_status() {
    local value
    value=$(some_command) || return $?
    echo "$value"
    return 0
}

🧪 Debugging and Troubleshooting

Debugging Techniques

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# ✅ Enable debugging for specific sections
set -x  # Enable trace
problematic_section
set +x  # Disable trace

# ✅ Conditional debugging
debug() {
    if [ "${DEBUG:-false}" = "true" ]; then
        echo "DEBUG: $*" >&2
    fi
}

# ✅ Error tracing
set -eEu -o pipefail
trap 'echo "Error at line $LINENO"' ERR

# ✅ Function entry/exit tracing
trace_functions() {
    trap 'echo "Entering function: $FUNCNAME"' DEBUG
}

Common Error Messages

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# "command not found" - Usually spacing or PATH issues
# "syntax error near unexpected token" - Usually quoting or bracket issues
# "integer expression expected" - Usually string/number confusion
# "no such file or directory" - Usually path or variable expansion issues

# ✅ Defensive programming
validate_inputs() {
    local required_vars=("$@")
    local missing=()

    for var in "${required_vars[@]}"; do
        if [ -z "${!var}" ]; then
            missing+=("$var")
        fi
    done

    if [ ${#missing[@]} -gt 0 ]; then
        echo "Missing required variables: ${missing[*]}" >&2
        return 1
    fi
}

🧾 Summary Checklist

Before Running Scripts

✅ Quote all variable expansions ✅ Check for proper shebang line ✅ Validate input parameters ✅ Handle exit codes appropriately ✅ Test with edge cases (empty values, spaces, special characters) ✅ Verify PATH and environment assumptions ✅ Check for race conditions in file operations

Common Fix Patterns

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Always quote variables
"$variable" not $variable

# Use proper test syntax
[ "$var" = "value" ] not [ $var = value ]

# Handle subshell scope
Use here-strings or process in main shell

# Preserve whitespace
Quote command substitution: "$(command)"

# Check tool availability
command -v tool >/dev/null 2>&1 || { echo "tool required"; exit 1; }

# Handle errors
set -eEu -o pipefail
trap 'echo "Error occurred"' ERR

🧠 Prevention Strategies

Defensive Script Template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
# Defensive script template

set -eEu -o pipefail  # Strict error handling

# Error handling
error_exit() {
    echo "Error: $1" >&2
    exit "${2:-1}"
}

trap 'error_exit "Unexpected error at line $LINENO"' ERR

# Input validation
validate_required() {
    local var_name="$1"
    local var_value="${!var_name}"

    if [ -z "$var_value" ]; then
        error_exit "Required variable $var_name is not set"
    fi
}

# Safe command execution
safe_execute() {
    if ! "$@"; then
        error_exit "Command failed: $*"
    fi
}

# Main logic
main() {
    # Validate inputs
    validate_required "INPUT_FILE"

    # Execute with error handling
    safe_execute command "$INPUT_FILE"
}

# Run main function with arguments
main "$@"

🧾 See Also