Table of Contents
Shell Best Practices
Script Structure
Start with a shebang line and safety flags. Use clear variable names, proper quoting, and functions to organize code.
#!/bin/bash set -e -u -o pipefail # Script configuration readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly DATA_FILE="$SCRIPT_DIR/data.txt" # Main function main() { if [[ $# -lt 1 ]]; then usage exit 1 fi local input="$1" process_input "$input" } # Helper function process_input() { local input="$1" # Use "$input" (quoted) not $input echo "Processing: $input" } usage() { cat << EOF Usage: $0 INPUT EOF } # Make script executable: chmod +x script.sh main "$@"
Explain the safety flags:
set -e— Exit immediately if any command fails (exit status non-zero)set -u— Exit if undefined variable is used (catches typos)set -o pipefail— Pipe fails if any command in the pipeline fails
Always quote variables to preserve whitespace and special characters:
# BAD: loses structure with spaces for file in $files; do process $file # Breaks if $files contains filenames with spaces done # GOOD: preserves structure for file in "$files"; do process "$file" done # GOOD: array iteration for file in "${files[@]}"; do process "$file" done
Debugging
Enable debug mode with set -x to trace command execution. Improve debug output with a custom PS4 prompt.
# Enable debugging for the entire script #!/bin/bash set -x # Or enable debugging only for a section set -x # ... code to debug ... set +x # Improve debug output with line numbers and filenames set -x PS4='+ ${BASH_SOURCE}:${LINENO}: ' # Now debug output shows which file and line is executing # Example output: # + /path/to/script.sh:42: command arg1 arg2
Use shellcheck to lint scripts and catch common errors:
# Install shellcheck (available in most package managers) # apt install shellcheck, brew install shellcheck, etc. # Check a script shellcheck script.sh # It catches issues like: # - Unquoted variables # - Unreachable code # - Incorrect comparison operators # - Variable shadowing
Test POSIX compatibility by running with sh instead of bash:
# Run script with sh (more portable, stricter) sh script.sh # Avoid bash-specific features for maximum portability: # - Use [ ] instead of [[ ]] (though [[ ]] is very common) # - Use $(cmd) instead of `cmd` # - Avoid arrays (not in POSIX sh) # - Avoid declare, local (check sh compatibility)
Common Pitfalls
Forgetting to quote variables is the most common mistake. It breaks on spaces, glob patterns, and empty values.
# BAD: breaks with spaces and glob patterns file="my file.txt" rm $file # Expands to: rm my file.txt (wrong!) # GOOD: preserves spaces and special characters rm "$file" # Correctly removes "my file.txt" # BAD: numeric comparison with string operator if [ "$num" = "5" ]; then # String comparison (wrong) echo "five" fi # GOOD: use -eq for numeric comparison if [[ $num -eq 5 ]]; then # Numeric comparison (correct) echo "five" fi
Use [[ ]] instead of [ ] for safer conditionals in bash (allows =~ regex, safer quoting):
# BAD: needs careful quoting if [ -z "$var" ]; then echo "empty"; fi # GOOD: safer with [[ if [[ -z $var ]]; then echo "empty"; fi # BAD: backticks are harder to nest result=`cat \`ls file.txt\`` # GOOD: $() is nestable and modern result=$(cat "$(ls file.txt)")
Always check exit codes when they matter:
# BAD: ignores failure silently download_file # GOOD: check status if ! download_file; then echo "Download failed" exit 1 fi # GOOD: short-circuit operators download_file || exit 1 # BAD: secrets exposed in environment export DB_PASSWORD="secret123" ps aux | grep script # Password visible! # GOOD: read from secure file DB_PASSWORD=$(cat /etc/app/db_password) export DB_USER="appuser" # Non-sensitive only
Performance
Avoid subshells and command substitutions in loops. Use built-in string operations instead of external commands.
# BAD: spawns subprocess for each iteration for line in "$(cat bigfile.txt)"; do process "$line" done # GOOD: reads directly without subprocess while IFS= read -r line; do process "$line" done < bigfile.txt # BAD: multiple sed invocations text=$1 text=$(echo "$text" | sed 's/a/b/') text=$(echo "$text" | sed 's/c/d/') # GOOD: use bash string operations text="${1//a/b}" # Replace first text="${text//c/d}" # Replace all
Minimize command substitutions and use built-ins:
# BAD: spawns external command count=$(echo "$str" | wc -c) # GOOD: use bash string length count=${#str} # BAD: multiple external calls upper=$(echo "$str" | tr 'a-z' 'A-Z') # GOOD: bash 4+ parameter expansion upper="${str^^}" # BAD: spawns grep if echo "$line" | grep -q "pattern"; then ... # GOOD: use bash pattern matching if [[ $line == *pattern* ]]; then ...
Prefer [[ ]] over [ ] for better performance and fewer quoting issues:
# BAD: slower and requires more quoting if [ -n "$var" ] && [ "$var" != "x" ]; then ... # GOOD: faster, safer if [[ -n $var && $var != x ]]; then ...
