| name | shell-guide |
| description | Shell/Bash scripting guardrails, patterns, and best practices for AI-assisted development.
Use when working with shell scripts (.sh, .bash), Makefiles, or when the user mentions Bash/Shell.
Provides POSIX compliance guidelines, error handling patterns, and ShellCheck rules
specific to this project's coding standards.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"language","language":"shell","extensions":".sh,.bash,.zsh"} |
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 pipefail to 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, local for function variables, named constants for magic values
- Fail Loudly: Never swallow errors silently; use
trap for cleanup and meaningful exit codes
- ShellCheck Clean: All scripts pass
shellcheck with zero warnings before commit
Guardrails
Shebang and Strict Mode
- Every script:
#!/usr/bin/env bash (or #!/bin/sh for POSIX)
- Immediately follow with
set -euo pipefail
- Use
set -x only for debugging, never in production scripts
- POSIX scripts must not use bash-specific features (
[[ ]], arrays, local)
#!/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 ))
grep -r "$pattern" "$directory"
for arg in "$@"; do process "$arg"; done
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 bash over #!/bin/bash (varies across systems)
command -v instead of which for executable checks
$(command) instead of backticks
printf over echo for portable output (flags/escapes differ)
Security
- Never use
eval (use arrays for dynamic command building)
- Validate all external input (arguments, env vars, file contents)
mktemp for temp files, never hardcoded /tmp/myapp.tmp
- No secrets in script files; read from environment or secret managers
umask 077 before creating sensitive files
tmpfile="$(mktemp)" || exit 1
trap 'rm -f "$tmpfile"' EXIT
eval "$user_input"
password="hunter2"
Script Structure
#!/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
db_host="${DB_HOST:-localhost}"
api_key="${API_KEY:?Error: API_KEY required}"
filename="archive.tar.gz"
name="${filename%%.*}"
ext="${filename#*.}"
path="/usr/local/bin/tool"
dir="${path%/*}"
base="${path##*/}"
upper="${var^^}"
lower="${var,,}"
Trap for Cleanup
cleanup() {
local exit_code=$?
rm -f "$tmpfile"
exit "$exit_code"
}
trap cleanup EXIT
trap 'echo "Interrupted" >&2; exit 130' INT TERM
Arrays (Avoid eval)
declare -a files=()
files+=("first.txt" "second.txt")
for file in "${files[@]}"; do echo "$file"; done
cmd=(curl --silent --fail)
[[ -n "${TOKEN:-}" ]] && cmd+=(--header "Authorization: Bearer $TOKEN")
"${cmd[@]}" "$url"
Functions
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
cat <<EOF # Interpolated
Hello, $USER at $(hostname)
EOF
cat <<'EOF'
This $variable stays literal.
EOF
Safe File Iteration
while IFS= read -r -d '' file; do
process "$file"
done < <(find "$dir" -type f -name "*.log" -print0)
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
diff <(sort file1.txt) <(sort file2.txt)
Testing
bats-core
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) or shellspec
- Test files in
test/ or spec/ directory
- Test names describe behavior:
"deploy requires environment argument"
- Use
setup/teardown for temp dirs and fixtures
- Coverage: >80% library functions, >60% scripts
- Each test must be independent
Tooling
ShellCheck
shellcheck script.sh
find . -name "*.sh" -exec shellcheck {} +
unused_looking_var="value"
shfmt
shfmt -w -i 4 -bn script.sh
shfmt -d -i 4 script.sh
Essential Commands
shellcheck *.sh
shfmt -d -i 4 *.sh
bats test/
bash -n script.sh
References
For detailed patterns and examples, see:
External References