shell-dev
SKILL.md
Shell Development
Master skill for defensive Bash programming, BATS testing, and ShellCheck static analysis. Covers production-grade scripting patterns, CLI tool creation, debugging, logging, service management, text processing, HTTP networking, and automated testing.
Quick Reference
| Topic | Reference |
|---|---|
| Defensive patterns | references/defensive.md |
| BATS testing | references/bats-testing.md |
| ShellCheck config | references/shellcheck.md |
| Argument parsing (getopts, getopt, subcommands) | references/argument-parsing.md |
| systemd services and timers | references/systemd-services.md |
| Text processing (awk, sed, pipelines) | references/text-processing.md |
| Production logging (structured, JSON, syslog) | references/logging-production.md |
| HTTP/networking (curl, wget, retry, APIs) | references/http-networking.md |
| Parallel execution (xargs, GNU parallel, pools) | references/parallel-execution.md |
| Advanced debugging (PS4, BASH_XTRACEFD, traps) | references/debugging-advanced.md |
Task Router
| Task | Go-to Reference |
|---|---|
Parse -v, --verbose, long options |
argument-parsing.md → getopts / getopt |
| Build git-style subcommand CLI | argument-parsing.md → subcommand routing |
| Validate CLI arguments (type, range, enum) | argument-parsing.md → validation patterns |
| Create systemd service for a script | systemd-services.md → unit file examples |
| Replace cron with systemd timer | systemd-services.md → timers |
| Handle SIGTERM gracefully | systemd-services.md → graceful shutdown |
| Log to journald from a service | systemd-services.md → journald integration |
| Extract fields from text/CSV/logs | text-processing.md → awk one-liners |
| Replace/transform text with sed | text-processing.md → sed patterns |
| Analyze log files | text-processing.md → log analysis |
| awk associative arrays / frequency count | text-processing.md → awk arrays |
| Structured log_info / log_error with levels | logging-production.md → core library |
| JSON logging for log aggregators | logging-production.md → JSON format |
| Log rotation with SIGHUP | logging-production.md → log rotation |
| Write to syslog from shell | logging-production.md → syslog integration |
| curl API calls with auth and error handling | http-networking.md → api_call_with_backoff |
| Download files with retry and resume | http-networking.md → download_with_retry |
| Check if port is open | http-networking.md → check_port |
| Rate-limit API calls | http-networking.md → rate limiting |
| Parallel file processing with xargs | parallel-execution.md → xargs -P |
| GNU parallel basics and advanced | parallel-execution.md → GNU parallel |
| Bounded process pool | parallel-execution.md → process pool |
| Error propagation from parallel jobs | parallel-execution.md → error propagation |
| Enable set -x with timestamps | debugging-advanced.md → PS4 patterns |
| Route trace output to file | debugging-advanced.md → BASH_XTRACEFD |
| Profile slow commands | debugging-advanced.md → DEBUG trap |
| Print stack trace on error | debugging-advanced.md → ERR trap |
| Interactive breakpoint in script | debugging-advanced.md → debug_break |
| ShellCheck suppression and config | shellcheck.md |
| BATS test setup, mocking, fixtures | bats-testing.md |
| Defensive patterns, traps, atomicity | defensive.md |
Script Template
Every production script starts with this skeleton:
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
trap 'echo "Error on line $LINENO" >&2' ERR
trap 'rm -rf -- "${TMPDIR:-}"' EXIT
TMPDIR=$(mktemp -d)
log_info() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2; }
log_error() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; }
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Options:
-v, --verbose Enable verbose output
-h, --help Show this help
EOF
exit "${1:-0}"
}
Core Defensive Rules
- Always
set -Eeuo pipefail-- strict mode catches errors early - Quote all variables --
"$var"prevents word splitting and globbing - Use
[[ ]]over[ ]-- safer conditionals in Bash - Trap ERR and EXIT -- clean up temp files, log failures
- Validate inputs -- check file existence, required vars with
${VAR:?msg} - Use
command -v-- safer thanwhichfor dependency checks - Use
$()not backticks -- nestable, readable command substitution - Iterate safely --
find -print0 | while read -r -d ''for filenames with spaces - Atomic writes -- write to temp file, then
mvto target - Design for idempotency -- safe to re-run without side effects
Argument Parsing Pattern
VERBOSE=false
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=true; shift ;;
-o|--output) OUTPUT="$2"; shift 2 ;;
-h|--help) usage 0 ;;
--) shift; break ;;
*) echo "ERROR: Unknown option: $1" >&2; usage 1 ;;
esac
done
Safe Patterns Cheat Sheet
| Need | Pattern |
|---|---|
| Required var | ${VAR:?VAR is not set} |
| Default value | ${VAR:-default} |
| Temp directory | TMPDIR=$(mktemp -d) with EXIT trap |
| File iteration | find . -print0 | while IFS= read -r -d '' f |
| Array from cmd | mapfile -t arr < <(command) |
| Process substitution | diff <(cmd1) <(cmd2) |
| Here string | read -r var <<< "$input" |
| Dry-run support | Wrap commands in run_cmd() that checks $DRY_RUN |
| Dependency check | command -v jq &>/dev/null || { echo "missing jq"; exit 1; } |
| Background procs | Track PIDs in array, trap cleanup SIGTERM SIGINT |
Debugging
Use these techniques to trace script execution and diagnose failures:
# Enable trace mode -- prints each command before execution
set -x
# Custom trace prefix showing file, function, and line number
export PS4='+${BASH_SOURCE[0]}:${FUNCNAME[0]:-main}:${LINENO}: '
# Trace specific sections only
set -x
problematic_code_here
set +x
# DEBUG trap -- runs before every command (use for logging/profiling)
trap 'echo "CMD: $BASH_COMMAND" >&2' DEBUG
# Print call stack on error (add to ERR trap)
trap 'echo "Error in ${FUNCNAME[0]:-main} at line $LINENO"; \
for i in "${!FUNCNAME[@]}"; do \
echo " ${FUNCNAME[$i]}() at ${BASH_SOURCE[$i]}:${BASH_LINENO[$i]}"; \
done' ERR
Logging Best Practices
# Structured logging with levels -- always log to stderr
LOG_LEVEL="${LOG_LEVEL:-INFO}"
_log() {
local level="$1"; shift
local -A levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
(( levels[$level] >= levels[${LOG_LEVEL}] )) || return 0
printf '[%s] %s: %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$level" "$*" >&2
}
log_debug() { _log DEBUG "$@"; }
log_info() { _log INFO "$@"; }
log_warn() { _log WARN "$@"; }
log_error() { _log ERROR "$@"; }
# Log to syslog for daemon/service scripts
logger -t "myapp" -p user.info "Service started"
logger -t "myapp" -p user.err "Fatal error: $msg"
BATS Testing Essentials
Test File Structure
#!/usr/bin/env bats
setup() {
TEST_DIR=$(mktemp -d)
source "${BATS_TEST_DIRNAME}/../bin/script.sh"
}
teardown() {
rm -rf "$TEST_DIR"
}
@test "function returns 0 on valid input" {
run my_function "good_input"
[ "$status" -eq 0 ]
}
@test "function fails on missing arg" {
run my_function
[ "$status" -ne 0 ]
[[ "$output" == *"Usage"* ]]
}
Key BATS Patterns
runkeyword captures$status,$output, and${lines[@]}setup_file/teardown_filerun once per file (expensive setup)setup/teardownrun per test (isolation)skipconditionally skip:command -v jq &>/dev/null || skip "jq not installed"- Mocking -- override functions or prepend stub dir to
$PATH - Fixtures -- store in
tests/fixtures/, copy to temp dir in setup
Running Tests
bats tests/*.bats # Run all tests
bats tests/*.bats --tap # TAP output for CI
bats tests/*.bats --parallel 4 # Parallel execution
ShellCheck Essentials
Project Config (.shellcheckrc)
shell=bash
enable=avoid-nullary-conditions,require-variable-braces
disable=SC1091
external-sources=true
Critical Error Codes
| Code | Issue | Fix |
|---|---|---|
| SC2086 | Unquoted variable | Add double quotes |
| SC2181 | Indirect $? check |
Use if command; then |
| SC2015 | && || as if-else |
Use proper if-then-else |
| SC2009 | grep on ps output |
Use pgrep |
| SC1091 | Can't follow source | Add # shellcheck source=path |
Best Practices Summary
- Start every script with strict mode and traps
- Write tests alongside scripts -- BATS makes it easy
- Run ShellCheck in CI and as a pre-commit hook
- Test both success and failure paths
- Use
set -xand customPS4for debugging -- remove before commit - Log to stderr with structured levels; use syslog for services
- Prefer
printfoverechofor portable output - Document ShellCheck suppressions -- explain the "why"
- Keep functions small with
local -rfor immutable locals - Use
find -print0andread -d ''for safe file iteration
Weekly Installs
1
Repository
george11642/geo…-pluginsGitHub Stars
2
First Seen
9 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1