| name | bash |
| description | [Applies to: **/*] Enforces modern, robust, and maintainable bash scripting practices, focusing on error handling, quoting, variable management, and code organization. |
| source | cursor_mdc |
bash Best Practices
This guide outlines the definitive best practices for writing bash scripts. Adhere to these rules to ensure your scripts are reliable, readable, and maintainable by the entire team.
1. Code Organization and Structure
1.1 Shebang and Interpreter Options
Always start scripts with #!/usr/bin/env bash for portability. Immediately follow with set -euo pipefail to ensure robust error handling.
set -e (errexit): Exit immediately if a command exits with a non-zero status.
set -u (nounset): Treat unset variables as an error and exit immediately.
set -o pipefail: The return value of a pipeline is the status of the last command to exit with a non-zero status, or zero if all commands exit successfully.
❌ BAD:
#!/bin/bash
✅ GOOD:
#!/usr/bin/env bash
set -euo pipefail
1.2 File Header and Comments
Every script must begin with a descriptive header. Use clear, concise comments for functions and complex logic.
#!/usr/bin/env bash
set -euo pipefail
1.3 Functions and main Entry Point
Encapsulate all script logic within functions. Use a main function as the primary entry point. This improves readability, reusability, and variable scoping.
❌ BAD:
#!/usr/bin/env bash
set -euo pipefail
echo "Starting operation..."
ls -l /tmp
✅ GOOD:
#!/usr/bin/env bash
set -euo pipefail
process_item() {
local item="${1}"
echo "Processing: ${item}"
}
main() {
echo "Script started."
for arg in "$@"; do
process_item "${arg}"
done
echo "Script finished."
}
main "$@"
1.4 Variable Scoping and Naming
Declare variables with local inside functions. Use UPPER_SNAKE_CASE for global readonly constants and lower_snake_case for local variables and function names.
❌ BAD:
#!/usr/bin/env bash
set -euo pipefail
GLOBAL_VAR="value"
my_func() {
temp_var="another_value"
}
✅ GOOD:
#!/usr/bin/env bash
set -euo pipefail
readonly CONFIG_FILE="/etc/my_app/config.conf"
readonly LOG_LEVEL="INFO"
my_function() {
local temp_message="This is a local message."
echo "${LOG_LEVEL}: ${temp_message}"
}
main() {
my_function
}
main "$@"
1.5 Indentation and Line Length
Use 2 spaces for indentation. Limit lines to approximately 80 characters. Break long lines with backslashes \ for readability.
❌ BAD:
if [[ "${long_variable_name_one}" == "${long_variable_name_two}" && "${another_long_variable}" -gt 100 ]]; then echo "This line is way too long and hard to read."; fi
✅ GOOD:
if [[ "${long_variable_name_one}" == "${long_variable_name_two}" && \
"${another_long_variable}" -gt 100 ]]; then
echo "This line is readable."
fi
1.6 Cleanup with trap
Use trap to ensure resources (e.g., temporary files) are cleaned up even if the script exits unexpectedly.
#!/usr/bin/env bash
set -euo pipefail
readonly TMP_DIR="$(mktemp -d)"
cleanup() {
echo "Cleaning up temporary directory: ${TMP_DIR}" >&2
rm -rf "${TMP_DIR}"
}
trap cleanup EXIT INT TERM
main() {
echo "Working in temporary directory: ${TMP_DIR}"
touch "${TMP_DIR}/temp_file.txt"
}
main "$@"
2. Common Patterns and Anti-patterns
2.1 Always Quote Variables and Command Substitutions
This is the single most important rule. Always double-quote variable expansions ("${var}") and use $(command) for command substitution to prevent word splitting and globbing.
❌ BAD:
files="file1.txt file with spaces.txt"
for f in $files; do
echo "Processing: $f"
done
output=`ls -l`
✅ GOOD:
files=("file1.txt" "file with spaces.txt")
for f in "${files[@]}"; do
echo "Processing: \"${f}\""
done
output=$(ls -l)
2.2 Prefer [[ ... ]] for Conditionals
Use the bash-specific [[ ... ]] construct over [ ... ] (test). [[ ... ]] is safer, handles quoting internally, and supports advanced features like regex matching (=~).
❌ BAD:
if [ "$my_var" = "value" ]; then
echo "Legacy test"
fi
✅ GOOD:
if [[ "${my_var}" == "value" ]]; then
echo "Modern test"
fi
if [[ "${filename}" =~ \.log$ ]]; then
echo "It's a log file."
fi
2.3 Use Long Options for Readability
Prefer long-form command options (--recursive) over short-form (-r) in scripts for improved readability.
❌ BAD:
rm -rf "${dir}"
✅ GOOD:
rm --recursive --force -- "${dir}"
2.4 Redirect Errors to STDERR
Ensure all error messages and diagnostic output go to STDERR (>&2), reserving STDOUT for the intended program output.
log_error() {
printf "[ERROR] %s: %s\n" "$(date '+%Y-%m-%dT%H:%M:%S%z')" "${*}" >&2
}
if ! some_command; then
log_error "Failed to execute some_command."
exit 1
fi
2.5 Heredocs for Multi-line Strings
Use heredocs for multi-line strings. Quote the tag (<<'EOF') to prevent variable expansion and command substitution within the heredoc.
❌ BAD:
echo "Hello ${USER},"
echo "This is a multi-line message."
echo "Current date: $(date)"
✅ GOOD (Literal):
cat <<'EOF'
Hello ${USER},
This is a literal multi-line message.
Current date: $(date)
EOF
✅ GOOD (Interpolated):
cat <<EOF
Hello ${USER},
This is an interpolated multi-line message.
Current date: $(date)
EOF
2.6 Arithmetic with (( ... ))
Use (( ... )) for arithmetic operations. It's cleaner and safer than expr or let.
❌ BAD:
COUNT=`expr $COUNT + 1`
let COUNT=COUNT+1
✅ GOOD:
count=0
((count++))
echo "${count}"
num_a=10
num_b=5
result=$((num_a * num_b))
echo "${result}"
2.7 Scoped Directory Changes
When changing directories, use a subshell (cd ...) or pushd/popd to ensure the change is temporary and doesn't affect the rest of the script.
❌ BAD:
cd /tmp/my_app
cd -
✅ GOOD:
(
cd /tmp/my_app || exit 1
echo "Current directory in subshell: $(pwd)"
)
echo "Current directory outside subshell: $(pwd)"
3. Common Pitfalls and Gotchas
3.1 while read in a Pipe
Variables set inside a while read loop that is part of a pipeline will not persist outside the loop, as the loop runs in a subshell.
❌ BAD:
count=0
ls | while read -r file; do
((count++))
done
echo "Total files: ${count}"
✅ GOOD:
count=0
while read -r file; do
((count++))
done < <(ls)
echo "Total files: ${count}"
mapfile -t files_array < <(ls)
count="${#files_array[@]}"
echo "Total files: ${count}"
3.2 sudo with Redirection
Redirection (>) happens before sudo executes the command. To write to a root-owned file, run the entire command under sudo.
❌ BAD:
echo "Sensitive data" > /root/protected_file
✅ GOOD:
echo "Sensitive data" | sudo tee /root/protected_file > /dev/null
sudo bash -c 'echo "Sensitive data" > /root/protected_file'
3.3 Avoid eval
eval is a security risk and makes scripts hard to debug. Avoid it unless absolutely necessary and you fully control the input.
❌ BAD:
user_input="rm -rf /"
eval "${user_input}"
✅ GOOD:
4. Testing Approaches
4.1 shellcheck
Integrate shellcheck into your workflow. It's an invaluable static analysis tool that catches common pitfalls and warns about bad practices.
shellcheck myscript.sh
4.2 Debugging with set -x
Use set -x (xtrace) for debugging. It prints each command and its arguments after expansion. Localize its use to specific sections.
#!/usr/bin/env bash
set -euo pipefail
my_debug_function() {
set -x
local debug_var="hello"
echo "Debug var: ${debug_var}"
set +x
}
main() {
echo "Before debug function."
my_debug_function
echo "After debug function."
}
main "$@"
4.3 Syntax Check with bash -n
Before running, perform a dry run to check for syntax errors.
bash -n myscript.sh