shell-scriptingWrite 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.
Install via ClawdBot CLI:
clawdbot install gitgoodordietrying/shell-scriptingRequires:
Write reliable, maintainable bash scripts. Covers argument parsing, error handling, portability, temp files, parallel execution, process management, and self-documenting scripts.
#!/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 "$@"
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
# 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
# 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)
: "${API_KEY:?Error: 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
# 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"
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))
# 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
# 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: {}"'
# 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"
# 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 {}
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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/*
# 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
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 ...
# 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
set -euo pipefail. It catches 80% of silent bugs.trap cleanup EXIT for temp files. Never rely on reaching the cleanup code at the end."$var" not $var. Unquoted variables break on spaces and globs.[[ ]] 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".usage() function and call it on -h/--help and on missing required arguments. Future users (including you) will thank you.printf over echo for anything that might contain special characters or needs formatting.bash -n script.sh (syntax check) before running.Generated Feb 28, 2026
A financial services firm needs to process daily transaction logs from multiple sources, validate formats, and generate summary reports. This script would handle argument parsing for input directories, error checking for missing files, and parallel execution to speed up processing across thousands of files.
A tech startup uses shell scripts to automate server deployments, including parsing command-line flags for environment selection (e.g., staging vs. production), managing temporary files for build artifacts, and handling errors to ensure rollback on failure. It ensures portability across Linux and macOS development machines.
A media production company needs to convert large batches of video or image files, such as resizing images or transcoding videos. The script would use parallel execution with xargs to speed up conversions, manage temp directories for intermediate files, and include a --help option for team members.
An e-commerce platform uses shell scripts to parse server logs, extract error patterns, and send alerts via email or Slack. It includes robust error handling to avoid crashes, argument parsing for log file paths, and background process management for continuous monitoring.
A small business automates daily backups of critical data to cloud storage or local servers. The script handles command-line options for backup sources and destinations, uses temp files for incremental checks, and includes traps for cleanup to ensure no leftover files after interruptions.
Offer shell scripting as part of a larger SaaS platform for automation, where users can customize scripts via a web interface. Revenue comes from subscription tiers based on usage limits and advanced features like parallel execution or error monitoring.
Provide consulting services to businesses needing tailored shell scripts for specific workflows, such as data processing or deployment automation. Revenue is generated through project-based fees or hourly rates for development and maintenance.
Distribute shell scripting templates and libraries as open source to build a community, then monetize by offering premium support, training, or enterprise-grade enhancements. Revenue streams include support contracts and custom feature development.
💬 Integration Tip
Integrate this skill by embedding shell scripts into CI/CD pipelines or using them as lightweight wrappers for existing tools, ensuring they include proper error handling and argument parsing for reliability.
Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output.
Command-line tool to manage Google Workspace services including Gmail, Calendar, Drive, Sheets, Docs, Slides, Contacts, Tasks, People, Groups, and Keep.
Runs shell commands inside a dedicated tmux session named claw, captures, and returns the output, with safety checks for destructive commands.
A modern text-based browser. Renders web pages in the terminal using headless Firefox.
NotebookLM CLI wrapper via `node {baseDir}/scripts/notebooklm.mjs`. Use for auth, notebooks, chat, sources, notes, sharing, research, and artifact generation/download.
Command-line JSON processor. Extract, filter, transform JSON.