Skill: gitgoodordietrying/shell-scripting (version 1.0.0)

--- name: shell-scripting description: Write robust, portable shell scripts. Use when parsing arguments, handling errors properly, writing POSIX-compatible scripts, managing temp files, running commands in parallel, managing background processes, or adding --help to scripts. metadata: {"clawdbot":{"emoji":"🐚","requires":{"bins":["bash"]},"os":["linux","darwin","win32"]}} --- # Shell Scripting Write reliable, maintainable bash scripts. Covers argument parsing, error handling, portability, temp files, parallel execution, process management, and self-documenting scripts. ## When to Use - Writing scripts that others (or future you) will run - Automating multi-step workflows - Parsing command-line arguments with flags and options - Handling errors and cleanup properly - Running tasks in parallel - Making scripts portable across Linux and macOS - Wrapping complex commands with a simpler interface ## Script Template ```bash #!/usr/bin/env bash set -euo pipefail # Description: What this script does (one line) # Usage: script.sh [options] <required-arg> readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_NAME="$(basename "$0")" # Defaults VERBOSE=false OUTPUT_DIR="./output" usage() { cat <<EOF Usage: $SCRIPT_NAME [options] <input-file> Description: Process the input file and generate output. Options: -o, --output DIR Output directory (default: $OUTPUT_DIR) -v, --verbose Enable verbose output -h, --help Show this help message Examples: $SCRIPT_NAME data.csv $SCRIPT_NAME -v -o /tmp/results data.csv EOF } log() { echo "[$(date '+%H:%M:%S')] $*" >&2; } debug() { $VERBOSE && log "DEBUG: $*" || true; } die() { log "ERROR: $*"; exit 1; } # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in -o|--output) OUTPUT_DIR="$2"; shift 2 ;; -v|--verbose) VERBOSE=true; shift ;; -h|--help) usage; exit 0 ;; --) shift; break ;; -*) die "Unknown option: $1" ;; *) break ;; esac done INPUT_FILE="${1:?$(usage >&2; echo "Error: input file required")}" [[ -f "$INPUT_FILE" ]] || die "File not found: $INPUT_FILE" # Main logic main() { debug "Input: $INPUT_FILE" debug "Output: $OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" log "Processing $INPUT_FILE..." # ... do work ... log "Done. Output in $OUTPUT_DIR" } main "$@" ``` ## Error Handling ### set flags ```bash set -e # Exit on any command failure set -u # Error on undefined variables set -o pipefail # Pipe fails if any command in the pipe fails set -x # Debug: print each command before executing (noisy) # Combined (use this in every script) set -euo pipefail # Temporarily disable for commands that are allowed to fail set +e some_command_that_might_fail exit_code=$? set -e ``` ### Trap for cleanup ```bash # Cleanup on exit (any exit: success, failure, or signal) TMPDIR="" cleanup() { [[ -n "$TMPDIR" ]] && rm -rf "$TMPDIR" } trap cleanup EXIT TMPDIR=$(mktemp -d) # Use $TMPDIR freely — it's cleaned up automatically # Trap specific signals trap 'echo "Interrupted"; exit 130' INT # Ctrl+C trap 'echo "Terminated"; exit 143' TERM # kill ``` ### Error handling patterns ```bash # Check command exists before using it command -v jq >/dev/null 2>&1 || die "jq is required but not installed" # Provide default values NAME="${NAME:-default_value}" # Required variable (fail if unset) : "${[REDACTED_SECRET] API_KEY environment variable is required}" # Retry a command retry() { local max_attempts=$1 shift local attempt=1 while [[ $attempt -le $max_attempts ]]; do "$@" && return 0 log "Attempt $attempt/$max_attempts failed. Retrying..." ((attempt++)) sleep $((attempt * 2)) done die "Command failed after $max_attempts attempts: $*" } retry 3 curl -sf https://api.example.com/health ``` ## Argument Parsing ### Simple: positional + flags ```bash # Manual parsing (no dependencies) FORCE=false DRY_RUN=false while [[ $# -gt 0 ]]; do case "$1" in -f|--force) FORCE=true; shift ;; -n|--dry-run) DRY_RUN=true; shift ;; -o|--output) [[ -n "${2:-}" ]] || die "--output requires a value" OUTPUT="$2"; shift 2 ;; --output=*) OUTPUT="${1#*=}"; shift ;; -h|--help) usage; exit 0 ;; --) shift; break ;; # End of options -*) die "Unknown option: $1" ;; *) break ;; # Start of positional args esac done # Remaining args are positional FILES=("$@") [[ ${#FILES[@]} -gt 0 ]] || die "At least one file is required" ``` ### getopts (POSIX, short options only) ```bash while getopts ":o:vhf" opt; do case "$opt" in o) OUTPUT="$OPTARG" ;; v) VERBOSE=true ;; f) FORCE=true ;; h) usage; exit 0 ;; :) die "Option -$OPTARG requires an argument" ;; ?) die "Unknown option: -$OPTARG" ;; esac done shift $((OPTIND - 1)) ``` ## Temp Files and Directories ```bash # Create temp file (automatically unique) TMPFILE=$(mktemp) echo "data" > "$TMPFILE" # Create temp directory TMPDIR=$(mktemp -d) # Create temp with custom prefix/suffix TMPFILE=$(mktemp /tmp/myapp.XXXXXX) TMPFILE=$(mktemp --suffix=.json) # GNU only # Always clean up with trap trap 'rm -f "$TMPFILE"' EXIT # Portable pattern (works on macOS and Linux) TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'myapp') trap 'rm -rf "$TMPDIR"' EXIT ``` ## Parallel Execution ### xargs -P ```bash # Run 4 commands in parallel cat urls.txt | xargs -P 4 -I {} curl -sO {} # Process files in parallel (4 at a time) find . -name "*.csv" | xargs -P 4 -I {} ./process.sh {} # Parallel with progress indicator find . -name "*.jpg" | xargs -P 8 -I {} sh -c 'convert {} -resize 800x600 resized/{} && echo "Done: {}"' ``` ### Background jobs + wait ```bash # Run tasks in background, wait for all pids=() for file in data/*.csv; do process_file "$file" & pids+=($!) done # Wait for all and check results failed=0 for pid in "${pids[@]}"; do wait "$pid" || ((failed++)) done [[ $failed -eq 0 ]] || die "$failed jobs failed" ``` ### GNU Parallel (if available) ```bash # Process files with 8 parallel jobs parallel -j 8 ./process.sh {} ::: data/*.csv # With progress bar parallel --bar -j 4 convert {} -resize 800x600 resized/{/} ::: *.jpg # Pipe input lines cat urls.txt | parallel -j 10 curl -sO {} ``` ## Process Management ### Background processes ```bash # Start in background long_running_command & BG_PID=$! # Check if still running kill -0 $BG_PID 2>/dev/null && echo "Running" || echo "Stopped" # Wait for it wait $BG_PID echo "Exit code: $?" # Kill on script exit trap 'kill $BG_PID 2>/dev/null' EXIT ``` ### Process supervision ```bash # Run a command, restart if it dies run_with_restart() { local cmd=("$@") while true; do "${cmd[@]}" & local pid=$! log "Started PID $pid" wait $pid local exit_code=$? log "Process exited with code $exit_code. Restarting in 5s..." sleep 5 done } run_with_restart ./my-server --port 8080 ``` ### Timeout ```bash # Kill command after 30 seconds timeout 30 long_running_command # With custom signal (SIGKILL after SIGTERM fails) timeout --signal=TERM --kill-after=10 30 long_running_command # Portable (no timeout command) ( sleep 30; kill $$ 2>/dev/null ) & TIMER_PID=$! long_running_command kill $TIMER_PID 2>/dev/null ``` ## Portability (Linux vs macOS) ### Common differences ```bash # sed: macOS requires -i '' (empty backup extension) # Linux: sed -i 's/old/new/g' file.txt # macOS: sed -i '' 's/old/new/g' file.txt # Portable: sed -i.bak 's/old/new/g' file.txt && rm file.txt.bak # date: different flags # GNU (Linux): date -d '2026-02-03' '+%s' # BSD (macOS): date -j -f '%Y-%m-%d' '2026-02-03' '+%s' # readlink -f: doesn't exist on macOS # Portable alternative: real_path() { cd "$(dirname "$1")" && echo "$(pwd)/$(basename "$1")"; } # stat: different syntax # GNU: stat -c '%s' file # BSD: stat -f '%z' file # grep -P: not available on macOS by default # Use grep -E instead, or install GNU grep ``` ### POSIX-safe patterns ```bash # Use printf instead of echo -e (echo behavior varies) printf "Line 1\nLine 2\n" # Use $() instead of backticks result=$(command) # Good result=`command` # Bad (deprecated, nesting issues) # Use [[ ]] for tests (bash), [ ] for POSIX sh [[ -f "$file" ]] # Bash (safer, no word splitting) [ -f "$file" ] # POSIX sh # Array check (bash only, not POSIX) if [[ ${#array[@]} -gt 0 ]]; then echo "Array has elements" fi ``` ## Config File Parsing ### Source a config file ```bash # Simple: source a key=value file # config.env: # DB_HOST=localhost # DB_PORT=5432 # Validate before sourcing (security: check for commands) if grep -qP '^[A-Z_]+=.*[;\`\$\(]' config.env; then die "Config file contains unsafe characters" fi source config.env ``` ### Parse INI-style config ```bash # config.ini: # [database] # host = localhost # port = 5432 # [app] # debug = true parse_ini() { local file="$1" section="" while IFS='= ' read -r key value; do [[ -z "$key" || "$key" =~ ^[#\;] ]] && continue if [[ "$key" =~ ^\[(.+)\]$ ]]; then section="${BASH_REMATCH[1]}" continue fi value="${value%%#*}" # Strip inline comments value="${value%"${value##*[![:space:]]}"}" # Trim trailing whitespace printf -v "${section}_${key}" '%s' "$value" done < "$file" } parse_ini config.ini echo "$database_host" # localhost echo "$app_debug" # true ``` ## Useful Patterns ### Confirm before destructive action ```bash confirm() { local prompt="${1:-Are you sure?}" read -rp "$prompt [y/N] " response [[ "$response" =~ ^[Yy]$ ]] } confirm "Delete all files in /tmp/data?" || die "Aborted" rm -rf /tmp/data/* ``` ### Progress indicator ```bash # Simple counter total=$(wc -l < file_list.txt) count=0 while IFS= read -r file; do ((count++)) printf "\rProcessing %d/%d..." "$count" "$total" >&2 process "$file" done < file_list.txt echo "" >&2 ``` ### Lock file (prevent concurrent runs) ```bash LOCKFILE="/tmp/${SCRIPT_NAME}.lock" acquire_lock() { if ! mkdir "$LOCKFILE" 2>/dev/null; then die "Another instance is running (lock: $LOCKFILE)" fi trap 'rm -rf "$LOCKFILE"' EXIT } acquire_lock # ... safe to proceed, only one instance runs ... ``` ### Stdin or file argument ```bash # Read from file argument or stdin input="${1:--}" # Default to "-" (stdin) if [[ "$input" == "-" ]]; then cat else cat "$input" fi | while IFS= read -r line; do process "$line" done ``` ## Tips - Always start with `set -euo pipefail`. It catches 80% of silent bugs. - Always use `trap cleanup EXIT` for temp files. Never rely on reaching the cleanup code at the end. - Quote all variable expansions: `"$var"` not `$var`. Unquoted variables break on spaces and globs. - Use `[[ ]]` instead of `[ ]` in bash. It handles empty strings, spaces, and pattern matching better. - `shellcheck` is the best linter for shell scripts. Run it: `shellcheck myscript.sh`. Install it if available. - `readonly` for constants prevents accidental overwrite: `readonly DB_HOST="localhost"`. - Write a `usage()` function and call it on `-h`/`--help` and on missing required arguments. Future users (including you) will thank you. - Prefer `printf` over `echo` for anything that might contain special characters or needs formatting. - Test scripts with `bash -n script.sh` (syntax check) before running.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)