Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash.
From compound-engineeringnpx claudepluginhub iliaal/compound-engineering-plugin --plugin compound-engineeringThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Executes pre-written implementation plans: critically reviews, follows bite-sized steps exactly, runs verifications, tracks progress with checkpoints, uses git worktrees, stops on blockers.
Produce bash scripts that pass shellcheck --enable=all and shfmt -d with zero warnings.
Target: GNU Bash 4.4+ on Linux. No macOS/BSD workarounds, no Windows paths, no POSIX-only restrictions.
#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
trap 'rm -rf -- "${_tmpdir:-}"' EXIT
-E propagates ERR traps into functionsinherit_errexit propagates errexit into $() command substitutions_tmpdir=$(mktemp -d)main() { ... } with source guard: [[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@" -- enables sourcing for testing"$var", "$(cmd)", "${array[@]}"local for function variables, local -r for function constants, readonly for script constantsprintf '%s\n' over echo -- predictable behavior, no flag interpretation[[ ]] for conditionals; (( )) for arithmetic; $() over backticks--: rm -rf -- "$path", grep -- "$pattern" "$file": "${VAR:?must be set}"eval user input; build commands as arrays: cmd=("grep" "--" "$pat" "$f"); "${cmd[@]}"local from assignment to preserve exit codes: local val; val=$(cmd)PS4='+${BASH_SOURCE[0]}:${LINENO}: ' with bash -x -- shows file:line per commandreadonly EX_USAGE=64 EX_CONFIG=78 -- no magic numbers in exit"${PIPESTATUS[@]}" shows exit code of each pipe stage, not just last failure# NUL-delimited file processing
while IFS= read -r -d '' f; do
process "$f"
done < <(find /path -type f -name '*.log' -print0)
# Array from command output
readarray -t lines < <(command)
readarray -d '' files < <(find . -print0)
# Glob with no-match guard
for f in *.txt; do [[ -e "$f" ]] || continue; process "$f"; done
verbose=false; output=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) verbose=true; shift ;;
-o|--output) output="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) printf 'Unknown: %s\n' "$1" >&2; exit 1 ;;
*) break ;;
esac
done
Dependency check:
require() { command -v "$1" &>/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 1; }; }
require jq; require curl
Dry-run wrapper:
run() { if [[ "${DRY_RUN:-}" == "1" ]]; then printf '[dry] %s\n' "$*" >&2; else "$@"; fi; }
run cp "$src" "$dst"
Atomic file write -- write to temp, rename into place:
atomic_write() { local tmp; tmp=$(mktemp); cat >"$tmp"; mv -- "$tmp" "$1"; }
generate_config | atomic_write /etc/app/config.yml
Retry with backoff:
retry() { local n=0 max=5 delay=1; until "$@"; do ((++n>=max)) && return 1; sleep $delay; ((delay*=2)); done; }
retry curl -fsSL "$url"
Script locking -- prevent concurrent runs:
exec 9>/var/lock/"${0##*/}".lock
flock -n 9 || { printf 'Already running\n' >&2; exit 1; }
Idempotent operations -- safe to rerun:
ensure_dir() { [[ -d "$1" ]] || mkdir -p -- "$1"; }
ensure_link() { [[ -L "$2" ]] || ln -s -- "$1" "$2"; }
Input validation: [[ "$1" =~ ^[1-9][0-9]*$ ]] || die "Invalid: $1" -- validate at script boundaries with [[ =~ ]]
umask 077 for scripts creating sensitive filestrap 'cleanup; exit 130' INT TERM -- preserves correct exit codes for callerslog() { printf '[%s] [%s] %s\n' "$(date -Iseconds)" "$1" "${*:2}" >&2; }
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
die() { error "$@"; exit 1; }
| Bad | Fix |
|---|---|
for f in $(ls) | for f in *; do or find -print0 | while read |
local x=$(cmd) | local x; x=$(cmd) -- preserves exit code |
echo "$data" | printf '%s\n' "$data" |
cat file | grep | grep pat file |
kill -9 $pid first | kill "$pid" first, -9 as last resort |
cd dir; cmd | `cd dir |
${path%/*} not dirname, ${path##*/} not basename, ${var//old/new} not sed(( )) over expr; [[ =~ ]] over echo | grepval=$(cmd) once, reuse $valxargs -0 -P "$(nproc)" for parallel workdeclare -A map for lookups instead of repeated grep${var@Q} shell-quoted, ${var@U} uppercase, ${var@L} lowercasedeclare -n ref=varname nameref for indirect accesswait -n wait for any background job$EPOCHSECONDS, $EPOCHREALTIME -- timestamps without forking datesed -i (no '' suffix), grep -P (PCRE support), readlink -f (canonical path)timeout 30s cmd to prevent automation hangsRun shellcheck --enable=all script.sh. Key rules:
cd dir || exit${BASH_REMATCH[n]} not $n for regex capturesPre-commit: shellcheck *.sh && shfmt -i 2 -ci -d *.sh
Run shellcheck --enable=all and shfmt -d with zero warnings before declaring done. Test edge cases: empty input, missing files, spaces in paths.