shell-guide
SKILL.md
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)
#!/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 ))
# 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 backticksprintfoverechofor 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
# 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
#!/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}" # 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
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)
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
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' # Literal (no expansion)
This $variable stays literal.
EOF
Safe File Iteration
# 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
diff <(sort file1.txt) <(sort file2.txt)
Testing
bats-core
# 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
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
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
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
External References
Weekly Installs
5
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
Mar 1, 2026
Security Audits
Installed on
gemini-cli5
opencode5
codebuddy5
github-copilot5
codex5
kimi-cli5