Write bash scripts following best practices for safety, portability, and maintainability. Use when creating new shell scripts or refactoring existing ones.
Write robust, portable, and maintainable bash scripts by applying a standard template, coding conventions, security rules, and portability guidelines.
sh compatibility is required.getopts if the script takes no arguments). Keep it minimal.shellcheck <script> (if available) and fix every warning. Explain each fix to the user.#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cleanup() {
# Remove temp files, restore state, etc.
:
}
trap cleanup EXIT
usage() {
cat <<EOF
Usage: ${SCRIPT_NAME} [options] <args>
Description of what this script does.
Options:
-h Show this help message
-v Enable verbose output
EOF
}
log() {
printf '%s: %s\n' "${SCRIPT_NAME}" "$*" >&2
}
die() {
log "error: $*"
exit 1
}
main() {
local verbose=0
while getopts ":hv" opt; do
case "${opt}" in
h) usage; exit 0 ;;
v) verbose=1 ;;
:) die "option -${OPTARG} requires an argument" ;;
*) die "unknown option -${OPTARG}" ;;
esac
done
shift $((OPTIND - 1))
# --- script logic starts here ---
}
main "$@"
snake_case for locals, UPPER_SNAKE_CASE for constants and environment variables."$var", "$@", "$(cmd)". Unquoted expansions are acceptable only inside [[ ]] on the right side of =~ or == with intentional globbing.fname() { } form. Do not use the function keyword.[[ ]] over [ ]. Use (( )) for arithmetic.$(command), never backticks.local inside functions. Separate declaration from assignment when capturing exit codes: local output; output="$(cmd)".eval: Do not use eval. If absolutely unavoidable, add a comment explaining why and what input has been validated.mktemp. Clean up in a trap cleanup EXIT handler. Never use predictable paths like /tmp/myscript.tmp.rm -rf: Never write rm -rf "$dir" without first verifying that $dir is non-empty and expected. Prefer rm -rf "${dir:?}" to abort on unset variables.curl ... | bash patterns. Download first, inspect, then execute.When the script must work on both macOS and Linux, watch for these common incompatibilities:
| Area | macOS (BSD) | Linux (GNU) | Workaround |
|---|---|---|---|
sed in-place | sed -i '' 's/.../g' | sed -i 's/.../g' | Use sed -i.bak ... && rm *.bak, or detect OS |
date relative | No -d flag | date -d '1 day ago' | Use date -v-1d on macOS or perl/python |
readlink -f | Not supported | Works | Use cd "$(dirname "$0")" && pwd pattern instead |
grep -P (PCRE) | Not supported | Works | Use grep -E or awk |
mktemp template | mktemp -t prefix | mktemp prefix.XXXXXX | Use mktemp "${TMPDIR:-/tmp}/prefix.XXXXXX" |
xargs -r | Not supported | Skips empty input | Pipe through grep . before xargs, or check input |
If strict POSIX sh compatibility is required, additionally avoid: [[ ]], arrays, local (non-POSIX but widely supported), process substitution <(), and {a..z} brace expansion.
getopts or usage.