Bash Cheat Sheet
Practical snippets for writing robust shell scripts and maintaining a productive Bash environment.
Versions: Bash 4.4+.
declare -A(associative arrays) and some string ops require Bash 4+; macOS ships Bash 3.2 by default - install via Homebrew (brew install bash).bats-coretesting examples use bats-core 1.10+.Last reviewed: May 2026
Script Boilerplate
Strict mode ✅ Use at the top of every script
Always start scripts with this. set -e exits on error, set -u treats unset variables as errors, set -o pipefail propagates failures through pipelines, and set -x prints each command before running it (useful for debugging - remove for production).
#!/usr/bin/env bash
set -euo pipefail
# set -x # uncomment for debug tracingRun as root, re-exec with sudo if needed
#!/usr/bin/env bash
[[ "$(whoami)" == root ]] || { sudo "$0" "$@"; exit $?; }Colour Output Helpers
Drop these into your script or .bashrc for consistent coloured output.
print_success() {
local lightcyan='\033[1;36m' nocolor='\033[0m'
echo -e "${lightcyan}$1${nocolor}"
}
print_error() {
local lightred='\033[1;31m' nocolor='\033[0m'
echo -e "${lightred}$1${nocolor}"
}
print_alert() {
local yellow='\033[1;33m' nocolor='\033[0m'
echo -e "${yellow}$1${nocolor}"
}Logging
Bash has no built-in logger - everything is echo plus convention. The minimum bar in any script you’d run in CI or in production: levels, timestamps, output to stderr for anything that isn’t structured data on stdout, and a single env var to control verbosity.
Leveled log functions
The plain baseline - levels, weights, UTC timestamps, everything to stderr. No colour, no terminal detection, nothing to go wrong when the output is captured or shipped.
#!/usr/bin/env bash
# Source this once at the top of your script.
# LOG_LEVEL: DEBUG < INFO < WARN < ERROR (default INFO)
LOG_LEVEL="${LOG_LEVEL:-INFO}"
# Map level names to numeric weights for comparison.
declare -A _LOG_WEIGHTS=( [DEBUG]=10 [INFO]=20 [WARN]=30 [ERROR]=40 )
_log() {
local level="$1"; shift
local msg="$*"
local want="${_LOG_WEIGHTS[$LOG_LEVEL]:-20}"
local have="${_LOG_WEIGHTS[$level]:-20}"
(( have < want )) && return 0
# ISO-8601 UTC timestamp, level-padded, write to stderr so stdout stays for data.
printf '%s %-5s %s\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$level" "$msg" >&2
}
log_debug() { _log DEBUG "$@"; }
log_info() { _log INFO "$@"; }
log_warn() { _log WARN "$@"; }
log_error() { _log ERROR "$@"; }
# Usage
log_info "Starting deploy to $env"
log_warn "Soft-delete disabled on vault $vault_name"
log_error "Apply failed with exit code $rc"
LOG_LEVEL=DEBUG ./deploy.sh # turn on verbose loggingAdding colour (like PowerShell’s streams)
PowerShell colours its Write-Warning / Write-Error / Write-Verbose streams by default - and it colours the whole line, not just a label. We do the same: the severity colour runs across the level and the message, leaving the timestamp neutral so it stays easy to scan. The trick is to emit ANSI only when it’ll be rendered: drop a colour map and a guard in alongside the weights, then wrap level-through-message in the printf.
# ...after the _LOG_WEIGHTS line, add a colour map and a guard:
declare -A _LOG_COLOURS=(
[DEBUG]=$'\033[2;37m' # dim grey
[INFO]=$'\033[1;36m' # cyan
[WARN]=$'\033[1;33m' # yellow
[ERROR]=$'\033[1;31m' # red
)
_LOG_RESET=$'\033[0m'
# Priority: NO_COLOR always wins; otherwise FORCE_COLOR / CLICOLOR_FORCE let you
# opt colour back in for CI log viewers that render ANSI but aren't real TTYs;
# otherwise fall back to "colour only when stderr is a terminal".
if [[ -n ${NO_COLOR:-} ]]; then
_LOG_COLOURS=(); _LOG_RESET='' # https://no-color.org
elif [[ -z ${FORCE_COLOR:-} && -z ${CLICOLOR_FORCE:-} && ! -t 2 ]]; then
_LOG_COLOURS=(); _LOG_RESET='' # not a TTY, not forced
fi
# ...then swap the printf in _log so colour spans the level AND the message
# (timestamp stays neutral), closing with the reset at the end of the line:
printf '%s %s%-5s %s%s\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"${_LOG_COLOURS[$level]:-}" "$level" "$msg" "$_LOG_RESET" >&2NO_COLOR is checked with -n (non-empty) so a bare NO_COLOR= doesn’t accidentally disable colour - that’s what the spec asks for. Because the colour map is emptied (not unset), ${_LOG_COLOURS[$level]:-} still expands cleanly under set -u.
Why stderr?
Anything diagnostic goes to stderr (>&2); stdout is reserved for data the caller wants to consume. This keeps result=$(my_script.sh) clean even when the script logs heavily, and CI runners still surface both streams in their UI.
# Right - log to stderr, return JSON on stdout
get_vault_secret() {
log_info "Fetching secret $1"
az keyvault secret show --vault-name "$kv" --name "$1" --query value -o tsv
}
secret=$(get_vault_secret "db-password") # secret captured, logs printed liveStructured (JSON) logging for log shippers
If a log shipper (Fluent Bit, Vector, Loki agent) is parsing your output, emit JSON lines instead of free text. One line per event keeps it grep-friendly and machine-parseable.
log_json() {
local level="$1"; shift
local msg="$*"
# Use jq to escape values correctly - never string-concat into JSON.
jq -cn \
--arg ts "$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ')" \
--arg level "$level" \
--arg msg "$msg" \
--arg host "$(hostname)" \
--arg pid "$$" \
'{ts: $ts, level: $level, host: $host, pid: ($pid|tonumber), msg: $msg}' >&2
}
log_json INFO "deploy started"
log_json ERROR "apply failed: $rc"Add context fields with extra --arg pairs and a richer jq template - e.g. correlation_id, env, module. Stick to one event per line.
trap for error context
Capture line number and command on unhandled errors so you don’t have to guess where the script died.
#!/usr/bin/env bash
set -Eeuo pipefail
on_err() {
local exit_code=$?
log_error "command '$BASH_COMMAND' failed at line $1 (exit=$exit_code)"
exit "$exit_code"
}
trap 'on_err $LINENO' ERRset -E is what makes ERR traps inherit into functions and subshells - skip it and the trap won’t fire from inside helpers.
Sensible defaults checklist
set -Eeuo pipefailat the top of every script.- All logging goes to
>&2; stdout is reserved for data. - Timestamp every line in UTC (
date -u +'%Y-%m-%dT%H:%M:%SZ'). LOG_LEVELenv var to flip verbose mode without code changes.- Colour only when it’ll render:
NO_COLORoff, and either a TTY ([[ -t 2 ]]) orFORCE_COLOR/CLICOLOR_FORCEset; never emit raw ANSI to files or log shippers. - One JSON object per line if anything will parse the output.
- An
ERRtrap that prints the failed command + line number.
String Utilities
title_case_convert() {
sed 's/.*/\L&/; s/[a-z]*/\u&/g' <<<"$1"
}
upper_case_convert() {
sed -e 's/\(.*\)/\U\1/' <<< "$1"
}
lower_case_convert() {
sed -e 's/\(.*\)/\L\1/' <<< "$1"
}Argument Parsing
Positional arguments with defaults
#!/usr/bin/env bash
full_name="${1:-Craig Thacker}"
email_address="${2:-user@example.com}"
echo "Name: $full_name"
echo "Email: $email_address"Named flags with while / case
#!/usr/bin/env bash
name=""
env="dev"
debug=false
while [[ $# -gt 0 ]]; do
case $1 in
--name) name="$2"; shift 2 ;;
--env) env="$2"; shift 2 ;;
--debug) debug=true; shift ;;
*) echo "Unknown flag: $1"; exit 1 ;;
esac
done
echo "name=$name env=$env debug=$debug"PATH Management
Remove duplicate entries from PATH
Keeps the first occurrence of each path and discards any subsequent duplicates.
PATH=$(printf %s "$PATH" | awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0)}')
export PATHDirectory Iteration
Run a script in every subdirectory (1 level deep)
#!/usr/bin/env bash
set -euo pipefail
START=$(pwd)
while IFS= read -r dir; do
cd "$dir"
./my-script.sh || echo "Script not in $dir, skipping"
cd "$START"
done < <(find . -maxdepth 1 -mindepth 1 -type d)Sort and format all Terraform modules in a directory
Runs terraform-sort.sh (from the Terraform Standards) across every terraform-<provider>-* module directory. Git releases are handled per-module in CI, not in bulk scripts.
#!/usr/bin/env bash
set -euo pipefail
back=$(pwd)
provider="${1:-azurerm}"
location="${2:-.}"
sort_script="${TERRAFORM_SORT_SCRIPT:-./terraform-sort.sh}"
workspace=$(find "${location}" -maxdepth 1 -name "terraform-${provider}-*" -type d | sort)
for dir in ${workspace}; do
cd "${dir}"
echo "Processing: ${dir}"
if [[ -x "$sort_script" ]]; then
"$sort_script" --sort-variables --sort-outputs --format-terraform --generate-readme
else
# Fallback: fmt only when sort script not present
terraform fmt -recursive
fi
cd "${back}"
doneTerraform Module Sort Helper 🏠 Personal workflow helper
Appends a reusable tfsort function to .bashrc that sorts variable and output blocks, runs terraform fmt, and regenerates README.md via terraform-docs. Git operations are handled separately - see Terraform Standards - Release workflow.
#!/usr/bin/env bash
tfsort() {
# Requires: terraform, terraform-docs, gawk
# Usage: tfsort [--readme-header-file HEADER.md]
local readme_header_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--readme-header-file) readme_header_file="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; return 1 ;;
esac
done
# Verify required tools
for tool in terraform terraform-docs gawk; do
command -v "$tool" &>/dev/null || { echo "ERROR: $tool not found in PATH" >&2; return 1; }
done
[[ -f main.tf || -f build.tf ]] \
|| { echo "ERROR: no main.tf or build.tf found - not a module root" >&2; return 1; }
echo ">> terraform fmt"
terraform fmt -recursive
# Sort variable blocks
if [[ -f variables.tf ]]; then
echo ">> sorting variables.tf"
_tf_sort_blocks variables.tf variable
fi
# Sort output blocks
if [[ -f outputs.tf ]]; then
echo ">> sorting outputs.tf"
_tf_sort_blocks outputs.tf output
fi
# Write README header if provided, then inject terraform-docs
if [[ -n "$readme_header_file" ]]; then
[[ -f "$readme_header_file" ]] \
|| { echo "ERROR: header file not found: $readme_header_file" >&2; return 1; }
cat "$readme_header_file" > README.md
printf '\n\n<!-- BEGIN_TF_DOCS -->\n<!-- END_TF_DOCS -->\n' >> README.md
fi
if [[ -f .terraform-docs.yml ]]; then
echo ">> terraform-docs . (using .terraform-docs.yml)"
terraform-docs .
else
[[ -f README.md ]] \
|| printf '<!-- BEGIN_TF_DOCS -->\n<!-- END_TF_DOCS -->\n' > README.md
echo ">> terraform-docs markdown table --output-mode inject"
terraform-docs markdown table --output-file README.md --output-mode inject .
fi
echo "Done."
}
# Helper: sort variable or output blocks in a file using gawk brace-depth counting
_tf_sort_blocks() {
local file="$1" keyword="$2" tmp_dir count=0
tmp_dir="$(mktemp -d)"
gawk -v kw="$keyword" -v tmpdir="$tmp_dir" '
BEGIN { in_block=0; depth=0; block=""; name="" }
!in_block && match($0, "^" kw " \"([^\"]+)\" \\{", arr) {
name=arr[1]; in_block=1; depth=0; block=$0"\n"
for(i=1;i<=length($0);i++){c=substr($0,i,1);if(c=="{")depth++;else if(c=="}")depth--}
if(depth==0){sub(/\n$/,"",block);print block>(tmpdir"/"name".tf");close(tmpdir"/"name".tf");in_block=0;block="";name=""}
next
}
in_block {
block=block $0"\n"
for(i=1;i<=length($0);i++){c=substr($0,i,1);if(c=="{")depth++;else if(c=="}")depth--}
if(depth==0){sub(/\n$/,"",block);print block>(tmpdir"/"name".tf");close(tmpdir"/"name".tf");in_block=0;block="";name=""}
}
' "$file"
local output="" first=true
while IFS= read -r -d $'\0' bf; do
$first && output="$(cat "$bf")" && first=false \
|| output="$output"$'\n\n'"$(cat "$bf")"
count=$((count + 1))
done < <(find "$tmp_dir" -name '*.tf' -print0 | sort -z)
rm -rf "$tmp_dir"
[[ $count -gt 0 ]] && printf '%s\n' "$output" > "$file" \
|| echo "WARNING: no $keyword blocks found in $file"
}
# Append both functions to .bashrc
echo "" >> ~/.bashrc
echo "# Terraform sort helper" >> ~/.bashrc
declare -f _tf_sort_blocks >> ~/.bashrc
declare -f tfsort >> ~/.bashrc
echo "tfsort appended to ~/.bashrc"See also: Terraform Standards - Module maintenance scripts for the full PowerShell, Bash, and Python implementations. Git - Tags for the tagging commands that trigger a module registry release.
Testing with bats
🛠️ Deeper reference - covers install, test structure, assertions, setup/teardown, command mocking via PATH override, and CI integration. For quick test execution, jump to “Running tests”.
bats-core (Bash Automated Testing System) is the de-facto unit test runner for bash. Tests are plain .bats files that look like bash with @test blocks. Companion libraries (bats-assert, bats-support, bats-file) give you readable assertions.
Install
# Linux (system-wide)
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local
# macOS
brew install bats-core
# Per-project (recommended for CI reproducibility) - vendor as git submodules
mkdir -p test/test_helper
git submodule add https://github.com/bats-core/bats-core.git test/bats
git submodule add https://github.com/bats-core/bats-support.git test/test_helper/bats-support
git submodule add https://github.com/bats-core/bats-assert.git test/test_helper/bats-assert
git submodule add https://github.com/bats-core/bats-file.git test/test_helper/bats-fileTest file structure
#!/usr/bin/env bats
# test/string_utils.bats
setup() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load 'test_helper/bats-file/load'
# Source the script under test
PROJECT_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
source "$PROJECT_ROOT/lib/string_utils.sh"
}
@test "upper_case_convert: lowercase input -> uppercase" {
run upper_case_convert "hello"
assert_success
assert_output "HELLO"
}
@test "upper_case_convert: already uppercase is unchanged" {
run upper_case_convert "FOO"
assert_output "FOO"
}
@test "title_case_convert: handles multiple words" {
run title_case_convert "hello world"
assert_output "Hello World"
}
@test "non-existent function fails cleanly" {
run not_a_real_function
assert_failure
}Useful assertions
@test "assertion examples" {
run echo "hello world"
assert_success # exit code 0
assert_failure # non-zero exit
assert_failure 2 # specific exit code
assert_output "hello world" # exact match
assert_output --partial "hello" # substring
assert_output --regexp '^hello.*$' # regex
refute_output "goodbye" # negative match
assert_line --index 0 "hello world" # specific line in multi-line output
# File assertions (from bats-file)
assert_file_exist /etc/hostname
assert_file_executable ./deploy.sh
assert_file_contains /etc/hostname "expected"
}setup / teardown / setup_file / teardown_file
setup_file() {
# Once per file - heavy setup goes here
export FIXTURE_DIR="$(mktemp -d)"
cp -r tests/fixtures/* "$FIXTURE_DIR"
}
teardown_file() {
rm -rf "$FIXTURE_DIR"
}
setup() {
# Per test - light/cheap setup
cd "$BATS_TEST_TMPDIR" # fresh temp dir per test, auto-cleaned
}
teardown() {
# Per test - rarely needed; $BATS_TEST_TMPDIR is auto-removed
:
}Mocking external commands - PATH override
Bash has no built-in mocking. The standard trick: prepend a temp directory to PATH containing fake executables that print what you want.
@test "deploy.sh calls terraform apply with -auto-approve" {
local mock_dir; mock_dir=$(mktemp -d)
# Fake `terraform` that records its args and exits 0
cat > "$mock_dir/terraform" <<'EOF'
#!/usr/bin/env bash
echo "$@" >> "$MOCK_CALLS"
EOF
chmod +x "$mock_dir/terraform"
export MOCK_CALLS="$BATS_TEST_TMPDIR/calls.log"
: > "$MOCK_CALLS"
PATH="$mock_dir:$PATH" run ./deploy.sh prod
assert_success
assert_file_contains "$MOCK_CALLS" "apply -auto-approve"
}Mocking shell functions - override after sourcing
If the script under test calls a helper function (not an external binary), redefine the function in the test after sourcing.
@test "deploy aborts if precheck fails" {
source ./deploy.sh
# Override the precheck function
run_precheck() { return 1; }
export -f run_precheck
run deploy_main
assert_failure
assert_output --partial "precheck failed"
}Running tests
# Run a single file
./test/bats/bin/bats test/string_utils.bats
# Run the whole suite
./test/bats/bin/bats test/
# Parallel (one process per file)
./test/bats/bin/bats --jobs 4 test/
# JUnit output for CI
./test/bats/bin/bats --report-formatter junit -o test-results test/
# TAP output (default; CI runners often parse this natively)
./test/bats/bin/bats --formatter tap test/GitHub Actions example
# .github/workflows/test.yml
- uses: actions/checkout@v4
with:
submodules: recursive # pull in bats + helpers
- name: Run bats tests
run: ./test/bats/bin/bats --report-formatter junit -o test-results test/
- uses: dorny/test-reporter@v1
if: always()
with:
name: bats
path: test-results/*.xml
reporter: java-junitCoverage with kcov
bats integrates with kcov for line-level coverage. Install kcov (apt install kcov on Ubuntu) then:
mkdir -p coverage
kcov --include-pattern=lib/ coverage ./test/bats/bin/bats test/
# coverage/index.html now has the reportSensible defaults checklist
- One
.batsfile per script/module under test, named<thing>.bats. setup()sources the script under test; never run installation steps inside a test.- Use
run <command>thenassert_*- direct invocation breaks status capture. - One assertion per behavioural rule; avoid mega-tests that fail for unclear reasons.
- Mock external commands via PATH override; never call real
terraform/az/kubectlfrom a unit test. - Tests run in
$BATS_TEST_TMPDIR(auto-cleaned) - never write to the working tree.
Process Management & Health Checks
Monitor a process, restart if dead
#!/usr/bin/env bash
# Ensure a service is always running; restart if it exits
SERVICE_PID_FILE="/var/run/my-service.pid"
SERVICE_CMD="python3 /opt/service/main.py"
LOG_FILE="/var/log/my-service.log"
check_and_restart() {
if [[ -f "$SERVICE_PID_FILE" ]]; then
local pid=$(cat "$SERVICE_PID_FILE")
if ! kill -0 "$pid" 2>/dev/null; then
echo "Process $pid is dead, restarting..." | tee -a "$LOG_FILE"
rm -f "$SERVICE_PID_FILE"
start_service
fi
else
start_service
fi
}
start_service() {
echo "Starting service..." | tee -a "$LOG_FILE"
$SERVICE_CMD >> "$LOG_FILE" 2>&1 &
echo $! > "$SERVICE_PID_FILE"
}
# Run every 30 seconds
while true; do
check_and_restart
sleep 30
doneHealth check with retry logic
#!/usr/bin/env bash
# Check if a service is healthy; retry with exponential backoff
health_check() {
local endpoint="$1"
local max_retries=5
local retry=0
local backoff=1
while (( retry < max_retries )); do
if curl -sf --max-time 5 "$endpoint" > /dev/null; then
echo "✓ Health check passed"
return 0
fi
echo "⚠ Health check failed (attempt $((retry + 1))/$max_retries), retrying in ${backoff}s..."
sleep "$backoff"
backoff=$((backoff * 2)) # exponential backoff
((retry++))
done
echo "✗ Health check failed after $max_retries attempts"
return 1
}
# Usage
health_check "http://localhost:8080/health" || exit 1Networking & Service Discovery
Wait for a port to be ready
#!/usr/bin/env bash
# Block until a service is listening on a port
wait_for_port() {
local host="$1"
local port="$2"
local timeout="${3:-30}"
local elapsed=0
echo "Waiting for $host:$port..."
while (( elapsed < timeout )); do
if nc -z "$host" "$port" 2>/dev/null; then
echo "✓ $host:$port is ready"
return 0
fi
echo " Not ready yet, retrying... (${elapsed}s/$timeout)"
sleep 2
elapsed=$((elapsed + 2))
done
echo "✗ Timeout waiting for $host:$port"
return 1
}
# Usage
wait_for_port localhost 5432 # PostgreSQL
wait_for_port redis-server 6379 # RedisResolve service from DNS
#!/usr/bin/env bash
# Service discovery - resolve a hostname to IPs
get_service_ips() {
local service_name="$1"
# Use getent to query DNS (more portable than nslookup)
getent hosts "$service_name" | awk '{print $1}' | sort -u
}
# Load balance across resolved IPs
load_balance_to_service() {
local service="$1"
local ips=($(get_service_ips "$service"))
if (( ${#ips[@]} == 0 )); then
echo "Error: No IPs found for $service"
return 1
fi
# Simple round-robin
local idx=$((RANDOM % ${#ips[@]}))
echo "${ips[$idx]}"
}
# Usage
SELECTED_IP=$(load_balance_to_service "api-service")
curl "http://$SELECTED_IP:8080/health"Cloud CLI Patterns (Azure / AWS)
Azure CLI with managed identity
#!/usr/bin/env bash
# Authenticate using managed identity (no secrets in code)
# Azure CLI automatically uses managed identity in Azure VMs, AKS pods, etc.
# Explicitly enable with:
az login --identity
# Run commands
az vm list-ip-addresses --subscription "my-subscription"
az keyvault secret show --vault-name "my-vault" --name "database-password" --query value -o tsv
# Retry pattern for transient failures
retry_az() {
local cmd=("$@")
local max_retries=3
local retry=0
while (( retry < max_retries )); do
if "${cmd[@]}"; then
return 0
fi
echo "Command failed, retrying... ($((retry + 1))/$max_retries)"
sleep $((2 ** retry)) # exponential backoff
((retry++))
done
echo "Command failed after $max_retries attempts"
return 1
}
# Usage
retry_az az deployment group create \
--resource-group "my-rg" \
--template-file "template.json" \
--parameters "environment=prod"Batch operations with Azure CLI
#!/usr/bin/env bash
# Apply operations to multiple resources
apply_to_resources() {
local resource_group="$1"
local resource_type="$2"
shift 2
local update_args=("$@") # e.g. --set tags.env=prod
# List all resources of the given type, NUL-safe.
while IFS= read -r resource; do
[[ -z "$resource" ]] && continue
echo "Updating $resource..."
# Use the generic `az resource update`, which works for any resource type.
az resource update \
--resource-group "$resource_group" \
--resource-type "$resource_type" \
--name "$resource" \
"${update_args[@]}"
done < <(az resource list \
--resource-group "$resource_group" \
--resource-type "$resource_type" \
--query "[].name" -o tsv)
}
# Usage: apply_to_resources "my-rg" "Microsoft.Storage/storageAccounts" --set tags.env=prodContainer & Kubernetes Helpers
Kubernetes port-forward with automatic cleanup
#!/usr/bin/env bash
# Forward a pod's port; clean up on exit
port_forward_pod() {
local pod="$1"
local local_port="$2"
local remote_port="${3:-$local_port}"
local namespace="${4:-default}"
local pf_pid
# Trap to clean up on exit
cleanup() {
echo "Killing port-forward (PID: $pf_pid)"
kill "$pf_pid" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
echo "Port-forwarding: localhost:$local_port -> $pod:$remote_port"
kubectl port-forward -n "$namespace" "pod/$pod" "$local_port:$remote_port" &
pf_pid=$!
wait "$pf_pid"
}
# Usage
port_forward_pod "api-server-abc123" 8080 8080 "production"Wait for Kubernetes deployment to be ready
#!/usr/bin/env bash
# Block until a deployment has all replicas ready
wait_for_deployment() {
local deployment="$1"
local namespace="${2:-default}"
local timeout="${3:-300}"
local elapsed=0
echo "Waiting for deployment/$deployment to be ready..."
while (( elapsed < timeout )); do
local ready=$(kubectl get deployment "$deployment" -n "$namespace" \
-o jsonpath='{.status.conditions[?(@.type=="Available")].status}')
if [[ "$ready" == "True" ]]; then
echo "✓ Deployment ready"
return 0
fi
echo " Waiting... (${elapsed}s/$timeout)"
sleep 5
elapsed=$((elapsed + 5))
done
echo "✗ Deployment did not become ready within $timeout seconds"
kubectl describe deployment "$deployment" -n "$namespace"
return 1
}
# Usage
wait_for_deployment "api-service" "production" 300Deployment & Rollback Patterns
Blue-green deployment with health check
#!/usr/bin/env bash
# Deploy to inactive slot, verify health, then swap
blue_green_deploy() {
local app_name="$1"
local resource_group="$2"
local new_version="$3"
local health_endpoint="$4"
# Pattern: always deploy to the "staging" slot, verify it, then swap into production.
local staging_slot="staging"
echo "Deploying $new_version to the $staging_slot slot of $app_name..."
# Create the staging slot if it does not already exist (idempotent).
az webapp deployment slot create \
--name "$app_name" --resource-group "$resource_group" \
--slot "$staging_slot" --configuration-source "$app_name" 2>/dev/null || true
# Point the staging slot at the new container image (adjust for your deploy method).
az webapp config container set \
--name "$app_name" --resource-group "$resource_group" --slot "$staging_slot" \
--container-image-name "myregistry.azurecr.io/api:$new_version"
# Health check the staging slot before swapping.
local staging_url="https://${app_name}-${staging_slot}.azurewebsites.net${health_endpoint}"
if ! curl -sf --max-time 10 "$staging_url" > /dev/null; then
echo "✗ Health check failed on $staging_slot, aborting swap"
return 1
fi
echo "✓ Health check passed, swapping $staging_slot into production..."
az webapp deployment slot swap \
--name "$app_name" --resource-group "$resource_group" \
--slot "$staging_slot" --target-slot production
echo "✓ Deployment complete"
}
# Usage
blue_green_deploy "my-api-app" "my-rg" "v2.1.0" "/health"Rollback with version pinning
#!/usr/bin/env bash
# Rollback to previous version if current deployment fails
rollback_deployment() {
local deployment="$1"
local namespace="${2:-default}"
local health_check_cmd="$3"
echo "Checking current deployment health..."
if ! eval "$health_check_cmd"; then
echo "✗ Health check failed, rolling back..."
# Rollback: set replicas to 0 for current version, restore previous
kubectl rollout undo "deployment/$deployment" -n "$namespace"
echo "Waiting for rollback to complete..."
kubectl rollout status "deployment/$deployment" -n "$namespace"
echo "✓ Rolled back to previous version"
return 0
fi
echo "✓ Deployment healthy, no rollback needed"
return 0
}
# Usage
rollback_deployment "api-service" "prod" \
"curl -sf http://api-service:8080/health > /dev/null"Quick One-Liners ⚡️
# Upgrade all outdated pip packages (Linux)
pip3 list -o | awk 'NR>2 {print $1}' | xargs -n1 pip3 install -U
# Find all files modified in the last 24 hours
find . -mtime -1 -type f
# Count lines in all .tf files
find . -name "*.tf" | xargs wc -l
# Recursively replace a string in all files
grep -rl 'old-string' . | xargs sed -i 's/old-string/new-string/g'
# Show disk usage, sorted by size
du -sh */ | sort -rh | head -20
# Watch a command every 2 seconds
watch -n2 'kubectl get pods'
# Watch a file for content changes by hash, polling every 5s
h=""; while sleep 5; do n=$(sha256sum /path/to/file | cut -d' ' -f1); t=$(date '+%F %T'); [[ "$n" == "$h" ]] && echo "[$t] Content unchanged ($n)" || { echo "[$t] Content changed! ($n)"; h="$n"; }; doneAnti-patterns
-
🚨 Unquoted variables -
rm -rf $DIRwithDIR=""expands torm -rf(and then whatever follows). Always quote:rm -rf "$DIR". The same applies to$@in argument passing - use"$@", not$@. -
🚨 Parsing JSON, YAML, or XML with
grep,sed, orawk- these tools operate on lines and break on multi-line values, escaped characters, and whitespace. Usejqfor JSON,yqfor YAML,xmllintfor XML. -
⚠️ Logging to stdout inside functions that may be captured -
result=$(my_func)captures all stdout including log lines, silently mixing data with diagnostics. Log to stderr (>&2); keep stdout for data only. -
⚠️
#!/bin/bashin shebangs -/bin/bashdoesn’t exist on all systems (notably NixOS, some containers). Use#!/usr/bin/env bashfor portability. -
🚨 Skipping
set -euo pipefail- without it, commands can fail silently mid-script and execution continues with corrupt state. Every script should start withset -Eeuo pipefail. -
⚠️
lsoutput in scripts - filenames can contain spaces, newlines, and glob characters that break word-splitting. Use glob patterns (for f in *.tf) orfind ... -print0 | xargs -0instead. -
⚠️
cat file | grep pattern- unnecessary fork. Usegrep pattern filedirectly. The same applies tocat file | wc -l→wc -l < file. -
🔬 Ignoring
$?after native commands - withset -e, a failed command exits immediately, but commands in conditionals (if cmd; then) don’t trigger the exit. Explicitly check:cmd || { log_error "cmd failed"; exit 1; }.