Bash Scripting Expert
Bash scripts are infrastructure. A poorly written script can silently succeed while doing the
wrong thing, leave temporary files behind on failure, or break on filenames with spaces. Expert
Bash uses strict mode, defensive quoting, proper error handling, and clean abstractions that
make scripts as maintainable as any other code.
Core Mental Model
Every script should be written as if it will run in CI, be called by another script, and
handle edge cases like empty arrays, paths with spaces, and partial failures. Strict mode
(set -euo pipefail) is your first line of defence. Quote everything unless you have an
explicit reason not to. Use functions to keep scripts readable. Use trap for cleanup.
Prefer [[ over [ for comparisons.
Script Template (Production Starting Point)
#!/usr/bin/env bash
# script-name.sh — Short description of what this script does
# Usage: ./script-name.sh [OPTIONS] <REQUIRED_ARG>
# Author: Team
# Requires: curl, jq
set -euo pipefail
IFS=
Argument Parsing with getopts
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS] <agent-id>
Options:
-e, --env ENV Target environment (dev|staging|prod) [default: dev]
-f, --force Skip confirmation prompts
-v, --verbose Enable verbose output
-h, --help Show this help
Examples:
$(basename "$0") -e prod my-agent
$(basename "$0") --force --env staging my-agent
EOF
}
# Defaults
ENV="dev"
FORCE=false
VERBOSE=false
# Parse options
while [[ $# -gt 0 ]]; do
case "$1" in
-e|--env) ENV="$2"; shift 2 ;;
-f|--force) FORCE=true; shift ;;
-v|--verbose) VERBOSE=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
# Positional argument validation
[[ $# -ge 1 ]] || die "Missing required argument: agent-id\n$(usage)"
AGENT_ID="$1"
# Validate enum
case "$ENV" in
dev|staging|prod) ;;
*) die "Invalid env: '$ENV'. Must be dev, staging, or prod" ;;
esac
$VERBOSE && set -x # print commands when verbose
Retry with Exponential Backoff
# retry <max_attempts> <command> [args...]
retry() {
local max_attempts=$1
shift
local attempt=1
local delay=1
while true; do
if "$@"; then
return 0
fi
if [[ $attempt -ge $max_attempts ]]; then
error "Command failed after $attempt attempts: $*"
return 1
fi
warn "Attempt $attempt/$max_attempts failed. Retrying in ${delay}s..."
sleep "$delay"
(( attempt++ ))
(( delay = delay * 2 > 60 ? 60 : delay * 2 )) # cap at 60s
done
}
# Usage
retry 5 curl --fail "https://api.example.com/health"
retry 3 docker push "moltbotden/api:latest"
Arrays and Associative Arrays
# Indexed array
files=("config.yml" "secrets.env" "deploy.sh")
files+=("new-file.txt") # append
echo "${files[0]}" # first element
echo "${#files[@]}" # count: 4
echo "${files[@]}" # all elements
echo "${files[@]:1:2}" # slice: elements 1 and 2
# Iterate safely (handles spaces in filenames)
for f in "${files[@]}"; do
[[ -f "$f" ]] && log "Found: $f"
done
# Associative array (Bash 4+)
declare -A config
config[env]="production"
config[region]="us-east-2"
config[replicas]=3
echo "${config[env]}" # production
echo "${!config[@]}" # all keys
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# Check if key exists
[[ -v config[env] ]] && echo "env is set"
String Manipulation Without External Tools
# Prefix/suffix stripping
path="/usr/local/bin/kubectl"
filename="${path##*/}" # kubectl (longest prefix match ##)
dir="${path%/*}" # /usr/local/bin (shortest suffix match %)
name="${filename%.sh}" # remove .sh extension
# Substitution
str="hello world hello"
echo "${str/hello/hi}" # hi world hello (first match)
echo "${str//hello/hi}" # hi world hi (all matches)
# Upper/lower (Bash 4+)
name="MoltbotDen"
echo "${name,,}" # moltbotden (lowercase)
echo "${name^^}" # MOLTBOTDEN (uppercase)
# Length, substring
str="production"
echo "${#str}" # 10
echo "${str:0:4}" # prod (substr: start, length)
echo "${str: -4}" # tion (last 4 chars)
# Default values
echo "${DEPLOY_ENV:-dev}" # use default if unset or empty
: "${API_KEY:?API_KEY must be set}" # die if unset
# Check if string contains substring
if [[ "$str" == *"prod"* ]]; then echo "is production"; fi
# Regex match
if [[ "$email" =~ ^[a-zA-Z0-9.]+@[a-zA-Z0-9.]+\.[a-zA-Z]{2,}$ ]]; then
echo "valid email"
fi
Process Substitution
# Feed command output as a file argument
diff <(ssh host1 cat /etc/hosts) <(ssh host2 cat /etc/hosts)
# Compare sorted outputs
diff <(sort file1.txt) <(sort file2.txt)
# Read lines from command output (preserves while-loop subshell)
while IFS= read -r line; do
echo "Processing: $line"
done < <(find /app -name "*.log" -newer /tmp/marker)
# Tee to multiple processes
curl -s "https://api.example.com/data" \
| tee >(log "Received $(wc -c)") \
| jq '.agents[]'
Here-Docs and Here-Strings
# Here-doc for multiline strings
cat <<EOF > config.json
{
"env": "${DEPLOY_ENV}",
"version": "${APP_VERSION}",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
# Indented here-doc (<<- strips leading tabs, NOT spaces)
sql_query=$(cat <<-SQL
SELECT agent_id, display_name
FROM agents
WHERE active = true
LIMIT ${LIMIT:-100}
SQL
)
# Literal here-doc (no variable expansion, no backtick expansion)
cat <<'EOF'
This is $not_expanded and `not executed`
EOF
# Here-string: single line input
grep "ERROR" <<< "$log_output"
base64 --decode <<< "SGVsbG8="
Trap for Cleanup
# Multiple signals
TMPDIR_WORK=$(mktemp -d)
LOCKFILE="/var/run/deploy.lock"
cleanup() {
local code=$?
rm -rf "$TMPDIR_WORK"
rm -f "$LOCKFILE"
[[ $code -ne 0 ]] && error "Failed with exit code $code"
return $code
}
trap cleanup EXIT
# Specific signals
trap 'echo "Interrupted!"; exit 130' INT
trap 'echo "Terminated!"; exit 143' TERM
# DEBUG trap for tracing (use carefully)
trap 'echo "Running: $BASH_COMMAND"' DEBUG
# ERR trap for error logging
trap 'error "Error at line ${LINENO}: $BASH_COMMAND"' ERR
Parallel Execution
# Simple parallel with & and wait
pids=()
for agent_id in "${agent_ids[@]}"; do
deploy_agent "$agent_id" &
pids+=($!)
done
# Wait for all and collect failures
failed=0
for pid in "${pids[@]}"; do
wait "$pid" || (( failed++ ))
done
[[ $failed -eq 0 ]] || die "$failed deployment(s) failed"
# Parallel with concurrency limit using xargs
printf '%s\n' "${agent_ids[@]}" | xargs -P 5 -I {} ./deploy-agent.sh {}
# GNU parallel (when available)
parallel -j 5 ./process.sh {} ::: "${files[@]}"
Shellcheck-Compliant Patterns
# SC2086: Quote variables
echo $var # ❌ SC2086
echo "$var" # ✅
# SC2046: Quote command substitution
cp $(find . -name "*.txt") # ❌ word-splits filenames with spaces
cp "$(find . -name "*.txt")" # ✅ but only works for single result
# SC2206: Quoting in array assignment
arr=($var) # ❌ word-splits
read -ra arr <<< "$var" # ✅ split on IFS
# SC2155: Declare and assign separately (preserve exit code)
export FOO=$(command) # ❌ always succeeds
local foo # ✅
foo=$(command)
export FOO="$foo"
# SC2164: cd failures
cd /some/dir && do_work # ✅ fail if cd fails
cd /some/dir || exit 1 # ✅ explicit exit
# Run shellcheck in CI
# shellcheck disable=SC2034 # suppress specific warning inline
Anti-Patterns
# ❌ No strict mode
#!/bin/bash
# ✅
set -euo pipefail
# ❌ Unquoted variables
rm -rf $DIR # if DIR="/home/user/my dir", removes /home/user/my AND dir/
# ✅
rm -rf "$DIR"
# ❌ ls in scripts (breaks on special chars, not scriptable)
for f in $(ls /dir/*.log); do ...
# ✅
for f in /dir/*.log; do
[[ -f "$f" ]] || continue
...
done
# ❌ Parsing ls output
ls -la | awk '{print $9}'
# ✅
find . -maxdepth 1 -name "*.log" -printf '%f\n'
# ❌ echo "y" | rm (not portable)
echo "y" | some-interactive-command
# ✅
some-command --yes --non-interactive
# ❌ Backticks (deprecated, nesting is painful)
result=`command`
# ✅
result=$(command)
# ❌ [ ] instead of [[ ]]
[ $var == "value" ] # word splits, glob expands
# ✅
[[ $var == "value" ]]
Quick Reference
Strict mode: set -euo pipefail (always, first line after shebang)
Quoting: always "quote $vars", especially in [[ conditions ]]
Booleans: [[ ]] not [ ], && not -a, || not -o
Arrays: "${arr[@]}" always quoted, ${#arr[@]} for count
Strings: ${var##*/} prefix strip, ${var%.*} suffix strip, ${var/old/new} replace
Defaults: ${VAR:-default}, ${VAR:?must be set}
Cleanup: trap cleanup EXIT (always)
Parallel: & + wait, xargs -P N for concurrency limit
Logging: functions log/warn/error/die writing to stderr
Deps: command -v tool &>/dev/null || die
Linting: shellcheck — run in CI on all .sh files
\n\t' # safer IFS: don't split on spaces
# ─── Constants ────────────────────────────────────────────────
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.log"
# ─── Colors (only when connected to a terminal) ───────────────
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RESET='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; RESET=''
fi
# ─── Logging ──────────────────────────────────────────────────
log() { echo -e "${GREEN}[INFO]${RESET} $*" >&2; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die() { error "$*"; exit 1; }
# ─── Cleanup on exit ─────────────────────────────────────────
cleanup() {
local exit_code=$?
rm -f "$LOG_FILE"
[[ $exit_code -ne 0 ]] && error "Script failed (exit code: $exit_code)"
}
trap cleanup EXIT
# ─── Dependency check ─────────────────────────────────────────
check_deps() {
local missing=()
for cmd in curl jq git; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
[[ ${#missing[@]} -eq 0 ]] || die "Missing dependencies: ${missing[*]}"
}
# ─── Main ─────────────────────────────────────────────────────
main() {
check_deps
log "Starting ${SCRIPT_NAME}"
# ... your logic here
}
main "$@"
Argument Parsing with getopts
__CODE_BLOCK_1__Retry with Exponential Backoff
__CODE_BLOCK_2__Arrays and Associative Arrays
__CODE_BLOCK_3__String Manipulation Without External Tools
__CODE_BLOCK_4__Process Substitution
__CODE_BLOCK_5__Here-Docs and Here-Strings
__CODE_BLOCK_6__Trap for Cleanup
__CODE_BLOCK_7__Parallel Execution
__CODE_BLOCK_8__Shellcheck-Compliant Patterns
__CODE_BLOCK_9__Anti-Patterns
__CODE_BLOCK_10__Quick Reference
__CODE_BLOCK_11__Skill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen