๐Ÿ’ฒ

Bash

Bash scripting reference: variables, arrays, functions, control flow, string/arithmetic operations, I/O, error handling, and script patterns

Variables & Parameters

Declaring, expanding, and applying default values to variables and special parameters

bashยทVariable basics
# Assign (no spaces around =)
name="Alice"
count=42
readonly PI=3.14159      # constant

# Expand
echo "$name"
echo "${name}"           # unambiguous form

# Unset
unset name

# Environment variables
export APP_ENV="production"
printenv APP_ENV
env | grep APP
bashยทParameter expansion & defaults
# Default values
echo "${VAR:-default}"       # use default if unset or empty
echo "${VAR:=default}"       # assign default if unset or empty
echo "${VAR:?error msg}"     # error and exit if unset or empty
echo "${VAR:+other}"         # use other if VAR is set

# String length
echo "${#name}"

# Substring
str="Hello, World"
echo "${str:7}"              # World
echo "${str:7:5}"            # World (5 chars from pos 7)

# Remove prefix / suffix
path="/usr/local/bin/bash"
echo "${path##*/}"           # bash (strip longest prefix match)
echo "${path%/*}"            # /usr/local/bin (strip suffix)
echo "${path#*/}"            # usr/local/bin/bash (strip shortest prefix)

# Replace
echo "${str/World/Bash}"     # replace first
echo "${str//l/L}"           # replace all
bashยทSpecial parameters
$0     # script name
$1..$9 # positional arguments
$@     # all arguments (preserves quoting)
$*     # all arguments as single word
$#     # number of arguments
$?     # exit status of last command
$$     # PID of current shell
$!     # PID of last background job
$_     # last argument of previous command

# Shift positional parameters
shift         # discard $1, $2 becomes $1
shift 2       # discard first two

Strings & Arithmetic

String manipulation, case conversion, printf formatting, and arithmetic

bashยทString operations
str="Hello, World!"

# Uppercase / lowercase (Bash 4+)
echo "${str^^}"              # HELLO, WORLD!
echo "${str,,}"              # hello, world!
echo "${str^}"               # Capitalise first char

# Check if string contains substring
if [[ "$str" == *"World"* ]]; then echo "found"; fi

# Regex match
if [[ "$str" =~ ^Hello ]]; then echo "starts with Hello"; fi
echo "${BASH_REMATCH[0]}"    # full match
echo "${BASH_REMATCH[1]}"    # first capture group
bashยทprintf & here-docs
# printf โ€” better than echo for formatting
printf "Name: %s, Age: %d\n" "Alice" 30
printf "%05d\n" 42           # 00042
printf "%.2f\n" 3.14159      # 3.14

# Here-document
cat <<EOF
Line one
Line two: $name
EOF

# Here-doc without variable expansion
cat <<'EOF'
Literal $name โ€” not expanded
EOF

# Here-string
grep "pattern" <<< "$variable"
bashยทArithmetic
# (( )) โ€” arithmetic evaluation
((x = 5 + 3))
((x++))
((x *= 2))
echo $((10 / 3))             # integer division: 3
echo $((10 % 3))             # modulo: 1
echo $((2 ** 8))             # exponentiation: 256

# let
let "x = 5 * 4"

# Floating point requires bc or awk
echo "scale=2; 10 / 3" | bc  # 3.33
awk 'BEGIN { printf "%.4f\n", 10/3 }'

Arrays

Indexed arrays, associative arrays, and common array operations

bashยทIndexed arrays
# Declare and assign
fruits=("apple" "banana" "cherry")
fruits[3]="date"

# Access
echo "${fruits[0]}"          # apple
echo "${fruits[@]}"          # all elements
echo "${#fruits[@]}"         # count
echo "${!fruits[@]}"         # all indices

# Slice
echo "${fruits[@]:1:2}"      # banana cherry

# Append
fruits+=("elderberry")

# Iterate
for fruit in "${fruits[@]}"; do
  echo "$fruit"
done

# Delete element / array
unset 'fruits[1]'
unset fruits
bashยทAssociative arrays (Bash 4+)
# Must declare explicitly
declare -A colors

colors["red"]="#FF0000"
colors["green"]="#00FF00"
colors["blue"]="#0000FF"

# Access
echo "${colors[red]}"

# All keys / values
echo "${!colors[@]}"         # keys
echo "${colors[@]}"          # values

# Iterate
for key in "${!colors[@]}"; do
  echo "$key = ${colors[$key]}"
done

# Check key exists
if [[ -v colors["red"] ]]; then echo "exists"; fi

Control Flow

if/elif/else, case, for, while, until, break, and continue

bashยทif / elif / else
# File tests
if [ -f "$file" ]; then
  echo "regular file"
elif [ -d "$file" ]; then
  echo "directory"
else
  echo "other"
fi

# String comparison ([[ preferred ])
if [[ "$env" == "prod" ]]; then echo "production"; fi
if [[ -z "$var" ]]; then echo "empty"; fi
if [[ -n "$var" ]]; then echo "not empty"; fi

