SH

Bash Scripting

Bash & Linux · 10 entries

Variables & Quoting

syntax
name=value
$name or ${name}
"double quotes" vs 'single quotes'
example
APP_ENV="production"
PORT=8080
echo "Server running on port ${PORT} in ${APP_ENV}"
FILES=$(ls *.conf)
READONLY_VAR="locked"
readonly READONLY_VAR
output
Server running on port 8080 in production

Note No spaces around the = sign. Double quotes expand variables; single quotes are literal. Always double-quote variable expansions ("$var") to prevent word splitting and glob expansion. Use $() for command substitution instead of backticks.

If / Else Conditional

syntax
if [[ condition ]]; then
  commands
elif [[ condition ]]; then
  commands
else
  commands
fi
example
if [[ -f "/opt/app/config.yml" ]]; then
  echo "Config found"
elif [[ -f "/etc/app/config.yml" ]]; then
  echo "Using system config"
else
  echo "No config found, using defaults"
  exit 1
fi

Note Use [[ ]] (double bracket) over [ ] for safer string comparisons and pattern matching. Common file tests: -f (file exists), -d (directory exists), -r (readable), -z (string is empty), -n (string is not empty). Use && and || inside [[ ]].

For Loop

syntax
for var in list; do
  commands
done
example
for host in web01 web02 web03; do
  echo "Deploying to ${host}..."
  scp app.tar.gz deploy@"${host}":/opt/releases/
done

# C-style:
for ((i=1; i<=5; i++)); do
  echo "Attempt ${i}"
done
output
Deploying to web01...
Deploying to web02...
Deploying to web03...

Note Never parse ls output in a for loop. Use globs instead: for f in *.log; do ... done. For iterating over lines in a file: while IFS= read -r line; do ... done < file.txt.

While Loop

syntax
while [[ condition ]]; do
  commands
done
example
RETRIES=0
while [[ $RETRIES -lt 5 ]]; do
  if curl -sf http://localhost:8080/health; then
    echo "Service is healthy"
    break
  fi
  echo "Retry ${RETRIES}..."
  sleep 2
  ((RETRIES++))
done

# Read file line by line:
while IFS= read -r line; do
  echo "Processing: ${line}"
done < servers.txt

Note break exits the loop. continue skips to the next iteration. The 'read file line by line' pattern is the correct way to process files; for loops split on whitespace, which causes bugs with filenames containing spaces.

Case Statement

syntax
case $variable in
  pattern1) commands ;;
  pattern2) commands ;;
  *) default ;;
esac
example
case "$1" in
  start)
    echo "Starting service..."
    systemctl start myapp
    ;;
  stop|restart)
    echo "${1}ing service..."
    systemctl "$1" myapp
    ;;
  status)
    systemctl status myapp
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|status}"
    exit 1
    ;;
esac

Note Each branch ends with ;;. Patterns support globbing and | for alternatives. case is cleaner than long if/elif chains when matching a single variable against many values. *) is the default/catch-all branch.

Functions

syntax
function_name() {
  commands
  return exit_code
}
example
log_msg() {
  local level="$1"
  local message="$2"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${message}"
}

log_msg "INFO" "Deployment started"
log_msg "ERROR" "Config file missing"
output
[2026-04-04 14:22:01] [INFO] Deployment started
[2026-04-04 14:22:01] [ERROR] Config file missing

Note Use local to scope variables to the function (without local, variables are global). Arguments are accessed via $1, $2, etc. $@ is all arguments. return sets the exit code (0-255); to return strings, echo them and capture with $().

Exit Codes & Error Handling

syntax
$?
set -e
set -o pipefail
command || handle_error
example
#!/usr/bin/env bash
set -euo pipefail

grep -q 'ready' status.txt || { echo 'Not ready'; exit 1; }

if ! deploy_app; then
  echo "Deploy failed with code $?"
  rollback
  exit 1
fi

Note $? holds the exit code of the last command (0 = success, non-zero = failure). set -e aborts the script on any non-zero exit. set -u treats unset variables as errors. set -o pipefail catches failures in piped commands. Always use all three in production scripts.

Trap Signals for Cleanup

syntax
trap 'commands' SIGNAL...
example
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"; echo "Cleaned up temp files"' EXIT

# Handle Ctrl+C gracefully:
trap 'echo "Interrupted!"; exit 130' INT

Note EXIT runs on any script exit (normal or error). INT catches Ctrl+C. TERM catches kill signals. trap is essential for cleaning up temp files, releasing locks, or restoring state. Multiple traps on the same signal replace the previous one.

Arrays

syntax
arr=(val1 val2 val3)
${arr[0]}  ${arr[@]}  ${#arr[@]}
example
SERVERS=("web01.prod" "web02.prod" "db01.prod")
echo "First: ${SERVERS[0]}"
echo "All: ${SERVERS[@]}"
echo "Count: ${#SERVERS[@]}"

SERVERS+=("cache01.prod")

for srv in "${SERVERS[@]}"; do
  ssh deploy@"${srv}" 'uptime'
done
output
First: web01.prod
All: web01.prod web02.prod db01.prod
Count: 3

Note Indices start at 0. Always quote "${arr[@]}" to preserve elements with spaces. ${#arr[@]} gives the length. += appends. unset 'arr[1]' removes an element (but does not reindex). Bash arrays are not available in sh/POSIX shell.

String Operations

syntax
${#string}          # length
${string:offset:length}  # substring
${string/pattern/replacement}
example
FILEPATH="/opt/app/logs/server.log"
echo "Length: ${#FILEPATH}"
echo "Filename: ${FILEPATH##*/}"
echo "Directory: ${FILEPATH%/*}"
echo "Change ext: ${FILEPATH%.log}.txt"

URL="https://api.example.com"
echo "${URL/#https/http}"
output
Length: 26
Filename: server.log
Directory: /opt/app/logs
Change ext: /opt/app/logs/server.txt
http://api.example.com

Note ## removes longest prefix match, # removes shortest. %% removes longest suffix, % removes shortest. These are pure Bash operations (no subprocess), so they are much faster than calling sed or basename in a loop.