Common Shell Scripting Mistakes
This guide covers frequent mistakes made in bash and shell scripts, their consequences, and how to fix them.
1. Unquoted Variables
Problem
# Wrong
file=/path/with spaces/file.txt
cat $file # Breaks into multiple argumentsConsequence
Word splitting and glob expansion cause unexpected behavior.
Solution
# Right
file="/path/with spaces/file.txt"
cat "$file"Rule
Always quote variable expansions unless you explicitly need word splitting.
2. Not Checking Command Success
Problem
# Wrong
cd /some/directory
rm -rf * # DANGEROUS if cd fails!Consequence
Commands execute even if previous commands fail, potentially catastrophic.
Solution
# Right
cd /some/directory || exit 1
rm -rf *
# Or use set -e
set -e
cd /some/directory
rm -rf *
# Or check explicitly
if ! cd /some/directory; then
echo "Failed to change directory" >&2
exit 1
fi
rm -rf *3. Using [ ] with Bash Features
Problem
# Wrong (== not POSIX, may fail in sh)
if [ "$var" == "value" ]; then
echo "match"
fiSolution
# POSIX sh - use single =
if [ "$var" = "value" ]; then
echo "match"
fi
# Or use bash [[ ]] (bash only)
if [[ "$var" == "value" ]]; then
echo "match"
fi4. Useless Use of cat (UUOC)
Problem
# Wrong - unnecessary cat
cat file.txt | grep pattern
cat file.txt | awk '{print $1}'Consequence
Wastes a process, less efficient.
Solution
# Right
grep pattern file.txt
awk '{print $1}' file.txt
# Or use redirection
< file.txt grep pattern5. Not Using -r with read
Problem
# Wrong
while read line; do
echo "$line"
done < fileConsequence
Backslashes are interpreted, leading character may be removed.
Solution
# Right
while IFS= read -r line; do
echo "$line"
done < file-rprevents backslash interpretationIFS=prevents leading/trailing whitespace trimming
6. Testing $? After Multiple Commands
Problem
# Wrong
command1
command2
if [ $? -eq 0 ]; then # Tests command2, not command1!
echo "Success"
fiSolution
# Right - test immediately
command1
if [ $? -eq 0 ]; then
echo "command1 succeeded"
fi
# Better - test directly
if command1; then
echo "Success"
fi7. Arrays in POSIX sh Scripts
Problem
#!/bin/sh
# Wrong - arrays not in POSIX sh
array=(one two three)
echo "${array[0]}"Solution
# Use bash
#!/bin/bash
array=(one two three)
echo "${array[0]}"
# Or use POSIX alternatives
set -- one two three
echo "$1"8. Not Declaring Functions Before Use
Problem
# Wrong - function not defined yet
my_function
my_function() {
echo "Hello"
}Solution
# Right - define first
my_function() {
echo "Hello"
}
my_function9. Using eval Unsafely
Problem
# DANGEROUS
user_input="$1"
eval "$user_input" # Command injection risk!Consequence
Security vulnerability - arbitrary code execution.
Solution
# Avoid eval when possible
# If necessary, sanitize input thoroughly
# Or use safer alternatives
# Example: dynamic variable names
var_name="my_var"
# Don't: eval "echo \$$var_name"
# Do: Use indirect expansion (bash)
echo "${!var_name}"10. Forgetting set -u
Problem
# Wrong - typo goes unnoticed
nmae="John" # Typo
echo "Hello, $name" # Prints "Hello, " (empty)Solution
# Right - use set -u
set -u
nmae="John" # Typo
echo "Hello, $name" # Error: name: unbound variable11. Incorrect String Comparison
Problem
# Wrong - numeric comparison on strings
if [ "$version" -gt "2.0" ]; then
echo "New version"
fiSolution
# Right - string comparison
if [ "$version" = "2.0" ]; then
echo "Exact match"
fi
# Or use proper version comparison
if [[ "$version" > "2.0" ]]; then
echo "Greater"
fi12. Not Handling Spaces in Filenames
Problem
# Wrong
for file in $(ls *.txt); do
echo "$file"
doneConsequence
Files with spaces break into multiple items.
Solution
# Right - use glob directly
for file in *.txt; do
echo "$file"
done
# Or use find with -print0
while IFS= read -r -d '' file; do
echo "$file"
done < <(find . -name "*.txt" -print0)13. Backticks Instead of $()
Problem
# Deprecated
result=`command arg1 arg2`Solution
# Modern
result=$(command arg1 arg2)Why
- Better nesting:
$(cmd1 $(cmd2)) - Better readability
- Fewer escaping issues
14. Using = Instead of == in [[ ]]
Not really a mistake, but inconsistent:
# Both work in [[ ]]
[[ "$var" = "value" ]] # POSIX style (works)
[[ "$var" == "value" ]] # Bash style (also works)
# Only = works in [ ]
[ "$var" = "value" ] # Works
[ "$var" == "value" ] # May fail in POSIX shRecommendation: Use = for portability, or stick to == in bash with [[ ]].
15. Not Quoting $@
Problem
# Wrong
script.sh "$@" # Right
command $@ # Wrong if args have spacesSolution
# Right
command "$@" # Preserves argument boundaries16. Using ls to Process Files
Problem
# Wrong
files=$(ls *.txt)
for file in $files; do
process "$file"
doneIssues
- Breaks on spaces
- Breaks on newlines in filenames
- Breaks on glob characters
Solution
# Right
for file in *.txt; do
process "$file"
done
# Or with find
find . -name "*.txt" -exec process {} \;17. Incorrect Exit Codes
Problem
# Wrong
function check_file() {
if [ -f "$1" ]; then
echo "File exists"
return 1 # Success should be 0!
fi
return 0
}Solution
# Right - 0 is success, non-zero is failure
function check_file() {
if [ -f "$1" ]; then
echo "File exists"
return 0
fi
return 1
}18. Using -a and -o in [ ]
Problem
# Deprecated and error-prone
[ "$a" = "x" -a "$b" = "y" ]
[ "$a" = "x" -o "$b" = "y" ]Solution
# Right - use && and ||
[ "$a" = "x" ] && [ "$b" = "y" ]
[ "$a" = "x" ] || [ "$b" = "y" ]
# Or use [[ ]] in bash
[[ "$a" = "x" && "$b" = "y" ]]
[[ "$a" = "x" || "$b" = "y" ]]19. Not Making Scripts Executable
Problem
# Wrong
bash script.sh # Works but not idealSolution
# Right
chmod +x script.sh
./script.shAnd include proper shebang:
#!/usr/bin/env bash20. Forgetting Final Newline
Problem
Some tools expect files to end with a newline.
Solution
Ensure your editor adds a final newline, or:
echo "" >> file21. Using grep -q Without Knowing Implications
Problem
# Potentially inefficient
if [ "$(grep pattern file)" ]; then
echo "Found"
fiSolution
# Better - grep -q exits on first match
if grep -q pattern file; then
echo "Found"
fi22. Incorrect glob Pattern
Problem
# Wrong - doesn't match hidden files
for file in *; do
process "$file"
doneSolution
# Include hidden files (bash)
shopt -s dotglob
for file in *; do
process "$file"
done
shopt -u dotglob
# Or explicitly
for file in * .[!.]* ..?*; do
[ -e "$file" ] && process "$file"
done23. Not Handling Empty Globs
Problem
# Fails if no .txt files
for file in *.txt; do
process "$file" # Processes literal "*.txt"
doneSolution
# Bash - fail gracefully
shopt -s nullglob
for file in *.txt; do
process "$file"
done
shopt -u nullglob
# POSIX - check existence
for file in *.txt; do
[ -e "$file" ] || continue
process "$file"
done24. Not Sanitizing Input
Problem
# Dangerous
rm -rf "/$1" # What if $1 is empty or manipulated?Solution
# Safer
if [ -z "$1" ]; then
echo "Error: No argument provided" >&2
exit 1
fi
# Validate
case "$1" in
/*)
echo "Error: Absolute paths not allowed" >&2
exit 1
;;
esac
rm -rf "$1"25. Using -e for File Existence
Not a mistake, but be specific:
[ -e "$file" ] # Exists (any type)
[ -f "$file" ] # Regular file
[ -d "$file" ] # Directory
[ -L "$file" ] # Symbolic link
[ -r "$file" ] # Readable
[ -w "$file" ] # Writable
[ -x "$file" ] # ExecutableQuick Checklist
Before running a script, verify:
- Proper shebang (#!/bin/bash or #!/bin/sh)
- set -euo pipefail (strict mode)
- All variables quoted
- Error handling for critical commands
- Using $() not backticks
- Not using ls for file processing
- Functions defined before use
- Proper exit codes (0 = success)
- Input validation
- ShellCheck passes
Resources
- ShellCheck - Catches most of these
- Bash Pitfalls
- POSIX Shell