Loops & Iteration

for Loops

Loop Over a Word List

The classic loop walks a list of words. Quote array expansions to keep elements intact.

for fruit in apple banana cherry; do
  echo "fruit: $fruit"
done

servers=(web1 web2 db1)
for s in "${servers[@]}"; do
  ping -c1 "$s" >/dev/null && echo "$s up" || echo "$s down"
done

C-Style Loop

When you need a counter, the arithmetic for reads like C — but note the double parentheses and the lack of $ inside them.

for ((i = 0; i < 5; i++)); do
  printf 'i=%d squared=%d\n' "$i" "$((i * i))"
done

Ranges and Brace Expansion

for n in {1..5}; do echo "$n"; done      # 1 2 3 4 5
for n in {10..1..2}; do echo "$n"; done  # 10 8 6 4 2 (step -2)
for c in {a..e}; do echo "$c"; done
{1..5} is expanded before the loop runs, so it builds the whole list in memory. For huge ranges prefer the C-style loop.

Globbing Loop

shopt -s nullglob
for f in *.log; do
  echo "processing $f"
done

while Loops

Reading a File Line by Line

This is the single most important loop idiom in shell scripting. Note the three essential details: IFS=, read -r, and the redirection on done.

while IFS= read -r line; do
  printf 'line: %s\n' "$line"
done < data.txt
  • IFS= stops trimming leading/trailing whitespace.
  • read -r stops the backslash from being eaten.
  • The redirection on done keeps the loop in the current shell.
cat file | while read … runs the loop in a subshell. Variables set inside are lost. Redirect from the file instead: while … ; done < file.

Counters and Conditions

n=0
while (( n < 3 )); do
  echo "attempt $n"
  (( n++ ))
done

while [[ -n "$pending" ]]; do
  process "$pending"
  pending=""
done

Infinite Loops

while true; do
  if ! check_service; then
    restart_service
  fi
  sleep 30
done

until Loops

until is the mirror image of while: it loops as long as the condition is false.

until curl -sf http://localhost:8080/health >/dev/null; do
  echo "waiting for service..."
  sleep 2
done
echo "service is up"

select Menus

select builds a numbered menu automatically and loops until you break. It reads from stdin, so it is for interactive scripts only.

select choice in "List files" "Show date" "Quit"; do
  case "$choice" in
    "List files") ls ;;
    "Show date")  date ;;
    "Quit")       break ;;
    *)            echo "invalid option" ;;
  esac
done

break & continue

for f in *.txt; do
  [[ -s "$f" ]] || continue        # skip empty files
  grep -q ERROR "$f" && { echo "fatal: $f"; break; }
  echo "checked $f"
done

for i in {1..3}; do
  for j in {1..3}; do
    (( i == j )) && continue 2     # '2' skips the outer loop iteration
    echo "$i-$j"
  done
done

The optional number after break or continue chooses how many loop levels to affect.

Pitfalls

  • Unquoted $var in a for list splits on spaces — use an array and "${arr[@]}".
  • Piping into while loses variables to the subshell — redirect instead.
  • Missing -r on read mangles Windows paths such as C:\temp.
  • for i in $(ls) breaks on filenames with spaces; loop over a glob instead.