# Numeric comparison
if (( count > 10 )); then echo "large"; fi
if [ "$count" -gt 10 ]; then echo "large"; fi   # POSIX style

# Combine conditions
if [[ "$a" == "x" && "$b" == "y" ]]; then echo "both"; fi
if [[ "$a" == "x" || "$b" == "y" ]]; then echo "either"; fi
bashยทcase
case "$1" in
  start)
    echo "Starting..."
    ;;
  stop|halt)
    echo "Stopping..."
    ;;
  restart)
    echo "Restarting..."
    ;;
  [0-9]*)
    echo "Numeric argument"
    ;;
  *)
    echo "Unknown: $1"
    exit 1
    ;;
esac
bashยทLoops
# for โ€” list
for item in one two three; do
  echo "$item"
done

# for โ€” C-style
for ((i = 0; i < 10; i++)); do
  echo "$i"
done

# for โ€” range with step
for i in {1..5}; do echo "$i"; done
for i in {0..20..5}; do echo "$i"; done

# while โ€” read file line by line
while IFS= read -r line; do
  echo "$line"
done < file.txt

# until
until ping -c1 host &>/dev/null; do
  echo "waiting..."; sleep 5
done

# break / continue
for i in {1..10}; do
  ((i % 2 == 0)) && continue
  ((i > 7))      && break
  echo "$i"
done

Functions

Declaring functions, local variables, return values, and common patterns

bashยทDeclaring & calling
# Two equivalent syntaxes
greet() {
  echo "Hello, $1!"
}

function greet {
  echo "Hello, $1!"
}

greet "World"

# Local variables (avoid polluting global scope)
calculate() {
  local result=$(( $1 * $2 ))
  echo "$result"
}

product=$(calculate 6 7)
echo "$product"    # 42
bashยทReturn values & error codes
# return sets exit status (0โ€“255)
is_even() {
  (( $1 % 2 == 0 ))   # returns 0 (true) if even
}

if is_even 4; then echo "even"; fi

# Return data via stdout
get_date() {
  date +%Y-%m-%d
}
today=$(get_date)

# Return data via nameref (Bash 4.3+)
get_info() {
  local -n _result=$1   # nameref โ€” points to caller's variable
  _result="some value"
}
get_info my_var
echo "$my_var"
bashยทUseful function patterns
# Logging helpers
info()  { echo "[INFO]  $*"; }
warn()  { echo "[WARN]  $*" >&2; }
error() { echo "[ERROR] $*" >&2; }
die()   { error "$*"; exit 1; }

# Require command exists
require() {
  command -v "$1" &>/dev/null || die "$1 is required but not installed"
}
require docker
require kubectl

# Retry with backoff
retry() {
  local tries=$1; shift
  for ((i=1; i<=tries; i++)); do
    "$@" && return 0
    echo "Attempt $i/$tries failed. Retrying in ${i}s..."
    sleep "$i"
  done
  return 1
}
retry 3 curl -sf https://api.example.com/health

Input / Output & Redirection

read, file descriptors, redirection, pipes, and process substitution

bashยทread โ€” user input
# Basic read
read -p "Enter name: " name
echo "Hello, $name"

# Silent (passwords)
read -s -p "Password: " password
echo

# Timeout
read -t 10 -p "Continue? [y/N] " answer || answer="n"

# Read into array
read -ra parts <<< "one two three"
echo "${parts[1]}"    # two

# Read file line by line
while IFS= read -r line; do
  echo ">> $line"
done < input.txt

# Read CSV with delimiter
while IFS=',' read -r field1 field2; do
  echo "$field1 | $field2"
done < data.csv
bashยทRedirection & file descriptors
# Standard streams: 0=stdin  1=stdout  2=stderr

command > out.txt          # stdout โ†’ file (overwrite)
command >> out.txt         # stdout โ†’ file (append)
command 2> err.txt         # stderr โ†’ file
command &> all.txt         # stdout + stderr โ†’ file
command 2>&1               # redirect stderr to stdout
command > /dev/null 2>&1   # suppress all output

# Custom file descriptors
exec 3> output.txt         # open fd 3 for writing
echo "data" >&3
exec 3>&-                  # close fd 3

exec 4< input.txt          # open fd 4 for reading
read -u4 line
exec 4<&-

# Process substitution
diff <(sort file1.txt) <(sort file2.txt)
wc -l <(find . -name "*.go")
bashยทPipes & tee
# Pipe stdout to next command
cat file.txt | grep error | sort | uniq -c | sort -rn

# tee โ€” write to file AND stdout
command | tee output.txt
command | tee -a output.txt     # append

# Named pipes (FIFOs)
mkfifo /tmp/mypipe
producer > /tmp/mypipe &
consumer < /tmp/mypipe

# Redirect stderr through pipe
command 2>&1 | grep error

