Shell Scripting Bash Loops Basics May 2026

Shell Scripting Loops: for & while

Master every form of bash looping — list-based for, C-style for, range for, while with conditions, reading files line-by-line, and loop control with break and continue. Covers all real-world patterns.

Loops are the engine of automation. Bash offers for for iterating over lists and ranges, and while for condition-based repetition. Knowing which to choose and how to use break and continue correctly is what makes scripts robust rather than fragile.

BASH
# ── Basic list ────────────────────────────────────────────
for fruit in apple banana cherry mango; do
  echo "  Fruit: ${fruit}"
done

# ── Loop over array ───────────────────────────────────────
servers=("web-01" "web-02" "db-01" "cache-01")
for srv in "${servers[@]}"; do
  echo "  Pinging ${srv}..."
  ping -c1 -W1 "${srv}" >/dev/null 2>&1 \
    && echo "    ✔ ${srv} reachable" \
    || echo "    ✘ ${srv} unreachable"
done

# ── Loop over command output ──────────────────────────────
for user in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do
  echo "  System user: ${user}"
done

# ── Loop over files matching pattern ─────────────────────
for f in /var/log/*.log; do
  [[ -f "${f}" ]] || continue   # skip if no match (glob safety)
  echo "  Log: ${f}  ($(wc -l < "${f}") lines)"
done
BASH
# ── Brace expansion range ─────────────────────────────────
for i in {1..5}; do
  echo "  Attempt ${i}"
done

# With step
for i in {0..100..10}; do    # 0 10 20 ... 100
  echo "  ${i}%"
done

# Countdown
for i in {10..1}; do
  printf "\r  Restarting in %2d seconds..." "${i}"
  sleep 1
done

# ── C-style for (most flexible) ───────────────────────────
for (( i=1; i<=5; i++ )); do
  echo "  i = ${i}"
done

# C-style with custom step
for (( i=0; i<=100; i+=25 )); do
  echo "  Progress: ${i}%"
done

# ── seq — useful when range is a variable ─────────────────
MAX=5
for i in $(seq 1 "${MAX}"); do
  echo "  Step ${i} of ${MAX}"
done

# NOTE: brace expansion {1..N} doesn't expand variables
# Use $(seq 1 $N) or C-style for when N is a variable
Key Reachable Unreachable Info
bash — server ping loop
vriddh@prod-01:~/scripts$bash ping_servers.sh
Pinging web-01...
✔ web-01 reachable
Pinging web-02...
✔ web-02 reachable
Pinging db-01...
✔ db-01 reachable
Pinging cache-01...
✘ cache-01 unreachable
BASH
# ── Basic while ───────────────────────────────────────────
count=1
while (( count <= 5 )); do
  echo "  Count: ${count}"
  (( count++ ))
done

# ── Retry loop with backoff ───────────────────────────────
MAX_RETRIES=5
attempt=1

while (( attempt <= MAX_RETRIES )); do
  echo "  Attempt ${attempt}/${MAX_RETRIES}..."
  if curl -sf "http://api.example.com/health" >/dev/null; then
    echo "  ✔ Service healthy"
    break
  fi
  echo "  ✘ Not ready — waiting $((attempt * 2))s"
  sleep $(( attempt * 2 ))
  (( attempt++ ))
done

(( attempt > MAX_RETRIES )) && {
  echo "ERROR: Service did not become healthy" >&2
  exit 1
}

# ── Infinite loop with break ──────────────────────────────
while true; do
  load=$(awk '{print $1}' /proc/loadavg)
  echo "  Load: ${load}"
  (( $(echo "${load} > 2.0" | bc -l) )) && {
    echo "  HIGH LOAD — alerting"
    break
  }
  sleep 5
done

The while read pattern is the correct and safe way to process files line by line. It handles spaces in fields, empty lines, and binary-safe reading — unlike looping over command substitution.

BASH
# ── Basic file reading ────────────────────────────────────
while IFS= read -r line; do
  echo "  → ${line}"
done < /etc/hosts

# ── Parse CSV with IFS ────────────────────────────────────
while IFS=',' read -r host port service; do
  [[ "${host}" == "#"* ]] && continue   # skip comments
  [[ -z "${host}" ]] && continue         # skip blank lines
  printf "  Checking %-20s %-6s %s\n" "${host}" "${port}" "${service}"
  nc -z "${host}" "${port}" 2>/dev/null \
    && echo "    ✔ UP" || echo "    ✘ DOWN"
done < services.csv

# ── Read from command output ──────────────────────────────
df -h | while IFS= read -r line; do
  echo "  ${line}"
done

# ── Process substitution (avoids subshell — vars persist) ─
total=0
while IFS=',' read -r name amount; do
  (( total += amount ))
done < <(grep '^item' sales.csv)
echo "Total: ${total}"   # Variable IS accessible here
bash — while read services.csv
vriddh@prod-01:~/scripts$bash check_services.sh
Checking prod-db-01 3306 MySQL
✔ UP
Checking prod-cache-01 6379 Redis
✔ UP
Checking prod-mq-01 5672 RabbitMQ
✘ DOWN
⚠ Pipe vs process substitutioncmd | while read runs the while loop in a subshell — any variables set inside are lost after the loop. Use while read; done < <(cmd) instead to keep variables in the current shell.
BASH
# ── break — exit the loop ─────────────────────────────────
for i in {1..10}; do
  (( i == 5 )) && break
  echo "  i=${i}"
done
# Prints 1 2 3 4 then stops

# ── continue — skip to next iteration ─────────────────────
for i in {1..10}; do
  (( i % 2 == 0 )) && continue   # skip even numbers
  echo "  odd: ${i}"
done

# ── break N — break out of N nested loops ─────────────────
for host in host1 host2 host3; do
  for port in 80 443 8080; do
    if nc -z "${host}" "${port}" >/dev/null 2>&1; then
      echo "  First open port: ${host}:${port}"
      break 2   # break out of BOTH loops
    fi
  done
done

# ── Real-world: find first available backup slot ──────────
for slot in {1..7}; do
  backup_dir="/backups/slot_${slot}"
  [[ -d "${backup_dir}" ]] && continue
  mkdir -p "${backup_dir}"
  echo "  Using backup slot ${slot}"
  break
done
BASH
#!/usr/bin/env bash
# rotate_logs.sh — Archive logs older than N days

set -euo pipefail

LOG_DIR="/var/log/myapp"
ARCHIVE_DIR="/var/log/myapp/archive"
DAYS_OLD=7
rotated=0
errors=0

mkdir -p "${ARCHIVE_DIR}"

echo "Rotating logs older than ${DAYS_OLD} days..."

while IFS= read -r -d '' logfile; do
  filename=$(basename "${logfile}")
  archive_name="${ARCHIVE_DIR}/${filename%.log}_$(date +%Y%m%d).log.gz"

  echo "  Archiving: ${filename}"
  if gzip -c "${logfile}" > "${archive_name}"; then
    rm -f "${logfile}"
    echo "  ✔ Archived to $(basename "${archive_name}")"
    (( rotated++ ))
  else
    echo "  ✘ Failed to archive ${filename}" >&2
    (( errors++ ))
  fi
done < <(find "${LOG_DIR}" -maxdepth 1 -name "*.log" \
          -mtime +"${DAYS_OLD}" -print0)

echo "Done — rotated: ${rotated}, errors: ${errors}"
(( errors > 0 )) && exit 1 || exit 0
bash — rotate_logs.sh
vriddh@prod-01:~/scripts$./rotate_logs.sh
Rotating logs older than 7 days...
Archiving: app_2026-04-20.log
✔ Archived to app_2026-04-20_20260501.log.gz
Archiving: app_2026-04-21.log
✔ Archived to app_2026-04-21_20260501.log.gz
Archiving: error_2026-04-19.log
✔ Archived to error_2026-04-19_20260501.log.gz
Done — rotated: 3, errors: 0
✔ Loop selection guide — Use for item in list when you know the items upfront. Use for ((i=0; i<N; i++)) when you need an index or step control. Use while read for file/stream processing. Use while true; do ... break; done for retry and polling patterns. Always use "$@" not $* in argument loops.