Bash Cheatsheet
Scripting
Use this Bash reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Script Header Best Practices
#!/usr/bin/env bash # Script: deploy.sh # Usage: ./deploy.sh [--env prod] <version> # Requires: git, curl, jq set -euo pipefail IFS=$'\n\t' # safer IFS: only newline and tab, not space # Useful constants readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) readonly SCRIPT_NAME=$(basename "${BASH_SOURCE[0]}") readonly LOG_FILE="/tmp/${SCRIPT_NAME%.sh}.log"
Argument Parsing
# Basic positional arguments name="${1:?Usage: $0 <name> [age]}" age="${2:-25}" # Manual flag parsing with shift verbose=false output="out.txt" while [[ $# -gt 0 ]]; do case "$1" in -v|--verbose) verbose=true; shift ;; -o|--output) output="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; --) shift; break ;; # end of options -*|--*) echo "Unknown option: $1" >&2; exit 1 ;; *) break ;; # first non-option arg esac done # Remaining positional args now in "$@" # getopts — built-in POSIX option parser while getopts "vho:n:" opt; do case $opt in v) verbose=true ;; h) usage; exit 0 ;; o) output="$OPTARG" ;; n) name="$OPTARG" ;; ?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;; esac done shift $(( OPTIND - 1 )) # remove parsed options, leave positional
Usage / Help Functions
usage() { cat <<EOF Usage: $SCRIPT_NAME [OPTIONS] <input> Options: -h, --help Show this help message -v, --verbose Enable verbose output -o, --output Output file (default: out.txt) -n, --name Name to use Examples: $SCRIPT_NAME -v input.txt $SCRIPT_NAME --output result.txt --name Alice input.txt EOF } [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && { usage; exit 0; }
Logging
# Simple colored logger readonly RED='\033[0;31m' readonly YELLOW='\033[1;33m' readonly GREEN='\033[0;32m' readonly NC='\033[0m' # no color log() { echo "[$(date '+%H:%M:%S')] $*" >&2; } info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; } warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; } die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } # Usage info "Starting deployment" warn "Config not found, using defaults" [[ -f required.txt ]] || die "required.txt is missing" # Log to file and stderr log_to_file() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE" >&2 }
Error Handling
set -euo pipefail # fail fast # Handle errors explicitly command || die "command failed" output=$(command) || die "command failed with $?" # Trap on ERR for automatic error messages trap 'echo "Error at line $LINENO: $(sed -n "${LINENO}p" "$0")" >&2' ERR # Cleanup on exit cleanup() { local exit_code=$? rm -f /tmp/myapp_$$.tmp [[ $exit_code -ne 0 ]] && echo "Script failed (exit $exit_code)" >&2 } trap cleanup EXIT # Trap signals trap 'echo "Interrupted"; exit 130' INT trap 'echo "Terminated"; exit 143' TERM # Retry logic retry() { local retries=$1 delay=$2; shift 2 local i for (( i=0; i<retries; i++ )); do "$@" && return 0 echo "Attempt $((i+1)) failed; retrying in ${delay}s..." >&2 sleep "$delay" done return 1 } retry 3 5 curl -sf https://api.example.com/health
Temporary Files and Directories
# Create temp file (auto-cleaned if trap cleanup EXIT) tmpfile=$(mktemp) tmpfile=$(mktemp /tmp/myapp.XXXXXX) # custom pattern # Create temp directory tmpdir=$(mktemp -d) tmpdir=$(mktemp -d /tmp/myapp.XXXXXX) # Always clean up trap 'rm -f "$tmpfile"; rm -rf "$tmpdir"' EXIT # Use temp file some_command > "$tmpfile" process "$tmpfile"
Portability and Compatibility
# Check bash version if (( BASH_VERSINFO[0] < 4 )); then die "Bash 4.0+ required (have $BASH_VERSION)" fi # Check for required commands require() { for cmd in "$@"; do command -v "$cmd" > /dev/null || die "Required command not found: $cmd" done } require git curl jq python3 # OS detection os=$(uname -s) case "$os" in Linux*) is_linux=true ;; Darwin*) is_mac=true ;; MINGW*|CYGWIN*) is_windows=true ;; *) die "Unsupported OS: $os" ;; esac # Running as root? (( EUID == 0 )) && die "Do not run as root" (( EUID == 0 )) || die "Must run as root"
Locking and Concurrency
# Prevent multiple instances LOCK_FILE="/tmp/${SCRIPT_NAME%.sh}.lock" if ! mkdir "$LOCK_FILE" 2>/dev/null; then die "Another instance is running (lock: $LOCK_FILE)" fi trap 'rmdir "$LOCK_FILE"' EXIT # flock-based locking (Linux) exec 9>"$LOCK_FILE" flock -n 9 || die "Another instance is running" # Background jobs with tracking pids=() for item in "${items[@]}"; do process "$item" & pids+=($!) done failed=0 for pid in "${pids[@]}"; do wait "$pid" || (( failed++ )) done (( failed == 0 )) || die "$failed jobs failed"
Script Self-Location
# Absolute path to the script (resolves symlinks) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")" # In functions, ${BASH_SOURCE[0]} still refers to the script file # ${BASH_SOURCE[1]} refers to the caller # Sourced or executed? if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" # executed directly fi # If sourced, functions are loaded but main is not called
Structured Script Pattern
#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # --- Config defaults --- VERBOSE=false OUTPUT="" INPUT="" # --- Functions --- usage() { echo "Usage: $0 [-v] [-o output] <input>"; } log() { "$VERBOSE" && echo "[LOG] $*" >&2 || true; } die() { echo "[ERR] $*" >&2; exit 1; } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -v) VERBOSE=true; shift ;; -o) OUTPUT="$2"; shift 2 ;; -h) usage; exit 0 ;; *) INPUT="$1"; shift ;; esac done [[ -n "$INPUT" ]] || { usage >&2; exit 1; } } main() { parse_args "$@" log "Input: $INPUT, Output: $OUTPUT" # ... do work ... } cleanup() { rm -f /tmp/myscript_$$.*; } trap cleanup EXIT main "$@"
Debugging Techniques
# Trace execution set -x # print each command set +x # disable trace # Trace a block only { set -x; critical_section; set +x; } 2>&1 # Show line numbers in errors trap 'echo "Error at line $LINENO"' ERR # PS4 controls the trace prefix PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' set -x # Dry run mode DRY_RUN=${DRY_RUN:-false} run() { if "$DRY_RUN"; then echo "[DRY RUN] $*" else "$@" fi } run rm -rf /tmp/old_data # Print all vars declare -p # all variables set # all vars and functions env # exported vars only
Configuration Files
# Source a config file safely CONFIG="${SCRIPT_DIR}/config.sh" [[ -f "$CONFIG" ]] && source "$CONFIG" # INI-style config via associative array declare -A config while IFS='=' read -r key value; do [[ "$key" =~ ^#|^$ ]] && continue # skip comments and blanks config["${key// /}"]="${value// /}" # trim spaces done < app.conf echo "${config[database_host]}" # Override from environment DB_HOST="${DB_HOST:-${config[database_host]:-localhost}}"
Progress and Status Display
# Spinner spinner() { local pid=$1 msg="${2:-Loading}" local chars='|/-\' while kill -0 "$pid" 2>/dev/null; do for c in ${chars:0:1} ${chars:1:1} ${chars:2:1} ${chars:3:1}; do printf "\r$msg $c" sleep 0.1 done done printf "\r$msg done\n" } long_running_cmd & spinner $! "Processing" # Progress bar progress_bar() { local current=$1 total=$2 width=${3:-40} local pct=$(( current * 100 / total )) local filled=$(( current * width / total )) local bar=$(printf '%*s' "$filled" | tr ' ' '#') printf "\r[%-*s] %3d%%" "$width" "$bar" "$pct" } for i in $(seq 1 100); do progress_bar $i 100 sleep 0.02 done echo