Linux bash scripting
Curated collection of agent skills for AI coding assistants.
npx -y skills add iliaal/ai-skills --skill linux-bash-scriptingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
6.8 KB, as published. Nobody here has run it
Linux Bash Scripting
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.
Script Foundation
#!/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
-Epropagates ERR traps into functionsinherit_errexitpropagates errexit into$()command substitutions- Always create temp dirs under the EXIT trap:
_tmpdir=$(mktemp -d) - Wrap body in
main() { ... }with source guard:[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"-- enables sourcing for testing
Core Rules
- Quote every expansion:
"$var","$(cmd)","${array[@]}" localfor function variables,local -rfor function constants,readonlyfor script constantsprintf '%s\n'overecho-- predictable behavior, no flag interpretation[[ ]]for conditionals;(( ))for arithmetic;$()over backticks- End options with
--:rm -rf -- "$path",grep -- "$pattern" "$file" - Require env vars:
: "${VAR:?must be set}" - Never
evaluser input; build commands as arrays:cmd=("grep" "--" "$pat" "$f"); "${cmd[@]}" - Keep untrusted/derived bytes off the command line: never build a heredoc body or an
sh -cstring from external data. An unquoted<<EOFcommand-substitutes$(...)/backticks in the content, and even a quoted<<'EOF'breaks if a content line equals the delimiter (the heredoc ends early and the rest runs as shell). Write the data to a file with a non-shell writer and have the consumer read the file - Allowlisting a command? Match the whole command against an anchored pattern (
^…$), never inspect individual arguments — shell operators (;,&&,|,#, newline) smuggle a second command past a per-argument check (rm -rf node_modules; rm -rf /). Unrecognized syntax must fail closed to deny/ask - Separate
localfrom assignment to preserve exit codes:local val; val=$(cmd) - Debug tracing:
PS4='+${BASH_SOURCE[0]}:${LINENO}: 'withbash -x-- shows file:line per command - Named exit codes:
readonly EX_USAGE=64 EX_CONFIG=78-- no magic numbers inexit - Pipeline diagnostics:
"${PIPESTATUS[@]}"shows exit code of each pipe stage, not just last failure
Safe Iteration
# 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
Argument Parsing
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
Production Patterns
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 077for scripts creating sensitive files- Signal cleanup:
trap 'cleanup; exit 130' INT TERM-- preserves correct exit codes for callers
Logging
log() { printf '[%s] [%s] %s\n' "$(date -Iseconds)" "$1" "${*:2}" >&2; }
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
die() { error "$@"; exit 1; }
Anti-Patterns
| 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 |
x=$(cmd) then an [[ -z $x ]] fallback check | x=$(cmd) || true -- under set -e a failed $() in a bare assignment aborts the script there, so the fallback never runs (opposite of the local case: local masks the failure, a bare assignment propagates it) |
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 |
Performance
- Parameter expansion over externals:
${path%/*}notdirname,${path##*/}notbasename,${var//old/new}notsed (( ))overexpr;[[ =~ ]]overecho | grep- Cache results:
val=$(cmd)once, reuse$val xargs -0 -P "$(nproc)"for parallel workdeclare -A mapfor lookups instead of repeated grep
Bash 4.4+ / 5.x
${var@Q}shell-quoted,${var@U}uppercase,${var@L}lowercasedeclare -n ref=varnamenameref for indirect accesswait -nwait for any background job$EPOCHSECONDS,$EPOCHREALTIME-- timestamps without forkingdate
Linux-Specific
- GNU coreutils differ from macOS:
sed -i(no''suffix),grep -P(PCRE support),readlink -f(canonical path) timeout 30s cmdto prevent automation hangs
ShellCheck
Run shellcheck --enable=all script.sh. Key rules:
- SC2155: Separate declaration from assignment
- SC2086: Double-quote variables
- SC2046: Quote command substitutions
- SC2164:
cd dir || exit - SC2327/SC2328: Use
${BASH_REMATCH[n]}not$nfor regex captures
Pre-commit: shellcheck *.sh && shfmt -i 2 -ci -d *.sh
Verify
Run shellcheck --enable=all and shfmt -d with zero warnings before declaring done. Test edge cases: empty input, missing files, spaces in paths.