# pipefail โ€” catch errors in pipelines
set -o pipefail
false | true; echo $?   # 1 (without pipefail would be 0)

Error Handling & Debugging

set options, trap, strict mode, and bash debug techniques

bashยทStrict mode
#!/usr/bin/env bash
set -euo pipefail
# -e  exit immediately on error
# -u  treat unset variables as errors
# -o pipefail  catch errors in pipelines
# Combine with -E for trap inheritance in functions:
set -Eeuo pipefail

# Allow a single command to fail
grep "pattern" file.txt || true
if ! command; then echo "failed but continuing"; fi
bashยทtrap โ€” cleanup & signals
# Run cleanup on exit (even on error)
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/lockfile
}
trap cleanup EXIT

# Catch errors with line number
on_error() {
  echo "Error on line $1" >&2
}
trap 'on_error $LINENO' ERR

# Handle signals
trap 'echo "Interrupted"; exit 130' INT TERM

# Ignore a signal
trap '' HUP

# Reset to default
trap - EXIT
bashยทDebugging
# Trace execution (print each command before running)
set -x
# ... commands ...
set +x    # stop tracing

# Run entire script in debug mode
bash -x script.sh

# Dry-run pattern
DRY_RUN=${DRY_RUN:-false}
run() {
  if [[ "$DRY_RUN" == "true" ]]; then
    echo "[DRY RUN] $*"
  else
    "$@"
  fi
}
run rm -rf /tmp/old_data

# Static analysis
shellcheck script.sh

Script Structure & Patterns

Shebang, getopts argument parsing, lock files, and script templates

bashยทScript header template
#!/usr/bin/env bash
set -Eeuo pipefail

# Reliable script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Colours
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'

log()   { echo -e "${GREEN}[INFO]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die()   { error "$*"; exit 1; }
bashยทArgument parsing with getopts
usage() {
  cat <<EOF
Usage: $(basename "$0") [-v] [-o output] <input>
  -v        verbose
  -o FILE   output file (default: output.txt)
  -h        show help
EOF
  exit 0
}

VERBOSE=false
OUTPUT="output.txt"

while getopts ":vo:h" opt; do
  case $opt in
    v) VERBOSE=true ;;
    o) OUTPUT="$OPTARG" ;;
    h) usage ;;
    :) die "Option -$OPTARG requires an argument" ;;
    ?) die "Unknown option: -$OPTARG" ;;
  esac
done
shift $((OPTIND - 1))   # remove parsed options

INPUT="${1:-}"
[[ -z "$INPUT" ]] && die "Input file required"
bashยทLock files & temp dirs
# Prevent concurrent runs
LOCKFILE="/tmp/$(basename "$0").lock"
exec 200>"$LOCKFILE"
flock -n 200 || die "Another instance is already running"
trap 'rm -f "$LOCKFILE"' EXIT

# Idempotent operation
MARKER="/var/run/setup-done"
if [[ ! -f "$MARKER" ]]; then
  # ... do setup ...
  touch "$MARKER"
fi

# Safe temp files / dirs
TMP=$(mktemp)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMP" "$TMPDIR"' EXIT

Advanced Features

Brace expansion, globs, subshells, command grouping, and parallel execution

bashยทBrace expansion & globs
# Brace expansion
echo {a,b,c}.txt              # a.txt b.txt c.txt
echo file{1..5}.log           # file1.log ... file5.log
mkdir -p project/{src,tests,docs}
cp config.yaml{,.bak}         # backup: config.yaml.bak

# Glob patterns
ls *.txt                      # any .txt
ls file?.log                  # single char wildcard
ls [abc]*.sh                  # character class
ls **/*.ts                    # recursive (needs globstar)

# shopt โ€” shell options
shopt -s globstar             # enable ** recursive glob
shopt -s nullglob             # no match โ†’ empty, not literal
shopt -s nocaseglob           # case-insensitive glob
shopt -s extglob              # extended patterns
ls !(*.log)                   # extglob: anything except .log
bashยทSubshells & grouping
# Subshell โ€” changes don't affect parent
(cd /tmp && ls)               # pwd unchanged afterwards
(export VAR=x; echo $VAR)     # VAR not set in parent

# Command grouping (same shell)
{ echo "start"; do_work; echo "end"; } > output.txt

# Command substitution
result=$(command)             # preferred โ€” nestable
files=$(find . -name "*.go" | wc -l)
echo "Found $((files)) Go files"
bashยทParallel execution
# Background jobs + wait
for url in "${urls[@]}"; do
  curl -sO "$url" &
done
wait    # wait for all background jobs

# Wait for specific PIDs
process1 & pid1=$!
process2 & pid2=$!
wait "$pid1" && wait "$pid2"

# xargs parallel
find . -name "*.jpg" | xargs -P4 -I{} convert {} -resize 800x {}

# Limit concurrency manually
MAX=4; running=0
for item in "${items[@]}"; do
  process "$item" &
  ((++running))
  (( running >= MAX )) && { wait -n; ((--running)); }
done
wait