⚠️ 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
| # ❌ 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
| # ❌ 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
| # ❌ 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
| # ❌ 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
| # ❌ 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
| # ❌ 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