iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Your First Script

A defensible bash script reads like a small program — shebang, strict mode, named functions, parameter parsing, traps for cleanup, and an entry point at the bottom. Adopting the conventions once means every future script is debuggable, robust against odd inputs, and survives review.

A template you can copy for any bash script

EXAMPLE
#!/usr/bin/env bash
# script-name.sh — one-line description
# Usage: ./script-name.sh [options] <required-arg>

# ===== 1) Strict mode — catch errors early =====
set -Eeuo pipefail
# -E   ensure ERR trap is inherited
# -e   exit on any error
# -u   error on undefined variables
# -o pipefail   a pipeline fails if ANY stage fails (not just the last)

IFS=$'\n\t'    # word-splitting on newline + tab only (no spaces)

# ===== 2) Globals =====
PROG_NAME=$(basename "$0")
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
TMP="$(mktemp -d -t ${PROG_NAME%.sh}.XXXXXX)"

# ===== 3) Cleanup on any exit =====
cleanup() {
  rm -rf "$TMP"
}
trap cleanup EXIT
trap 'echo "ERROR on line $LINENO. cmd: $BASH_COMMAND" >&2' ERR

# ===== 4) Help + usage =====
usage() {
  cat <<USAGE
$PROG_NAME -- import a CSV into orders table

Usage:
  $PROG_NAME [options] <path-to-csv>

Options:
  -d, --dry-run        do not write to the database
  -e, --env <name>     environment (dev|staging|prod) [default: dev]
  -v, --verbose        more output
  -h, --help           this help
USAGE
}

# ===== 5) Defaults + arg parsing =====
DRY_RUN=0
ENV=dev
VERBOSE=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    -d|--dry-run)  DRY_RUN=1; shift ;;
    -e|--env)      ENV="${2:?missing env value}"; shift 2 ;;
    -v|--verbose)  VERBOSE=1; shift ;;
    -h|--help)     usage; exit 0 ;;
    --)            shift; break ;;
    -*)            echo "unknown option: $1" >&2; usage; exit 2 ;;
    *)             break ;;
  esac
done

CSV_PATH="${1:-}"
if [[ -z "$CSV_PATH" ]]; then
  echo "missing CSV path" >&2; usage; exit 2
fi

# ===== 6) Helpers — keep them small and named =====
log() {
  printf '[%s] %s\n' "$(date +'%Y-%m-%dT%H:%M:%S%z')" "$*" >&2
}

debug() {
  (( VERBOSE )) && log "DEBUG: $*"
}

die() {
  log "FATAL: $*"
  exit 1
}

require() {
  command -v "$1" >/dev/null 2>&1 || die "need '$1' on PATH"
}

confirm() {
  read -r -p "$1 [y/N] " ans
  [[ "${ans:-N}" =~ ^[Yy]$ ]]
}

# ===== 7) Main work =====
import_csv() {
  local path="$1"
  [[ -f "$path" ]] || die "CSV not found: $path"
  log "importing $path into $ENV"

  debug "using TMP=$TMP"

  if (( DRY_RUN )); then
    log "DRY RUN — would import $(wc -l <"$path") rows"
    return 0
  fi

  # actual work goes here
  while IFS=, read -r id customer total_cents; do
    debug "row id=$id"
    # ... call the DB, etc.
  done < <(tail -n +2 "$path")
}

main() {
  require psql
  require jq

  import_csv "$CSV_PATH"
  log "done."
}

main "$@"

# ===== Patterns to internalise =====
# - set -Eeuo pipefail at the top of EVERY script
# - mktemp -d for any temp dir; clean up in EXIT trap
# - one main() function called at the bottom
# - small named helpers (log, debug, die, require)
# - manual --long-option parsing (avoid getopt) for portability
# - read -r when reading user input (preserve backslashes)
# - quote ALL variables: "$var"
# - use [[ ... ]] over [ ... ]
# - prefer command substitution $(cmd) over backticks
# - use 'local' inside functions for variables

# ===== Pitfalls =====
# - 'set -e' does NOT exit inside subshells in all cases; check explicitly
# - 'pipefail' is essential — without it, 'grep foo | wc' shows 0 even when grep fails
# - Bash arrays are 0-indexed; "${arr[@]}" expands each element quoted
# - Don't 'cd' without checking; use 'cd ... || die'
# - eval is almost always wrong; reach for arrays or here-docs instead

Why it matters

`set -Eeuo pipefail` + a trap on ERR + a single main() function is the trio that turns a bash script from "works on the laptop where I wrote it" into a small program you can hand to a colleague. Drop it into every script you write; the discipline pays for itself the first time a missing file would have silently produced wrong output instead of a clear error.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
#!/usr/bin/env bash
set -euo pipefail
echo "Hello from a script"
Try it Yourself »

Exercise

Bash shebang line.

Discussion

Loading…