Table of Contents
- Missing or Incorrect Shebang Line
- Ignoring Exit Codes
- Unquoted Variables
- Not Checking if Files/Directories Exist
- Overusing Global Variables
- Insecure Temporary Files
- Incorrect Use of
set -e - Not Escaping Special Characters in Input
- Poor Error Handling and Logging
- Not Testing Scripts Thoroughly
1. Missing or Incorrect Shebang Line
What’s the Problem?
The shebang line (#!/path/to/interpreter) tells the system which shell (e.g., bash, sh) to use to run the script. Omitting it or using an incorrect interpreter (e.g., #!/bin/sh when the script uses bash-specific features) leads to unexpected behavior or syntax errors.
Example of the Mistake
# No shebang line
echo "Hello, World"
array=("apple" "banana") # Bash-specific syntax; fails if run with /bin/sh
Why It Fails
If the script is executed as ./script.sh without a shebang, the system defaults to the current shell (e.g., sh), which may not support bash features like arrays. Using #!/bin/sh (a POSIX-compliant shell) with bash-only code will also cause failures.
How to Fix It
Always start scripts with a explicit shebang. Use #!/bin/bash for bash scripts or #!/bin/sh only for POSIX-compliant code:
#!/bin/bash
echo "Hello, World"
array=("apple" "banana") # Now works with bash
Best Practices
- Use
#!/usr/bin/env bashinstead of#!/bin/bashfor portability (works ifbashis in the user’sPATH). - Avoid mixing
bashand POSIXshsyntax.
2. Ignoring Exit Codes
What’s the Problem?
Commands return exit codes (0 = success, non-zero = failure). Scripts often ignore these, proceeding even if critical commands fail (e.g., cd to a non-existent directory).
Example of the Mistake
#!/bin/bash
cd /non/existent/directory # Fails, but script continues
echo "Working in $(pwd)" # Prints the original directory, not /non/existent/directory
Why It Fails
The cd command fails (exit code 1), but the script proceeds, leading to incorrect behavior (e.g., writing files to the wrong directory).
How to Fix It
Explicitly check exit codes or use set -e to exit on non-zero codes:
#!/bin/bash
set -e # Exit immediately if any command fails
cd /non/existent/directory # Script exits here
echo "Working in $(pwd)" # Never runs
Or check manually for granular control:
#!/bin/bash
if ! cd /non/existent/directory; then
echo "Error: Failed to cd to /non/existent/directory" >&2
exit 1
fi
echo "Working in $(pwd)"
Best Practices
- Use
set -euo pipefail(see Section 9) for strict error handling. - Explicitly handle expected failures (e.g.,
grep "pattern" file || trueto ignore “not found” errors).
3. Unquoted Variables
What’s the Problem?
Unquoted variables undergo word splitting and globbing, breaking filenames with spaces, tabs, or special characters (e.g., *).
Example of the Mistake
#!/bin/bash
file="my file.txt" # Filename with a space
cat $file # Unquoted: splits into "my" and "file.txt" → "cat: my: No such file"
Why It Fails
The variable $file expands to my file.txt, which the shell splits into two arguments (my and file.txt). cat tries to open my and file.txt separately, leading to errors.
How to Fix It
Always quote variables with "$var" to preserve spaces and prevent globbing:
#!/bin/bash
file="my file.txt"
cat "$file" # Quoted: passes "my file.txt" as a single argument → works
Best Practices
- Quote variables, command substitutions (
"$(command)"), and array elements. - Avoid unquoted variables unless intentional word splitting is needed.
4. Not Checking if Files/Directories Exist
What’s the Problem?
Scripts often assume files or directories exist, leading to “No such file or directory” errors when they don’t.
Example of the Mistake
#!/bin/bash
input_file="$1"
grep "error" "$input_file" > errors.log # Fails if $input_file doesn't exist
Why It Fails
If $1 is not provided or the file doesn’t exist, grep throws an error, and the script may write to errors.log incorrectly.
How to Fix It
Check for file/directory existence before using them:
#!/bin/bash
input_file="$1"
# Check if input file exists
if [ ! -f "$input_file" ]; then
echo "Error: File $input_file not found." >&2
exit 1
fi
grep "error" "$input_file" > errors.log
Best Practices
- Use
-f(file),-d(directory),-r(readable), or-x(executable) flags in[ ]or[[ ]]to validate paths. - Handle missing arguments with
if [ $# -eq 0 ]; then echo "Usage: $0 <file>"; exit 1; fi.
5. Overusing Global Variables
What’s the Problem?
Global variables are accessible everywhere, leading to unintended side effects (e.g., functions modifying variables used elsewhere).
Example of the Mistake
#!/bin/bash
count=0
increment() {
count=$((count + 1)) # Modifies global variable
}
increment
echo "Count: $count" # Output: Count: 1 (expected)
# Later in the script...
count=5
increment
echo "Count: $count" # Output: Count: 6 (unintended if 'count' was reused)
Why It Fails
The increment function relies on and modifies the global count, making the script harder to debug and prone to side effects.
How to Fix It
Use local variables in functions to limit scope:
#!/bin/bash
count=0
increment() {
local local_count=$1 # Local variable; no side effects
echo $((local_count + 1))
}
count=$(increment "$count")
echo "Count: $count" # Output: Count: 1
count=5
count=$(increment "$count")
echo "Count: $count" # Output: Count: 6 (explicit and intentional)
Best Practices
- Pass variables as function arguments and return values explicitly.
- Use
localfor all function variables to avoid polluting the global namespace.
6. Insecure Temporary Files
What’s the Problem?
Hardcoding temporary file paths (e.g., /tmp/myscript.tmp) creates security risks: attackers can predict filenames and use symlinks to overwrite sensitive data.
Example of the Mistake
#!/bin/bash
temp_file="/tmp/myscript.tmp" # Predictable path
echo "Sensitive data" > "$temp_file" # Risk of symlink attack
Why It Fails
An attacker could create a symlink /tmp/myscript.tmp pointing to /etc/passwd, causing the script to overwrite system files.
How to Fix It
Use mktemp to generate unique, secure temporary files:
#!/bin/bash
temp_file=$(mktemp) # Creates a unique file (e.g., /tmp/tmp.abc123)
trap 'rm -f "$temp_file"' EXIT # Clean up on exit
echo "Sensitive data" > "$temp_file"
# ... use $temp_file ...
Best Practices
- Always use
mktempormktemp -dfor directories. - Add a
trapto delete temporary files on script exit (even if it fails).
7. Incorrect Use of set -e
What’s the Problem?
set -e exits the script on any non-zero exit code, but some commands return non-zero intentionally (e.g., grep not finding a pattern, diff finding differences).
Example of the Mistake
#!/bin/bash
set -e
grep "pattern" file.txt # Exits if "pattern" not found (non-zero exit code)
echo "This line never runs if grep fails"
Why It Fails
grep returns 1 if no matches are found, causing set -e to exit the script prematurely.
How to Fix It
Either disable set -e for specific commands or handle non-zero codes explicitly:
#!/bin/bash
set -e
# Option 1: Ignore non-zero with '|| true'
grep "pattern" file.txt || true # Script continues even if grep fails
# Option 2: Check explicitly
if grep "pattern" file.txt; then
echo "Pattern found"
else
echo "Pattern not found" # No exit
fi
Best Practices
- Use
set -ejudiciously. Combine withset -o pipefailto exit on pipeline failures. - For commands with expected non-zero codes, add
|| trueor check withif.
8. Not Escaping Special Characters in Input
What’s the Problem?
User input with special characters (e.g., ;, *, $) can be interpreted as shell commands, leading to injection attacks.
Example of the Mistake
#!/bin/bash
read -p "Enter a filename: " filename
cat "$filename" #看似安全,但如果输入是 "; rm -rf /" 呢?
Why It Fails
If a user enters ; rm -rf /, the script executes cat; rm -rf /, deleting system files (in a worst-case scenario).
How to Fix It
Treat input as data, not code. Use printf "%q" to escape special characters or avoid passing input directly to commands:
#!/bin/bash
read -p "Enter a filename: " filename
# Validate input: check if it's a valid file path
if [[ ! "$filename" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
echo "Error: Invalid filename." >&2
exit 1
fi
if [ -f "$filename" ]; then
cat "$filename"
else
echo "File not found." >&2
fi
Best Practices
- Sanitize all user input (e.g., restrict allowed characters).
- Avoid using
evalwith untrusted input.
9. Poor Error Handling and Logging
What’s the Problem?
Scripts often fail silently or lack context about errors (e.g., “Permission denied” without line numbers or file paths).
Example of the Mistake
#!/bin/bash
cp file1.txt /backup/ # Fails if /backup/ is read-only; no error message
Why It Fails
The cp command fails, but the script gives no indication, leaving the user unaware of the problem.
How to Fix It
Enable “bash strict mode” and add logging:
#!/bin/bash
set -o errexit # Exit on non-zero
set -o nounset # Treat unset variables as errors
set -o pipefail # Exit if any command in a pipeline fails
set -o xtrace # Optional: Print commands as they run (debugging)
# Log errors with context
error_exit() {
echo "Error: $1 (Line $2)" >&2
exit 1
}
cp file1.txt /backup/ || error_exit "Failed to copy file1.txt to /backup/" "$LINENO"
Best Practices
- Use
set -euo pipefailfor strict error checking. - Define an error-handling function with
$LINENOto report where failures occur.
10. Not Testing Scripts Thoroughly
What’s the Problem?
Scripts are often deployed without testing edge cases (e.g., empty inputs, missing files, special characters), leading to production failures.
Example of the Mistake
#!/bin/bash
# Script to process a file, but never tested with empty files or spaces in filenames
file="$1"
grep "data" "$file" | wc -l
Why It Fails
- If
$1is empty (nounsetnot enabled), the script runsgrep "data" "", which errors. - If the file has spaces and
$fileis unquoted, word splitting occurs.
How to Fix It
Test rigorously with tools and edge cases:
- Use
shellcheck: A linter for shell scripts (catches unquoted variables, missing checks, etc.).shellcheck script.sh # Highlights issues like unquoted variables - Test Edge Cases:
- Empty inputs (
./script.sh ""). - Files with spaces, special characters (
./script.sh "my file.txt"). - Missing dependencies (e.g.,
grepnot installed).
- Empty inputs (
- Debug with
set -x: Print commands as they execute to trace issues.
Conclusion
Shell scripting is a powerful tool, but its flexibility comes with pitfalls. By avoiding these 10 common mistakes—using explicit shebangs, checking exit codes, quoting variables, securing temporary files, and testing rigorously—you can write scripts that are reliable, secure, and easy to maintain.
Adopt best practices like “bash strict mode” (set -euo pipefail), use shellcheck for linting, and always validate inputs. With these habits, you’ll transform error-prone scripts into robust automation tools.