Module 6 — Loops

Updated 3 September 2026

Module 6 — Loops. Doing something to 50 files or 500 hosts without typing it 500 times — the core of every batch operation and fleet task.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Modules 1–5. Load-bearing here: word splitting and globbing (Module 2 — one famous loop trap is built from them), exit codes (Module 3 — while runs on verdicts exactly as if does), < redirection (Module 4), and [ ]/case (Module 5). Tools: nothing new. Keep ~/bash-course.

Part A — for: once per item on a list

A1. The for-in loop

The for loop runs its body once per word in a list, with a variable holding the current word each time:

bash
for name in alice bob carol; do
    echo "Provisioning account for $name"
done

Read it as a sentence: for each name in this list, do the body, done. The pieces: name is an ordinary variable (Module 2 rules apply — read it as "$name" inside the body); the list is any sequence of words; dodone bracket the body the way thenfi bracketed a conditional. And the crucial mechanical fact, on which both the power and the traps of this module hang: the list is built by the shell's ordinary rewriting machinery first — expansion, splitting, globbing (Module 2's pipeline) — and then the loop runs over whatever words emerged.

Real-world analogy — the clipboard round. A ward nurse on medication rounds: a clipboard lists patients; the nurse visits each in turn, performing the same procedure with a different name filled in each time. The clipboard is written out completely before the round starts — patients admitted mid-round are not on it.

Where the analogy stops working. The nurse reads names as lines on a form. The shell's clipboard is built by word splitting — one name containing a space becomes two visits to two nonexistent patients. Every for-loop trap in this module is that one mismatch wearing different costumes, so keep the clipboard image and this correction together.

🧪 Exercise 6.1 — the round
bash
for name in alice bob carol; do
    echo "Provisioning account for $name"
done
Expected result — click to reveal
plain text
Provisioning account for alice
Provisioning account for bob
Provisioning account for carol

What to read out of it: three words in, three iterations out, in list order. Nothing ran in parallel — iteration 2 started only after iteration 1's body finished (parallel fan-out is Module 12's &). If a body command failed mid-round, the loop would carry on to the next item — loops inherit bash's carry-on default (Module 3), and Part D returns to what production does about that.

A2. Counted rounds: ranges, C-style, and a first taste of arithmetic

