A curated collection of practical shell scripting patterns, tricks, and battle-tested examples
Strict mode, argument handling, and the building blocks.
#!/usr/bin/env bash
# Strict mode — catch errors early
set -euo pipefail
IFS=$'\n\t'
# Script directory (works with symlinks too)
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Defaults
VERBOSE=0
OUTPUT="/tmp/output.log"
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS] <input_file>
Options:
-o, --output FILE Output file (default: $OUTPUT)
-v, --verbose Enable verbose output
-h, --help Show this help
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) OUTPUT="$2"; shift 2 ;;
-v|--verbose) VERBOSE=1; shift ;;
-h|--help) usage ;;
-*) echo "Unknown option: $1" >&2; usage ;;
*) break ;;
esac
done
[[ $# -lt 1 ]] && { echo "Error: input file required" >&2; usage; }
INPUT="$1"
log() { (( VERBOSE )) && printf '[%s] %s\n' "$(date +%T)" "$*" >&2; }
log "Processing $INPUT → $OUTPUT"
set -euo pipefail makes scripts fail fast. -e exits on error, -u catches unset variables, -o pipefail catches failures in piped commands.
Parameter expansion is insanely powerful — skip sed for simple transforms.
file="/var/log/nginx/access.log.gz"
# Extraction
echo "${file##*/}" # access.log.gz (basename)
echo "${file%/*}" # /var/log/nginx (dirname)
echo "${file%%.*}" # /var/log/nginx/access
echo "${file%.gz}" # /var/log/nginx/access.log
# Substring: ${var:offset:length}
hash="a1b2c3d4e5f6"
echo "${hash:0:8}" # a1b2c3d4
# Search & replace
path="/usr/local/bin"
echo "${path//\// > }" # > usr > local > bin
# Case conversion (bash 4+)
name="hello world"
echo "${name^^}" # HELLO WORLD
echo "${name^}" # Hello world
# Default values
echo "${EDITOR:-vim}" # use vim if $EDITOR is unset
echo "${1:?'arg required'}" # exit with error if $1 empty
# Length
msg="hello"
echo "${#msg}" # 5
Arrays make complex data handling possible without reaching for Python.
# Indexed arrays
servers=("web01" "web02" "db01" "cache01")
echo "First: ${servers[0]}"
echo "Count: ${#servers[@]}"
echo "All: ${servers[*]}"
# Append
servers+=("monitor01")
# Slice: ${array[@]:offset:length}
echo "${servers[@]:1:2}" # web02 db01
# Iterate with index
for i in "${!servers[@]}"; do
printf ' [%d] %s\n' "$i" "${servers[$i]}"
done
# Associative arrays (bash 4+)
declare -A ports=(
[nginx]=80
[ssh]=22
[postgres]=5432
[redis]=6379
)
for svc in "${!ports[@]}"; do
printf '%-12s → %s\n' "$svc" "${ports[$svc]}"
done
# Build array from command output
mapfile -t pids < <(pgrep -f nginx)
echo "nginx PIDs: ${pids[*]}"
Traps, background jobs, and graceful signal handling.
# Cleanup trap — runs on EXIT, INT, TERM
TMPDIR=$(mktemp -d)
cleanup() {
rm -rf "$TMPDIR"
echo "Cleaned up $TMPDIR"
}
trap cleanup EXIT
# Parallel execution with wait
for host in web0{1..4}; do
ssh "$host" 'uptime' &
done
wait # blocks until all background jobs finish
# Timeout a command
timeout 5 curl -sS "https://example.com" || echo "timed out"
# Lock file — prevent concurrent runs
LOCKFILE="/var/run/myscript.lock"
acquire_lock() {
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Already running"; exit 1; }
}
acquire_lock
# Process substitution — diff two commands
diff <(ssh web01 'cat /etc/hosts') <(ssh web02 'cat /etc/hosts')
kill -9 as a first resort. Send SIGTERM first, wait, then escalate: kill $PID; sleep 2; kill -0 $PID 2>/dev/null && kill -9 $PID
Safe file iteration, atomic writes, and log parsing.
# Safe line-by-line reading
while IFS= read -r line; do
echo "Processing: $line"
done < "input.txt"
# Read CSV fields
while IFS=',' read -r name ip role; do
printf '%-15s %-15s %s\n' "$name" "$ip" "$role"
done < "servers.csv"
# Atomic write — never leave a half-written file
tmp=$(mktemp)
generate_config > "$tmp"
mv "$tmp" "/etc/app/config.yml"
# Find + exec (safer than piping to xargs)
find /var/log -name '*.log' -mtime +30 -exec gzip {} \;
# Watch a log file and react
tail -F /var/log/auth.log | while read -r line; do
if [[ "$line" == *"Failed password"* ]]; then
ip=$(grep -oP 'from \K[\d.]+' <<< "$line")
echo "[$(date +%T)] Failed login from $ip"
fi
done
HTTP requests, port scanning, and DNS queries — all from bash.
# Health check with retry logic
check_endpoint() {
local url="$1" retries=5 delay=3
for (( i=1; i<=retries; i++ )); do
if curl -sfSo /dev/null -w '%{http_code}' "$url" | grep -q '^2'; then
echo "✓ $url is up"
return 0
fi
echo "Attempt $i/$retries failed, retrying in ${delay}s..."
sleep "$delay"
done
echo "✗ $url is DOWN"
return 1
}
# Quick port check (no netcat needed)
port_open() {
timeout 2 bash -c "echo > /dev/tcp/$1/$2" 2>/dev/null
}
port_open "google.com" 443 && echo "open" || echo "closed"
# Resolve DNS and extract IPs
dig +short "example.com" A | while read -r ip; do
echo " $ip → $(dig +short -x "$ip" | head -1)"
done
# Download with progress + resume
curl -LO -C - --retry 3 "https://releases.example.com/app.tar.gz"
# SSL cert expiry check
cert_expiry() {
echo | openssl s_client -servername "$1" -connect "$1:443" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2
}
echo "Cert expires: $(cert_expiry vk.rtscom.com)"
Reusable patterns for production scripts.
# ── Colored logging ──
declare -A _c=([red]='\e[31m' [green]='\e[32m' [yellow]='\e[33m' [reset]='\e[0m')
log_info() { printf "${_c[green]}[INFO]${_c[reset]} %s\n" "$*"; }
log_warn() { printf "${_c[yellow]}[WARN]${_c[reset]} %s\n" "$*" >&2; }
log_error() { printf "${_c[red]}[ERROR]${_c[reset]} %s\n" "$*" >&2; }
# ── Retry wrapper ──
retry() {
local n=0 max="$1" delay="$2"
shift 2
until "$@"; do
(( ++n >= max )) && { log_error "Failed after $n attempts: $*"; return 1; }
log_warn "Attempt $n failed, retrying in ${delay}s..."
sleep "$delay"
done
}
retry 3 5 curl -sf "https://api.example.com/health"
# ── Config file parser ──
parse_config() {
local file="$1"
while IFS='=' read -r key value; do
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
printf -v "CFG_${key^^}" '%s' "${value## }"
done < "$file"
}
# After: parse_config app.conf
# Access: echo "$CFG_DATABASE_HOST"
# ── Spinner for long operations ──
spinner() {
local pid="$1" chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
while kill -0 "$pid" 2>/dev/null; do
for (( i=0; i<${#chars}; i++ )); do
printf '\r %s Working...' "${chars:$i:1}"
sleep 0.1
done
done
printf '\r ✓ Done! \n'
}
# Usage: long_task & spinner $!
Copy-paste ready commands for daily sysadmin work.
# Top 10 largest files in current directory tree
find . -type f -exec du -h {} + | sort -rh | head -10
# Kill all processes matching a pattern
pkill -f 'pattern' # SIGTERM
pkill -9 -f 'pattern' # SIGKILL (last resort)
# Monitor disk usage changes in real time
watch -n5 'df -h | grep -E "/$|/var|/home"'
# List open ports with process names
ss -tlnp
# Show all unique IPs hitting nginx today
awk '{print $1}' /var/log/nginx/access.log | sort -u | wc -l
# Generate a random 32-char password
openssl rand -base64 24
# Quick HTTP server from current directory
python3 -m http.server 8080
# Replace string in all files recursively
grep -rl 'old_string' . | xargs sed -i 's/old_string/new_string/g'
# Compare two directories
diff <(cd dir1 && find . | sort) <(cd dir2 && find . | sort)
# Backup with timestamp
tar czf "backup-$(date +%F_%H%M).tar.gz" /etc/nginx/
Test operators, redirections, and special variables.
| Expression | Description |
|---|---|
| [[ -f $f ]] | True if file exists and is regular |
| [[ -d $d ]] | True if directory exists |
| [[ -z $s ]] | True if string is empty |
| [[ -n $s ]] | True if string is not empty |
| [[ $a == $b ]] | String equality |
| [[ $a =~ regex ]] | Regex match |
| [[ $n -gt 0 ]] | Numeric comparison (also: -lt, -eq, -ne, -ge, -le) |
| Variable | Meaning |
|---|---|
| $? | Exit status of last command |
| $$ | PID of current shell |
| $! | PID of last background process |
| $# | Number of positional parameters |
| $@ | All parameters (individually quoted) |
| $0 | Script name |
| ${PIPESTATUS[@]} | Exit codes of all commands in last pipeline |
| Redirect | Meaning |
|---|---|
| cmd > file | Stdout to file (overwrite) |
| cmd >> file | Stdout to file (append) |
| cmd 2>&1 | Stderr to stdout |
| cmd &> file | Both stdout+stderr to file |
| cmd1 | cmd2 | Pipe stdout to next command |
| cmd1 |& cmd2 | Pipe stdout+stderr |
| cmd < <(cmd2) | Process substitution |