Module 9 · Shell scripting basics
Loops: while and until
Loop while a condition holds or until it does, count with a variable, read a file line by line and avoid the subshell trap that eats your totals.
What you will learn
- Write `while` and `until` loops with a counter.
- Read a file line by line with `while read -r line; do …; done < file`.
- Explain why variables set inside `cmd | while read` are lost.
for needs its list up front. while does not: it runs a condition command, and as long as that command succeeds, it runs the body again. until is the mirror image, looping as long as the condition fails. Both end with done, and the condition is any command, usually [ ].
~% cat lab/l86/countdown.sh
#!/bin/sh
n=5
while [ "$n" -gt 0 ]; do
echo "$n"
n=$(( n - 1 ))
done
echo "Liftoff"
~% lab/l86/countdown.sh
5
4
3
2
1
Liftoff
~% n=0; until [ -e /tmp/ready ]; do n=$(( n + 1 )); [ "$n" -ge 3 ] && touch /tmp/ready; done; echo "waited $n turns"
waited 3 turns
Every while needs something in the body that eventually makes the condition false, here n=$(( n - 1 )). Forget it and the loop never ends; press Ctrl+C to kill it. Infinite loops on purpose are written while true; do …; done or while :; do …; done with a break inside, for example to retry a command until it works, with a sleep 1 between attempts so you do not burn the CPU.
Reading a file line by line
The most useful while in shell programming is while read -r line; do …; done < file. read takes one line from its input, stores it in line, and succeeds; at end of file it fails and the loop stops. The -r keeps backslashes literal. The redirection goes after done, so the whole loop reads from the file. Give read several names and the line is split on spaces: read -r name age puts the first word in name and the rest in age; set IFS=: in front to split on colons instead, handy for /etc/passwd.
~% cat lab/l86/numbers.txt
10
20
30
~% total=0
~% while read -r n; do total=$(( total + n )); done < lab/l86/numbers.txt
~% echo "total=$total"
total=60
~% total=0
~% cat lab/l86/numbers.txt | while read -r n; do total=$(( total + n )); done
~% echo "total=$total"
total=0
One more subtlety: read fails on the last line if the file does not end with a newline, so that line is silently skipped. Files you create with printf '…\n' or vi are fine; for files of unknown origin write while read -r l || [ -n "$l" ]; do to process the leftover.
| Loop | Runs while | Typical use |
|---|---|---|
for x in list | items remain | Files, fixed lists, seq |
while cmd | cmd succeeds | Counters, reading lines, retries |
until cmd | cmd fails | Waiting for a file or a service |
Commands in this lesson
| Command | What it does |
|---|---|
while [ cond ]; do …; done | Repeat while the test succeeds. |
until [ cond ]; do …; done | Repeat until the test succeeds. |
while read -r line; do …; done < file | Process a file line by line. |
IFS=: read -r a b c | Split the line on colons. |
while true; do …; break; done | Loop until an explicit break. |
sleep 1 | Pause between retries. |
Quiz
When does a `while` loop stop?
- When its condition command fails (non-zero status)
- After ten iterations
- When the body prints something
Where does the input redirection go in a line-reading loop?
- After `read`: `while read -r l < file; do`
- After `done`: `done < file`
- After `while`: `while < file read -r l`
`cat f | while read -r n; do t=$((t+n)); done; echo $t` prints an empty value. Why?
- `read` cannot read numbers
- The loop ran in a subshell because of the pipe, so `t` died with it
- `$((…))` is not allowed inside loops
What does `-r` do in `read -r line`?
- Reads the file in reverse
- Keeps backslashes literal instead of treating them as escapes
- Removes the trailing newline
Practice
Write an executable script `/root/lab/l86/countdown.sh` that uses a `while` loop to print 5, 4, 3, 2, 1 on separate lines and then `Liftoff`.
Write an executable script `/root/lab/l86/hello.sh` that reads `/root/lab/l86/names.txt` line by line and prints `Hello, NAME` for each line.
Write an executable script `/root/lab/l86/total.sh` that adds up the numbers in `/root/lab/l86/numbers.txt` (one per line) and prints only the total. Remember the subshell trap: feed the loop with `<`, not a pipe.
Open this lesson in the app to do the tasks in a real Linux machine and have them checked.