"Do this N times" is just a for-in over a list of numbers, and Module 1's brace expansion builds the list: for i in {1..5} — bash rewrites {1..5} to 1 2 3 4 5 before the loop ever looks. For loops whose count is computed rather than fixed, bash borrows C's syntax: for ((i=1; i<=3; i++)) — start, keep-going condition, step. Inside (( )) you are in arithmetic land: a small dialect where variables need no $, <= compares numbers (not strings — no Module 5 dictionary surprises), and i++ means add one. The same land is reachable inline anywhere as $(( expression ))arithmetic expansion, the missing tool for counters: n=$((n-1)) computes n minus one and assigns it back. (Bash arithmetic is integer-only; $((7/2)) is 3. Decimals need other tools — Module 11's awk.)

🧪 Exercise 6.2 — three ways to count
bash
for i in {1..5}; do echo "attempt $i"; done
for ((i=1; i<=3; i++)); do echo "tick $i"; done
n=10
echo "$((n / 3)) and a remainder of $((n % 3))"    # integer division; % = remainder
Expected result — click to reveal
plain text
attempt 1
attempt 2
attempt 3
attempt 4
attempt 5
tick 1
tick 2
tick 3
3 and a remainder of 1

What to read out of it: the one-line loop form (; do … ; done) is the same grammar compacted — semicolons standing in for newlines (Module 3's glue). The last line is the arithmetic preview earning its keep: 10 / 3 is 3, not 3.33… — bash truncates, silently. Scripts that compute percentages or averages in bash arithmetic are quietly wrong in the decimals; know the limitation now, reach for awk later.

A3. Looping over files: globs, and the no-match surprise

The right way to loop over files is to let globbing build the clipboard: for f in logs/*.log. Module 2 taught that unquoted * expands to matching filenames — here that is not a hazard but the design: each matching path arrives as one word, spaces and all, because glob results do not get re-split. This is the one place the quoting reflex relaxes on the list side (the glob must be unquoted to work) while staying strict in the body ("$f", always).

Counter-intuitive: a glob that matches nothing does not produce nothing — it stays as-is, and your loop runs once, with the literal pattern as the value: f = logs/*.zip. A cleanup loop then processes (or deletes!) a "file" named logs/*.zip, or errors confusingly. This default (unmatched globs pass through as text) made sense for interactive shells; in scripts it is a landmine. Defenses: guard the body with [ -e "$f" ] || continue (Module 5 + this module's continue), or flip the shell option shopt -s nullglob, which makes empty matches yield an empty list — zero iterations, as a script almost always wants.
🧪 Exercise 6.3 — files done right, and the landmine stepped on

The second loop misbehaves on purpose.

bash
cd ~/bash-course
mkdir -p logs
touch logs/app.log "logs/db backup.log" logs/web.log
for f in logs/*.log; do echo "found: $f"; done
for f in logs/*.zip; do echo "processing: $f"; done   # ← no .zip files exist
Expected result — click to reveal (contains a deliberate misbehavior)
plain text
found: logs/app.log
found: logs/db backup.log
found: logs/web.log
processing: logs/*.zip

What to read out of it: line 2 is the quiet triumph — db backup.log, spaces included, arrived as one item with no quoting gymnastics: glob-built lists are split-safe. Line 4 is the landmine: no zip files exist, so the loop ran once anyway, holding the raw pattern as if it were a filename. In a report loop this prints nonsense; in a rm/mv loop it acts on nonsense. Production loops over globs either guard ([ -e "$f" ] || continue) or set shopt -s nullglob at the top of the script — pick one and be consistent.

Interview questions — Part A

🎯 "Write down the Syntax for all the loops in Shell Scripting." — asked verbatim at Edureka; FinalRound AI asks "What are the different types of loops in shell scripting? Write a script using a for loop to print numbers 1 to 10."

The direct answer: three constructs. for var in list; do …; done (once per word); while command; do …; done (repeat while the guard succeeds); until command; do …; done (repeat while it fails). Plus bash's C-style counter: for ((i=1; i<=10; i++)); do …; done. The 1-to-10 script: for i in {1..10}; do echo "$i"; done.

Going deeper: say what the list is — the product of expansion, splitting, and globbing — because every follow-up trap question ("what if a filename has a space?") is really asking that; and note while/until take a command whose exit status is the condition, the same mechanism as if (Module 5).

The details that separate candidates: offering {1..10} and the C-style form unprompted, with the rule for choosing (fixed range → brace; computed bound → C-style, since {1..$n} does not expand — brace expansion runs before variables exist, Module 2's pipeline order); and mentioning seq only to say modern bash rarely needs it.

🎯 "How do you use loops to iterate over files in a directory in Bash?" — asked verbatim at Zero To Mastery

The direct answer: for f in /path/*.log; do … "$f" …; done — glob builds the list, each match is one word regardless of spaces; quote "$f" everywhere in the body.

Going deeper: name the two classic failures — parsing ls output instead of globbing (splits on spaces, mangles odd names; the same disease as looping over cat, Part C) and the unmatched-glob pass-through (guard with [ -e "$f" ] || continue or shopt -s nullglob). For recursive trees, find + while read (Part C) or bash's globstar ** are the tools.

The details that separate candidates: stating why glob lists are split-safe (filename expansion happens after word splitting in the pipeline, and its results are never re-split) — mechanism, not folklore; plus the habit of ./-prefixing or using full paths so a filename beginning with - cannot masquerade as an option to the body's commands.

Part B — while and until: repeat on a verdict

B1. while: if that keeps coming back

while command; do … done re-runs its body as long as the guard command keeps exiting 0 — Module 5's if, on repeat. The guard is checked before each round, so a guard that fails immediately means zero iterations. Everything that could stand guard for if can stand guard here: [ ] tests, [[ ]], grep -q, your own scripts. The mirror-image until command loops while the guard fails — tailor-made for "keep waiting until the service answers."

bash
n=3
while [ "$n" -gt 0 ]; do
    echo "countdown: $n"
    n=$((n-1))      # A2's arithmetic: the loop must CHANGE what the guard measures
done
echo "liftoff"
Real-world analogy — the doorman's rule. A doorman lets the queue keep flowing while the "seats available" sign stays lit; every admitted guest changes the seat count, and eventually the sign flips. until is the same doorman with the opposite rule: keep turning people away until the kitchen rings ready.

Where the analogy stops working. The seat count changes because guests exist — the world updates itself. In a while loop, nothing updates unless the body does it: forget n=$((n-1)) and the guard measures the same unchanged world forever — the infinite loop. Every accidental infinite loop is a body that stopped moving the thing its guard measures; check that connection first when a script hangs (and Ctrl-C, Module 13's polite kill, gets you out meanwhile).

🧪 Exercise 6.4 — countdown and stakeout
bash
n=3
while [ "$n" -gt 0 ]; do echo "countdown: $n"; n=$((n-1)); done
echo "liftoff"
n=1
until [ "$n" -gt 3 ]; do echo "poll $n"; n=$((n+1)); done
Expected result — click to reveal
plain text
countdown: 3
countdown: 2
countdown: 1
liftoff
poll 1
poll 2
poll 3

What to read out of it: the countdown's guard measured n, and the body moved n — that pairing is what ends a while loop. Before liftoff, the guard ran a fourth time (n=0, -gt 0 fails) — guards always run once more than the body. The until loop polled exactly three times: it keeps going while its guard fails, and [ "$n" -gt 3 ] failed for n=1,2,3 then passed for n=4. If you ever misread an until as "loop while true," the printed poll count is the tell.

Trap: the infinite loop is not exotic — it is one missing line. Delete n=$((n-1)) from the countdown and the guard measures an unchanging world forever; the script hangs with no error, because nothing is wrong by bash's lights. When any script hangs, your first question is now mechanical: which loop's body stopped moving the thing its guard measures? (Ctrl-C interrupts the hang — Module 13 explains exactly what that keystroke sends.)

B2. break and continue: leaving early, skipping one

Two builtins steer a running loop. break abandons the whole loop immediately — the search-is-over move. continue abandons only the current iteration and jumps to the next item — the skip-this-one move. Both read naturally with Module 5's conditionals: guard, then steer.

🧪 Exercise 6.5 — steer the round
bash
for host in web-1 web-2 db-1 web-3; do
    case "$host" in
        db-*) echo "skipping $host (not a web host)"; continue ;;
    esac
    echo "checking $host"
done
for i in {1..10}; do
    if [ "$i" -eq 4 ]; then echo "found it at attempt $i"; break; fi
    echo "attempt $i: not yet"
done
Expected result — click to reveal
plain text
checking web-1
checking web-2
skipping db-1 (not a web host)
checking web-3
attempt 1: not yet
attempt 2: not yet
attempt 3: not yet
found it at attempt 4

What to read out of it: continue let db-1 announce itself and step aside — web-3 still got its turn; the round survived the skip. break ended the second loop with six attempts unspent — attempts 5–10 simply never printed. Note the division of labor: case recognized the shape (db-*), continue did the steering — Module 5 and this module composing, which is how real scripts read.

Interview questions — Part B

🎯 "What is the difference between break and continue commands?" — asked verbatim at Edureka

The direct answer: break exits the entire loop at once; continue ends only the current iteration and moves to the next. Both work in for, while, and until.

Going deeper: give each its canonical shape — break for searches ("stop at first match") and retry loops ("stop on first success"); continue for filters ("skip items that don't apply"). Both take an optional numeric argument for nested loops: break 2 exits two levels — powerful, rarely wise (a comment is mandatory when you use it).

The details that separate candidates: knowing continue in a while loop still runs the guard next (so a continue above the counter-update line can create an infinite loop — the B1 analogy's broken connection, self-inflicted); and that break/continue affect only loops — in a case you exit a branch with ;;, and mixing those up is a syntax error interviewers bait with.

🎯 "How do you use loops in Bash?" — asked verbatim at Zero To Mastery; KnowledgeHut asks "How do you use loops (e.g., for, while) in a shell script?"

The direct answer: for over lists (words, ranges, globs), while on a repeating verdict, until on a repeating failure, steered by break/continue — each with dodone bodies.

Going deeper: choose by data shape — known list → for; condition-driven → while/until; file lines → while read (never for — the trap has its own section in this module). Show the retry idiom that fuses the family: until curl -fsS "$url" >/dev/null 2>&1; do sleep 2; done — Modules 3, 4, and this one in a single line.

The details that separate candidates: mentioning that loop bodies inherit carry-on-past-failure semantics, and what production does about it (guards in the body, or set -e with its loop subtleties, Module 14); plus the resource nuance that while read < file opens the file once, not per-iteration — a preview of why Part C's pattern scales to gigabyte files.

Part C — Reading a file line by line (the famous trap, and the real pattern)

C1. Why for over a file is wrong

The tempting spelling — for f in $(cat files.txt) — is one of bash's most-written bugs, and you already own every piece of its autopsy. $(cat …) substitutes the file's entire text (Module 2 A3); it is unquoted, so word splitting carves it at every space, tab, and newline alike (Module 2 B1). The loop does not iterate over lines — it iterates over words, and a line monthly report.txt becomes two items. Globbing even runs on each word (a line containing * explodes into filenames — Module 2 B3). The loop's clipboard was built by the wrong machine.

🧪 Exercise 6.6 — the autopsy, live

The first loop mangles its data on purpose.

bash
cd ~/bash-course
printf 'monthly report.txt\nnotes.txt\n' > files.txt
for f in $(cat files.txt); do echo "would archive: [$f]"; done
while IFS= read -r line; do echo "would archive: [$line]"; done < files.txt
Expected result — click to reveal (contains a deliberate mangling)
plain text
would archive: [monthly]
would archive: [report.txt]
would archive: [notes.txt]
would archive: [monthly report.txt]
would archive: [notes.txt]

What to read out of it: two lines in, three archive candidates out of the for-loop — the brackets (Module 2's probe style) show exactly where the split fell. The while-loop below it got both lines intact. Now imagine the body was mv "$f" /archive/ — the for-version just tried to archive two files that don't exist, and if a file named monthly happened to exist, it archived the wrong file with no error at all. That silent last case is why this trap has a section, not a footnote.

C2. The real pattern, word by word: while IFS= read -r line

The correct tool reads one line per iteration, using machinery you now own end to end:

bash
while IFS= read -r line; do
    echo "processing: $line"
done < input.txt

read is a builtin that reads one line from stdin into a variable, exiting 0 on success and non-zero at end-of-file — a verdict, which is exactly what while wants: the loop runs once per line and stops itself at the file's end. The < input.txt is Module 4's stdin redirection, feeding the whole loop (the file opens once, streams line by line — gigabytes welcome). The two amulets guard the edges: -r stops read from treating backslashes as escapes (without it, a Windows path C:\new\folder arrives as C:newfolder — data silently altered); IFS= blanks the split-variable for just this command, preserving leading and trailing whitespace exactly (indented config lines survive). You met IFS as word splitting's delimiter list (Module 2); setting it empty for the duration of read is the "do not trim anything" instruction.

Real-world analogy — the conveyor scanner. The for-over-cat version dumps the whole parcel manifest into a shredder and processes the confetti. while read is a barcode scanner at a conveyor: one parcel passes, one beep, one action — the parcel's contents are never opened, never reinterpreted, and the belt stops by itself when the last parcel passes (read's end-of-file verdict).

Where the analogy stops working. A scanner reads any parcel identically. read without its amulets edits the parcels as they pass — eating backslashes (no -r) and trimming whitespace (default IFS). The full incantation while IFS= read -r is not superstition; each token disarms one specific mutilation, and dropping one reintroduces exactly that mutilation and no other.

🧪 Exercise 6.7 — the amulets, tested one at a time

The first read mutilates on purpose.

bash
cd ~/bash-course
printf 'C:\\new\\folder\n' > paths.txt
read line < paths.txt
echo "no -r:   [$line]"
read -r line < paths.txt
echo "with -r: [$line]"
Expected result — click to reveal (contains a deliberate mutilation)
plain text
no -r:   [C:newfolder]
with -r: [C:\new\folder]

What to read out of it: without -r, the backslashes were consumed as escape prefixes — \n in the data became n, and nobody was told. With -r, the line arrived byte-for-byte. Any data that can contain backslashes — Windows paths, regexes, JSON fragments — is corrupted by a bare read. There is no situation in modern scripting where you want the unescaping, which is why ShellCheck (SC2162) flags every read without -r: the correct spelling is simply read -r, always.

Part D — Choosing your loop, and loops at fleet scale

D1. The decision tree

Note: the diagram below is a Mermaid code block. Notion does not render it automatically — click the block and switch it from "Code" to "Preview" (or "Split") to see the flowchart.
Diagram source
flowchart TD
    A["I need to repeat something"] --> B{"Driven by what?"}
    B -->|"a known list<br>of words"| C["for x in a b c"]
    B -->|"a range<br>of numbers"| D{"bound known<br>when typed?"}
    D -->|"yes"| E["for i in {1..N}"]
    D -->|"computed"| F["for ((i=1; i<=n; i++))"]
    B -->|"files matching<br>a pattern"| G["for f in *.log<br>+ nullglob or -e guard"]
    B -->|"lines of a file<br>or command output"| H["while IFS= read -r line<br>done < file"]
    B -->|"a condition<br>not yet true"| I["until check; do<br>sleep; done"]
    B -->|"a condition<br>still true"| J["while check; do<br>work; done"]
Now imagine this at 500 hosts. The fleet version of a loop body is ssh "$host" '…' — and three loop realities change scale. Serial is slow: 500 hosts × 2 seconds is 17 minutes; production fans out in controlled batches (& and wait, Module 12, or xargs -P, Module 11 — never unbounded). One failure must not end the round: a down host should be logged and skipped (|| { echo "FAILED: $host" >>failed.txt; continue; }), not allowed to break the loop — and the failed-list becomes the retry input, which is why fleet loops read hostlists with while read (the list is a file) and write a failures file (the next run's input). The loop body must be safe to re-run: reruns happen; bodies built from idempotent steps (mkdir -p, cp to versioned names) make the retry loop boring, which is the highest compliment fleet automation gets.

Interview questions — Parts C–D

🎯 "Write a shell script that uses a while loop to read a file line by line and prints each line." — asked verbatim at FinalRound AI; their companion: "Write a shell script that reads a list of names from a file and prints each name on a new line."

The direct answer:

#!/bin/bash while IFS= read -r line; do printf '%s\n' "$line" done < "$1"

Going deeper: justify every token, because that is what the question actually tests — read's per-line verdict drives while; < "$1" streams the file once; -r preserves backslashes; IFS= preserves edge whitespace; printf '%s\n' rather than echo prints hostile lines faithfully (Module 2's echo caveat). Then name the anti-pattern you did not write: for line in $(cat file) splits on all whitespace and glob-expands — words, not lines.

The details that separate candidates: handling the last line when the file lacks a trailing newline (while IFS= read -r line || [ -n "$line" ] — read returns non-zero but still fills the variable); and reading from a command instead of a file with done < <(command) (process substitution, Module 12) rather than piping into while — because a piped while runs in a subshell and its variables vanish (Module 12's headline gotcha, worth naming a module early).

🎯 "What are the different types of loops in shell scripting?" — asked verbatim (with the 1-to-10 exercise) at FinalRound AI; the deeper cut interviewers add: "when is each the wrong choice?"

The direct answer: for (lists, ranges, globs), while (repeat on success), until (repeat on failure), plus C-style counting — with break/continue steering all of them.

Going deeper than the published version — the wrong-choice map: for is wrong for file lines (splitting) and for unbounded conditions (it needs a finite list up front); while true with a manual break is wrong where until condition says the same thing declaratively; C-style is wrong in #!/bin/sh scripts (bashism — dash rejects it, Module 1 C2's dialect trap again).

The details that separate candidates: the clipboard principle stated once, cleanly — a for-list is fully built by expansion before iteration begins, so mutating the list mid-loop does not change the round — and its corollary, that {1..$n} cannot work (brace expansion precedes variable expansion). Mechanism answers outrank syntax answers on every one of these.

Part E — Production

E1. 🏭 Production practices

These are the practices in production — how the things this module taught are actually done on real systems:

File lines are read with while IFS= read -r, never for $(cat …). The full incantation is the team spelling; ShellCheck (SC2013 for the cat-loop, SC2162 for bare read) enforces it mechanically.

Glob loops declare their empty-match policy. shopt -s nullglob at the top of the script, or [ -e "$f" ] || continue as the body's first line — one of the two, in every glob loop, so a quiet directory can never feed a literal *.log to the body.

Loop bodies are failure-isolated. Fleet rounds log-and-continue per item (do_thing "$host" || { echo "$host" >> failed.txt; continue; }), and the failures file is the next run's input — retries are designed in, not improvised.

Bodies are idempotent. mkdir -p, versioned or timestamped outputs, checks-before-writes — so re-running a partially completed round is always safe.

Counters and math use $(( )), and its integer-only truncation is respected. Percentages, averages, and anything decimal go to awk — bash arithmetic silently floors, and monitoring thresholds computed with floored division have paged people at 3 a.m.

Unbounded waits get bounds. Every until poll carries a max-attempts counter and a sleep — a stuck dependency should fail the script loudly after N tries, not hang it forever (the pattern appears fully built in Module 15).

E2. Production-practice table

SymptomWhat is really happeningWhat to runThe fix
A filename loop processed "two files" that are halves of one real nameThe list came from $(cat …) or $(ls) — word splitting built the clipboardRerun the list-builder through printf '[%s]\n' and count brackets (Module 2's probe)while IFS= read -r for line data; a glob for directory contents
A cleanup loop acted on a literal *.zipThe glob matched nothing and passed through as text — the no-match defaultfor f in *.zip; do echo "[$f]"; done in an empty dir — watch the pattern printshopt -s nullglob, or [ -e "$f" ] || continue first in the body
Script hangs forever, no output, no errorA while/until guard measures something the body no longer changesCtrl-C, then inspect the loop: find the guard's variable, find where the body updates itRestore the update line; add a max-attempts counter to every polling loop
Windows-origin paths in a processed list lost their backslashesread without -r consumed backslashes as escape prefixesprintf 'a\\b\n' | { read x; echo "$x"; } vs the same with read -rread -r, always; let ShellCheck SC2162 make it unforgettable
Indented lines from a config arrived with their leading spaces strippedDefault IFS trimmed whitespace at both ends of each read lineCompare read -r x vs IFS= read -r x on a line with leading spacesThe full spelling: while IFS= read -r line
One dead host ended the whole 500-host round at host 37The body's failing command was the last word — or set -e ended the scriptCheck the loop body for unguarded commands that can fail per-itemPer-item guard: cmd || { log "$host"; continue; }; failures file for retry

E3. 🎓 Capstone — four tickets from the queue

Work each ticket yourself before opening the answer. Everything needed was taught in Modules 1–6.
🎓 Ticket 1 — "Our log-rotation script ran in a freshly provisioned environment and created a compressed archive named *.log.gz. Literally. That is the filename."

Diagnosis. The no-match pass-through (A3): the fresh environment had no logs yet, for f in /var/log/app/*.log iterated once with the literal pattern, and the body faithfully compressed a file it first created by redirecting into "$f".gz. The loop worked exactly as written; the written thing assumed matches always exist.

Work the steps: reproduce in an empty dir: for f in *.log; do echo "[$f]"; done[*.log]. One line, whole bug.

Fix and prevention: shopt -s nullglob at the top (zero matches → zero iterations), or open the body with [ -e "$f" ] || continue. Then delete the impostor file — carefully: it is named with a glob character, so quote it everywhere: rm '/var/log/app/*.log.gz' (unquoted, that rm is a Module 2 globbing incident waiting to happen — on a now-populated directory).

🎓 Ticket 2 — "A migration script reads paths from manifest.txt and copies each to the new volume. QA reports some files landed with mangled names — C:newconfig instead of the documented C:\new\config — and files with spaces threw 'cannot stat' errors in pairs."

Diagnosis. Two mutilations, two missing amulets (C1/C2). The paired cannot-stat errors are word splitting's fingerprint (Module 2 B1) — the loop is for p in $(cat manifest.txt). The eaten backslashes are read without -r — so somewhere a second reader, or the rewrite of the first, dropped the escape guard.

Work the steps: run the manifest through both list-builders with the bracket probe (Exercise 6.6's exact experiment) and compare item counts against wc -l manifest.txt; then Exercise 6.7's -r test on a backslashed line.

Fix and prevention: the canonical reader — while IFS= read -r path; do cp -- "$path" "$dest/"; done < manifest.txt — plus the || [ -n "$path" ] guard if manifests may lack trailing newlines. Prevention is mechanical: ShellCheck flags both the cat-loop (SC2013) and the bare read (SC2162); wire it into CI and this ticket class retires.

🎓 Ticket 3 — "A 'wait for the database' init loop has hung a deployment for 4 hours: until pg_isready -h $DB_HOST; do sleep 5; done. The DB was actually up the whole time."

Diagnosis. Two stacked faults. The hang-forever part is a missing bound (E1's rule): an until with no attempt limit turns any persistent failure into an infinite stakeout. The "DB was up" part means the guard itself was failing for a different reason — with DB_HOST unset (Module 2's silent empty expansion), pg_isready -h with no value errors out with a usage failure — non-zero forever, truth never consulted.

Work the steps: run the guard alone and read its actual complaint: pg_isready -h "$DB_HOST"; echo $? — the stderr line names the missing argument. printf '[%s]\n' "$DB_HOST" confirms the empty jar.

Fix and prevention: validate inputs before looping ([ -n "$DB_HOST" ] || { echo "DB_HOST unset" >&2; exit 2; }); bound the loop (for i in {1..60}; do pg_isready … && break; sleep 5; done with a final "gave up" exit); and distinguish the guard's failure kinds if the tool provides them — Module 3's not-found-vs-couldn't-look discipline applies to readiness probes too.

🎓 Ticket 4 — "Fleet patch round: for host in $(cat hosts.txt); do ssh $host 'apply-patch'; done. It ran for 40 minutes, then stopped at host 212 when one box refused SSH — 288 hosts unpatched, and nobody can say which of the first 211 actually succeeded."

Diagnosis. Every fleet sin from D1's callout in one line: serial round (40 minutes of single-file SSH), no per-item failure isolation (the refused connection's failing verdict ended the round via the script's set -e), no record of outcomes (success and failure both vanished into scrollback), and the hostlist read by the splitting-vulnerable cat-loop (dormant until a hostname needs escaping, but present).

Work the steps: immediate triage needs the missing record — rerun read-only checks in a loop that writes files: while IFS= read -r host; do ssh -o ConnectTimeout=5 "$host" 'check-patch' && echo "$host" >> ok.txt || echo "$host" >> failed.txt; done < hosts.txt.

Fix and prevention: rebuild the round with the production shape: while read over the manifest; per-host || { echo "$host" >> failed.txt; continue; }; outcomes logged to files that feed the retry; a connect timeout so dead hosts cost 5 seconds, not a hang; and batched parallelism when Module 12's wait arrives. The design goal, stated plainly in the runbook: a patch round must be interruptible, resumable, and auditable — three properties, three files, one while-loop.

E4. Documentation reference

TopicAuthoritative referenceWhat you'll find there
for, while, untilBash manual — Looping ConstructsExact syntax and semantics of all three loops, including the C-style for
break, continueBash manual — Bourne Shell BuiltinsBoth steering builtins, including their numeric nested-loop arguments
read and its optionsBash manual — Bash BuiltinsThe read builtin: -r, IFS interaction, exit status at end-of-file
Why the cat-loop splitsBash manual — Word SplittingThe IFS mechanism that dismembers unquoted substitutions
Glob loops and their empty-match behaviorBash manual — Filename ExpansionPattern rules, plus nullglob and friends under "Pattern Matching"

E5. Self-assessment

Answer from memory, out loud or on paper. Every answer is in this module.

  1. What exactly is the list a for loop iterates over — built by what, and when?
  2. Why does {1..$n} fail while for ((i=1; i<=n; i++)) works? Which pipeline fact explains it?
  3. What happens when for f in *.zip finds no zip files, and what are the two production defenses?
  4. Why are glob-built lists safe for filenames with spaces while $(ls)-built lists are not?
  5. What does $((n-1)) do, and what does bash arithmetic silently do to 7/2?
  6. A while loop hangs forever. What is the first structural question to ask about its guard and body?
  7. while vs until — one sentence each, and the canonical use of until in deployments.
  8. break vs continue — and what does continue placed above the counter-update line risk?
  9. Perform the autopsy on for f in $(cat files.txt): name each rewriting step and what it does to a line containing a space.
  10. In while IFS= read -r line; do … done < file — justify all four unusual tokens (IFS=, -r, the read-as-guard, the trailing < file).
  11. What happens to a file's last line if it lacks a trailing newline, and what is the guard?
  12. Name the three properties a fleet patch round must have, and the file-based pattern that provides them.

E6. Sources

Interview questions in this module were captured verbatim from:

Edureka — Top 60 Shell Scripting Interview Questions and Answers — "Write down the Syntax for all the loops in Shell Scripting.", "What is the difference between break and continue commands?" (page updated Dec 9, 2024)

Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "How do you use loops in Bash?", "How do you use loops to iterate over files in a directory in Bash?" (published June 18, 2026)

FinalRound AI — 25 Essential Shell Scripting Interview Questions You Need to Know — "What are the different types of loops in shell scripting? Write a script using a for loop to print numbers 1 to 10.", "Write a shell script that uses a while loop to read a file line by line and prints each line.", "Write a shell script that reads a list of names from a file and prints each name on a new line." (page updated April 2, 2025)

KnowledgeHut — Shell Scripting Interview Questions and Answers — "How do you use loops (e.g., for, while) in a shell script?" (no publication date shown on page)

A note on the corpus: loop syntax and file-reading are richly represented in published question lists; the specific traps this module dwells on — the unmatched-glob pass-through and the missing -r/IFS= amulets — are asked about in real interviews mostly as follow-ups ("what breaks if…?") rather than as published questions, and the module treats them at the depth the follow-ups demand. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2.

Next: your loops still work on hard-coded lists and filenames. Real tools take their targets from the command line — ./deploy.sh staging --dry-run — and that machinery (positional parameters, $@, shift, getopts, read prompts) is Module 7 — Script Arguments and User Input.
Spotted a mistake or want something added? Send me a note.