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