Loops: Automating Repetition
- Explain what a loop buys you — one piece of logic applied to many things — and pick for vs while by shape
- Write for loops over lists, ranges, command output, and files
- Use while loops, including reading a file line by line the correct way
- Control loops with break and continue
- Apply loops to real batch tasks against real files
Why loops exist: write once, apply to many
Build the mental model — a loop is an assembly line, and the two loop types differ by what ends them
Picture the task that makes people learn scripting in the first place: "compress every log older than a week — on all forty of them." Without a loop you'd type the same command forty times, changing one filename each time. Typos creep in around file twelve; boredom-induced mistakes around twenty. The loop is the fix, and it's the heart of all automation: you write the logic once, and the shell applies it to each item in a list.
Think of it as an assembly line. Items arrive one at a time — filenames, numbers, server names, lines of a file. Your loop body is the workstation: each item stops, gets processed with the same steps, and moves on. The variable (
$file,$i,$host) is just the name of whatever is currently on the belt.Bash gives you two loop shapes, and choosing between them is a one-question decision — do you know the list up front?
- `for` — "do this for each of THESE." You have the items already: five filenames, the numbers 1–10, three hostnames. The loop ends when the list runs out.
- `while` — "keep doing this UNTIL something changes." You don't know how many rounds: retry until a service answers, read lines until the file ends, count until a threshold. The loop ends when its condition — the same exit-code test you learned with
if— goes false.
That's the whole taxonomy. Everything in this lesson is one of those two shapes wearing different clothes.
Check yourself
You're writing a script that pings a host every 5 seconds until it responds. for or while — and why?
The for loop
Iterate a literal list, a range, and command output with one syntax
The syntax reads almost like English:
for fruit in apple banana cherry; do ... done. Bash sets$fruitto the first item, runs the body, sets it to the second, runs it again — until the list is exhausted.doanddonefence the body the waythenandfifence anif.The power move is realizing how many things can be the list: literal words, a numeric range like
{1..5}(brace expansion), the output of a command via$(...), or — next step — filename globs.for fruit in apple banana cherry; do echo "I like $fruit" doneI like apple I like banana I like cherry
Three items, three trips through the body. On each trip $fruit holds the current item — nothing more mysterious than that. This literal-list form is what you'll use for small fixed sets: three services to restart, two directories to back up. for i in {1..3}; do echo "attempt $i"; done for user in $(cut -d: -f1 /etc/passwd | head -3); do echo "account: $user"; doneattempt 1 attempt 2 attempt 3 account: root account: bin account: daemon
- d:
- (cut) split fields on the colon delimiter — /etc/passwd's separator
- f1
- (cut) keep field 1, the username
Two more list sources. {1..3} expands to 1 2 3 before the loop even starts ({1..10..2} would step by 2). The second loop's list is command output: $(...) runs the pipeline, and its words become the items. Real usernames from the real passwd file — the loop doesn't care where the list came from. Check yourself
What brace-expansion writes the numbers 1 through 5 for a for loop?
Loop over files — the pattern you'll use most
Process every file matching a glob, safely, including the no-match trap
Here's the workhorse. Give
fora filename glob and the shell expands it into the list of matching paths — every.login a directory, every.confunder/etc. This is the forty-log-files problem from the intro solved in one line.Two safety habits make the difference between a pattern you can trust and one that burns you, and both are visible in the next command. First: quote `"$file"` everywhere you use it — filenames with spaces are legal, and an unquoted variable splits into pieces. Second: guard against the no-match case, which is stranger than you'd expect.
for file in /var/log/*.log; do [ -e "$file" ] || continue echo "$file is $(du -h "$file" | cut -f1)" done/var/log/boot.log is 8.0K /var/log/dnf.log is 124K /var/log/lastlog is 12K
- h
- (du) human-readable size — 124K instead of a raw block count
- e
- (test) true if the path exists — any type
The glob expands to every matching path and the body reports each one's human-readable size. The guard line is doing quiet, important work: if a path doesn't actually exist, skip to the next item. Why would it not exist? That's the trap below. Check yourself
Your loop is
for f in /data/*.csv; do rm "$f"; doneand /data has no CSV files. What does the loop actually try to delete?while loops — repeat until the world changes
Loop on a condition, and read a file line by line the correct way
whilereuses everything you learned about conditionals:while CONDITION; do ... donere-runs the body as long as the condition's exit code is 0. Anything that can followifcan followwhile— a[ ]test, agrep -q, aping -c1. The body's job is usually to change something the condition checks, or you've written an infinite loop.The most important
whilein all of shell scripting, though, is the one that reads a file line by line — and it's worth learning its exact incantation, because the "obvious" alternative is a famous bug.count=1 while [ "$count" -le 3 ]; do echo "count $count" ((count++)) donecount 1 count 2 count 3
The anatomy: the condition tests $count, the body increments it with ((count++)) — Bash's arithmetic syntax — and after three trips the -le test fails and the loop ends. Delete the increment line and you've built the classic infinite loop: the condition checks a value nothing ever changes (Ctrl+C rescues you). while IFS= read -r line; do echo "got: $line" done < /etc/hostnamegot: web01.example.com
- r
- (read) raw mode — backslashes in the line stay literal instead of escaping things
- IFS=
- empty field separator for this read — keeps the line's whitespace untouched
THE way to read a file line by line. read pulls one line per iteration and exits non-zero at end-of-file — which is what ends the loop. The < /etc/hostname at the bottom feeds the whole loop from the file. IFS= preserves leading/trailing whitespace exactly, and -r stops backslashes being interpreted. For CSV, set the separator and name multiple fields: while IFS=',' read -r name email; do ... Check yourself
What's the safe construct to read a file one line at a time?
Control the loop: break and continue
Exit early or skip an item without restructuring the loop
Real batch jobs aren't uniform: some items should be skipped (a file that fails validation), and sometimes the whole job should stop early (you found what you were searching for). Two keywords handle both without touching the loop's structure:
- `continue` — "done with THIS item, next please." Skips the rest of the body for the current item.
- `break` — "done with the WHOLE loop." Exits immediately; execution resumes after
done.
You've already used
continueonce — the glob guard[ -e "$f" ] || continueis exactly this: item's no good, next.for i in {1..6}; do if [ $((i % 2)) -eq 0 ]; then continue; fi if [ "$i" -eq 5 ]; then break; fi echo "odd: $i" done echo "loop finished"odd: 1 odd: 3 loop finished
Trace it item by item: 1 is odd → printed. 2 is even → continue skips it. 3 → printed. 4 → skipped. 5 → break kills the loop before printing, and 6 is never visited at all. The echo after done proves execution lands there. This trace-by-hand exercise is worth doing once slowly — break/continue bugs are almost always a mis-predicted trace. Check yourself
A loop searches 500 log files for the first one containing "FATAL". Once found, checking the rest is wasted work. Which keyword stops the loop — break or continue?
Now do it — batch-process real files in your live terminal
Run each loop shape against real data and trigger the no-match trap on purpose
Loops only become instinct through your own fingers. You'll run all three shapes — a range
for, a globforover real system files, and a line-readingwhile— then spring the unmatched-glob trap deliberately so you recognize it forever.for i in {1..5}; do echo "square of $i is $((i*i))"; doneThe range loop with arithmetic — $((...)) does the math. Change the range to {2..10..2}, or the operation to iii, and predict the output before you press Enter. Predicting-then-checking is the fastest way to calibrate your mental model. for f in /etc/*.conf; do [ -e "$f" ] || continue; echo "$(basename "$f"): $(wc -l < "$f") lines"; done | headThe glob loop doing genuinely useful work: every .conf in /etc, each reported with its line count. Note the guard is present even though /etc certainly has .conf files — it costs nothing and the habit pays off the day the glob is emptier than you assumed. mkdir -p /tmp/loop-lab && cd /tmp/loop-lab for f in *.nothing; do echo "item: $f"; doneThe trap, on your terms: an empty directory, a glob that matches nothing — and the loop still prints 'item: *.nothing'. That literal string is what your rm or gzip would have received. Now add the guard ([ -e "$f" ] || continue) and run it again: silence. That before/after is the lesson. printf "web01\ndb01 primary\ncache02\n" > hosts.txt while IFS= read -r host; do echo "checking: [$host]"; done < hosts.txtBuild a three-line inventory (one entry contains a space) and read it back line by line. The brackets in the output prove each line arrived whole — 'db01 primary' stays one item. Now try the broken form, for h in $(cat hosts.txt), and watch it split into four fragments. Seeing both makes the rule stick.
- A loop applies one body to many items — write the logic once, review it once, trust it everywhere. Choose by shape: for when you have the list, while when a condition decides.
- for VAR in LIST; do ... done — the list can be literal words, a {1..5} range, $(command) output, or a glob; always quote "$VAR" in the body.
- An unmatched glob stays literal (.log arrives as the string .log) — guard file loops with [ -e "$f" ] || continue, or shopt -s nullglob.
- Read files with while IFS= read -r line; do ...; done < file — never for line in $(cat file), which splits on every space, not on lines.
- continue skips the current item, break exits the whole loop; iterate arrays with "${arr[@]}" to keep elements with spaces intact.
| Need | Form |
|---|---|
| Fixed list / range | for x in a b c · for i in {1..5} |
| Index math | for ((i=0;i<n;i++)) |
| Each matching file | for f in *.log; do [ -e "$f" ] || continue |
| Repeat until condition | while [ cond ]; do ... done |
| Read a file by lines | while IFS= read -r l; do ...; done < f |
| Skip item / stop loop | continue · break |
| Fixed roster | for s in "${servers[@]}" |
