Shell/Bash Guide
Applies to: Bash 4+, POSIX sh, Automation Scripts, CI/CD Pipelines, Makefiles
Core Principles
- •Strict Mode Always: Every script starts with
set -euo pipefailto fail fast on errors - •Quote Everything: All variable expansions must be double-quoted to prevent word splitting and globbing
- •Explicit Over Implicit: Use
[[ ]]for conditionals,localfor function variables, named constants for magic values - •Fail Loudly: Never swallow errors silently; use
trapfor cleanup and meaningful exit codes - •ShellCheck Clean: All scripts pass
shellcheckwith zero warnings before commit
Guardrails
Shebang and Strict Mode
- •Every script:
#!/usr/bin/env bash(or#!/bin/shfor POSIX) - •Immediately follow with
set -euo pipefail - •Use
set -xonly for debugging, never in production scripts - •POSIX scripts must not use bash-specific features (
[[ ]], arrays,local)
bash
#!/usr/bin/env bash
set -euo pipefail
[[ "${TRACE:-}" == "1" ]] && set -x
Quoting
- •Always double-quote:
"$var","$@","${arr[@]}","$(command)" - •Single quotes for literals that must not be interpolated
- •Only omit quotes in arithmetic:
$(( count + 1 ))
bash
# Correct grep -r "$pattern" "$directory" for arg in "$@"; do process "$arg"; done # Wrong - word splitting and globbing bugs grep -r $pattern $directory for arg in $@; do process $arg; done
Error Handling
- •Check return codes:
if ! command; then handle_error; fi - •Inline:
critical_cmd || { echo "Failed" >&2; exit 1; } - •Never use
set +e(restructure logic instead) - •Exit codes: 0 = success, 1 = general error, 2 = usage error
- •Errors to stderr:
echo "Error: message" >&2
Portability
- •
#!/usr/bin/env bashover#!/bin/bash(varies across systems) - •
command -vinstead ofwhichfor executable checks - •
$(command)instead of backticks - •
printfoverechofor portable output (flags/escapes differ)
Security
- •Never use
eval(use arrays for dynamic command building) - •Validate all external input (arguments, env vars, file contents)
- •
mktempfor temp files, never hardcoded/tmp/myapp.tmp - •No secrets in script files; read from environment or secret managers
- •
umask 077before creating sensitive files
bash
# Safe temp file tmpfile="$(mktemp)" || exit 1 trap 'rm -f "$tmpfile"' EXIT # Never do this eval "$user_input" # Command injection password="hunter2" # Hardcoded secret
Script Structure
code
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# ── Configuration (with defaults) ─────────────────────
LOG_LEVEL="${LOG_LEVEL:-info}"
OUTPUT_DIR="${OUTPUT_DIR:-./output}"
# ── Functions ──────────────────────────────────────────
usage() { ... }
cleanup() { ... }
main() { ... }
# ── Traps & Entry ─────────────────────────────────────
trap cleanup EXIT
main "$@"
Key Patterns
Parameter Expansion
bash
db_host="${DB_HOST:-localhost}" # Default value
api_key="${API_KEY:?Error: API_KEY required}" # Required (fail if unset)
filename="archive.tar.gz"
name="${filename%%.*}" # "archive" (longest suffix removal)
ext="${filename#*.}" # "tar.gz" (shortest prefix removal)
path="/usr/local/bin/tool"
dir="${path%/*}" # "/usr/local/bin"
base="${path##*/}" # "tool"
upper="${var^^}" # UPPERCASE (Bash 4+)
lower="${var,,}" # lowercase (Bash 4+)
Trap for Cleanup
bash
cleanup() {
local exit_code=$?
rm -f "$tmpfile"
exit "$exit_code" # Preserve original exit code
}
trap cleanup EXIT
trap 'echo "Interrupted" >&2; exit 130' INT TERM
Arrays (Avoid eval)
bash
declare -a files=()
files+=("first.txt" "second.txt")
for file in "${files[@]}"; do echo "$file"; done
# Build commands safely with arrays
cmd=(curl --silent --fail)
[[ -n "${TOKEN:-}" ]] && cmd+=(--header "Authorization: Bearer $TOKEN")
"${cmd[@]}" "$url"
Functions
bash
process_file() {
local file="$1"
local -r max_lines=1000
local line_count
line_count="$(wc -l < "$file")"
if (( line_count > max_lines )); then
echo "Warning: $file exceeds $max_lines lines" >&2
fi
}
All variables inside functions must be declared local.
Here Documents
bash
cat <<EOF # Interpolated Hello, $USER at $(hostname) EOF cat <<'EOF' # Literal (no expansion) This $variable stays literal. EOF
Safe File Iteration
bash
# Handles spaces, newlines, special characters
while IFS= read -r -d '' file; do
process "$file"
done < <(find "$dir" -type f -name "*.log" -print0)
# Simple globs (Bash 4+)
shopt -s nullglob globstar
for file in "$dir"/**/*.sh; do process "$file"; done
Never use for file in $(find ...) -- it breaks on spaces.
Process Substitution
bash
diff <(sort file1.txt) <(sort file2.txt)
Testing
bats-core
bash
# test/deploy.bats
setup() { export TMPDIR="$(mktemp -d)"; }
teardown() { rm -rf "$TMPDIR"; }
@test "deploy requires environment argument" {
run ./deploy.sh
[ "$status" -ne 0 ]
[[ "$output" == *"Usage:"* ]]
}
Testing Standards
- •Test with
bats-core(preferred) orshellspec - •Test files in
test/orspec/directory - •Test names describe behavior:
"deploy requires environment argument" - •Use
setup/teardownfor temp dirs and fixtures - •Coverage: >80% library functions, >60% scripts
- •Each test must be independent
Tooling
ShellCheck
bash
shellcheck script.sh # Single file
find . -name "*.sh" -exec shellcheck {} + # All scripts
# Suppress with justification
# shellcheck disable=SC2034 # Variable used by sourced script
unused_looking_var="value"
shfmt
bash
shfmt -w -i 4 -bn script.sh # Format in place (4-space indent) shfmt -d -i 4 script.sh # Check only (diff output)
Essential Commands
bash
shellcheck *.sh # Lint shfmt -d -i 4 *.sh # Check formatting bats test/ # Run tests bash -n script.sh # Syntax check (no execution)
References
For detailed patterns and examples, see:
- •references/patterns.md -- Script templates, trap patterns, portable scripting examples