Bash Scripting 11 ๐ง While & Until Loops โ Processing conditional loops and line reading
A while loop repeats a command list as long as a condition is true. An until loop repeats it as long as a condition is false. The two constructs are mirror images: while runs while the condition succeeds, and until runs while the condition fails. Both evaluate the condition before every iteration, so a loop whose condition is false (for while) or true (for until) at the start never runs its body. The previous chapter on for loops covered iteration over a known list; this chapter covers iteration over a conditionโthe pattern used to read a file line by line, to retry an operation until it succeeds, to poll for a resource, and to process input until an end-of-file marker.
The most important use of the while loop in shell scripting is line-oriented input. The for loop splits on whitespace and is the wrong construct for files where each line is a record. The while IFS= read -r line pattern reads one line per iteration, preserves leading and trailing whitespace, and does not interpret backslashes. It is the correct way to process a file line by line, and it is the pattern that this chapter establishes before covering the other conditional loops.
Key point: The while and until conditions are commands, not boolean expressions. The loop runs while the command returns 0 (success) for while, and while it returns non-zero (failure) for until. The read command is the standard condition for line-oriented input: it returns 0 while it can read a line and a non-zero status at end-of-file. Always use IFS= read -r to preserve whitespace and backslashes, and always redirect the file into the loop with < file rather than piping it, to avoid running the loop in a subshell.
Why conditional loops require attention to subshells
The pipeline-subshell problem.
cat file | while read line; do ...; done runs the while loop in a subshell, because each segment of a pipeline runs in its own process. A variable that is modified inside the loop is not visible after the pipeline ends, because the modification happened in the subshell. The correct form redirects the file into the loop: while read line; do ...; done < file. The loop runs in the current shell, and the variables it modifies are visible afterward .
The IFS problem.
The read command splits the line into fields using the IFS variable. By default, IFS includes space, tab, and newline, so leading and trailing whitespace is stripped, and the line is split into multiple words. Setting IFS= for the read command disables the splitting, and the entire line is stored in the variable, including the whitespace . The -r flag disables the backslash interpretation, so a backslash in the input is read literally instead of escaping the next character.
The read-returns-nonzero-on-final-line problem.
read returns a non-zero status when it reaches end-of-file, but it still assigns the last line to the variable before returning. A loop while read line; do ...; done < file processes every line correctly, because the condition is evaluated after the assignment. The loop terminates on the iteration after the last line, when read returns non-zero and the body is not executed. This is the correct behavior, but it means that a file without a trailing newline still has its last line processed.
a. The while loop and line-oriented input
The while loop evaluates its condition command before each iteration. The read command is the standard condition for line-oriented input.
#!/usr/bin/env bash
# Read a file line by line, preserving whitespace
while IFS= read -r line; do
echo "Line: $line"
done < /etc/passwd
The IFS= read -r line condition reads one line from the redirected file into the line variable. The loop body executes while the read succeeds. The IFS= prefix sets the field separator to empty for the read command only, so the entire line is stored without splitting. The -r flag prevents backslash interpretation . The < /etc/passwd redirects the file into the loop, so the loop runs in the current shell, not a subshell.
The while loop also works with a counter and an arithmetic condition.
# Count from 0 to 4
count=0
while [ "$count" -lt 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
The condition [ "$count" -lt 5 ] is a test command that returns 0 while the count is less than five. The body increments the count. The loop terminates when the count reaches five and the test returns non-zero .
b. The until loop and retry logic
The until loop is the mirror of while. It runs the body while the condition command returns non-zero (failure). It is the natural construct for “retry until success.”
#!/usr/bin/env bash
# Retry a command until it succeeds or the attempts are exhausted
attempts=0
max_attempts=5
until ping -c 1 -W 1 192.168.1.1 > /dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge "$max_attempts" ]; then
echo "Host unreachable after $max_attempts attempts" >&2
exit 1
fi
echo "Attempt $attempts failed; retrying in 2 seconds..." >&2
sleep 2
done
echo "Host is reachable"
The until ping ... condition runs the ping command. The loop body executes while ping fails. The body increments the attempt counter, checks the maximum, and sleeps before the next attempt. The loop terminates when ping succeeds, and the success message is printed after the loop . The until form is clearer than while ! ping ... because the negation is in the construct rather than in the condition.
The until loop also works with a command that creates a resource.
# Wait for a file to appear
until [ -f /tmp/ready ]; do
echo "Waiting for /tmp/ready..."
sleep 1
done
echo "File appeared"
The condition [ -f /tmp/ready ] returns non-zero while the file does not exist. The loop body sleeps and re-checks. When the file appears, the condition returns zero and the loop terminates.
c. Loop control and the read loop with file descriptors
The break and continue commands work in while and until loops the same way they work in for loops. break exits the loop immediately, and continue skips to the next iteration.
#!/usr/bin/env bash
# Skip comments and blank lines
while IFS= read -r line; do
# Skip blank lines
[ -z "$line" ] && continue
# Skip comments
[[ "$line" == \#* ]] && continue
echo "Config: $line"
done < config.txt
The continue command skips the rest of the body for blank lines and comment lines. The -z test checks for an empty string, and the [[ "$line" == \#* ]] test checks for a leading # . The remaining lines are processed.
The read command can read from a file descriptor other than stdin. This is useful when the loop body also reads from stdin, such as a loop that prompts the user for input on each iteration.
# Read from file descriptor 3, leaving stdin for the body
while IFS= read -r -u 3 line; do
echo "Processing: $line"
read -r -p "Continue? (y/n) " answer
[ "$answer" = "n" ] && break
done 3< config.txt
The -u 3 flag tells read to read from file descriptor 3 instead of stdin. The 3< config.txt redirects the file into descriptor 3. The read -r -p inside the body reads from stdin, which is still connected to the terminal . This pattern is necessary when the loop needs to both process a file and interact with the user.
Complete Example Session
#!/usr/bin/env bash
# ============================================
# PART 1: THE BASIC WHILE LOOP
# ============================================
count=0
while [ "$count" -lt 3 ]; do
echo "Iteration $count"
count=$((count + 1))
done
# ============================================
# PART 2: LINE-BY-LINE FILE PROCESSING
# ============================================
while IFS= read -r line; do
echo "Read: $line"
done < /etc/hostname
# ============================================
# PART 3: SKIPPING BLANK LINES AND COMMENTS
# ============================================
while IFS= read -r line; do
[ -z "$line" ] && continue
[[ "$line" == \#* ]] && continue
echo "Config: $line"
done < /etc/hosts
# ============================================
# PART 4: THE UNTIL LOOP
# ============================================
attempt=1
until [ "$attempt" -gt 3 ]; do
echo "Attempt $attempt"
attempt=$((attempt + 1))
done
# ============================================
# PART 5: RETRY UNTIL SUCCESS
# ============================================
retries=0
until curl -sf http://localhost > /dev/null; do
retries=$((retries + 1))
if [ "$retries" -ge 5 ]; then
echo "Service unreachable" >&2
exit 1
fi
sleep 2
done
echo "Service is up"
# ============================================
# PART 6: THE BREAK COMMAND
# ============================================
while IFS= read -r line; do
if [ "$line" = "STOP" ]; then
break
fi
echo "Processing: $line"
done < input.txt
# ============================================
# PART 7: THE CONTINUE COMMAND
# ============================================
while IFS= read -r line; do
[[ "$line" == *"skip"* ]] && continue
echo "Kept: $line"
done < input.txt
# ============================================
# PART 8: READING FROM A DIFFERENT FD
# ============================================
while IFS= read -r -u 3 line; do
echo "Line: $line"
done 3< /etc/hostname
# ============================================
# PART 9: THE READ LOOP WITH A COUNTER
# ============================================
line_number=0
while IFS= read -r line; do
line_number=$((line_number + 1))
echo "$line_number: $line"
done < /etc/passwd
The nine parts demonstrate the conditional loop patterns: the basic while, the line-by-line read, the skip logic, the basic until, the retry loop, the break, the continue, the file-descriptor read, and the line counter. The critical pattern is Part 2: the IFS= read -r with the file redirected into the loop, which preserves the line content and keeps the loop in the current shell.
Quick Reference
Loop Syntax
| Loop | Condition True When | Runs While |
|---|---|---|
while cmd; do ... done | cmd returns 0 | Condition succeeds |
until cmd; do ... done | cmd returns 0 | Condition fails |
The Read Pattern
| Element | Purpose |
|---|---|
IFS= | Disable word splitting |
-r | Disable backslash interpretation |
line | Variable that receives the line |
< file | Redirect file into the loop |
-u N | Read from file descriptor N |
Loop Control
| Command | Effect |
|---|---|
break | Exit the loop immediately |
continue | Skip to the next iteration |
break N | Exit N nested loops |
continue N | Continue the Nth enclosing loop |
Comparison
| Construct | Use Case |
|---|---|
for x in list | Iterate over a known list |
while read | Process a file line by line |
while condition | Loop while a condition holds |
until condition | Loop until a condition holds |
C-style for | Numeric iteration with an index |
Best Practices
โ Do This:
# Use IFS= read -r for line input
while IFS= read -r line; do echo "$line"; done < file # โ
# Redirect the file into the loop, not a pipe
while read line; do ...; done < file # โ
# Use until for retry logic
until ping -c 1 host; do sleep 2; done # โ
# Use continue to skip lines
[[ "$line" == \#* ]] && continue # โ
# Use -u 3 when the body also reads from stdin
while IFS= read -r -u 3 line; do ...; done 3< file # โ
โ Don’t Do This:
# Use a pipeline; the loop runs in a subshell
cat file | while read line; do ...; done # โ
# Omit IFS=
while read line; do ...; done < file # Strips whitespace # โ
# Omit -r
while read line; do ...; done < file # Interprets \ # โ
# Use while ! for retry
while ! ping -c 1 host; do ...; done # Correct but less clear # โ
(works, but until is clearer)
# Forget the increment
while [ "$i" -lt 5 ]; do echo "$i"; done # Infinite loop # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Variable lost after the loop | Used a pipeline | Redirect into the loop |
| Whitespace stripped | IFS not cleared | IFS= read -r line |
| Backslash interpreted | -r not used | read -r line |
| Infinite loop | No increment or condition change | Increment inside the body |
| Last line not processed | File has no trailing newline | read still assigns the last line; verify the condition |
| Loop reads from stdin unexpectedly | Body uses read without a different fd | Use -u 3 for the file |
Real-World Examples
1. Count from 0 to 4
i=0; while [ "$i" -lt 5 ]; do echo "$i"; i=$((i+1)); done
2. Read a file line by line
while IFS= read -r line; do echo "$line"; done < file
3. Skip comments
while IFS= read -r line; do [[ "$line" == \#* ]] && continue; echo "$line"; done < file
4. Retry until success
until ping -c 1 host; do sleep 2; done
5. Wait for a file
until [ -f /tmp/ready ]; do sleep 1; done
6. Break on a marker
while IFS= read -r line; do [ "$line" = "STOP" ] && break; echo "$line"; done < file
7. Read from fd 3
while IFS= read -r -u 3 line; do echo "$line"; done 3< file
8. Line counter
n=0; while IFS= read -r line; do n=$((n+1)); echo "$n: $line"; done < file
9. Numeric loop with while
n=10; while [ "$n" -gt 0 ]; do echo "$n"; n=$((n-1)); done
10. Retry with a limit
tries=0; until cmd; do tries=$((tries+1)); [ "$tries" -ge 5 ] && exit 1; sleep 2; done
Visual
While vs Until
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MIRROR IMAGES โ
โ โ
โ while condition; do โ
โ body โ
โ done โ
โ โ Runs while condition returns 0 (success) โ
โ โ
โ until condition; do โ
โ body โ
โ done โ
โ โ Runs while condition returns non-zero (failure) โ
โ โ
โ while ! condition โก until condition โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Line-Reading Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE read CONDITION DRIVES THE LOOP โ
โ โ
โ while IFS= read -r line; do ...; done < file โ
โ โ โ
โ โผ โ
โ read one line into $line โ
โ โ โ
โ โโโ Success (line read) โ run body โ loop again โ
โ โ โ
โ โโโ Failure (EOF) โ exit loop โ
โ โ
โ The last line is assigned before read returns EOF. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Pipeline Subshell Trap
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ cat file | while read line โ LOOP RUNS IN A SUBSHELL โ
โ โ
โ count=0 โ
โ cat file | while read line; do โ
โ count=$((count + 1)) โ modified in the subshell โ
โ done โ
โ echo "$count" โ 0 (the parent shell's count is unchanged)โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ count=0 โ
โ while read line; do โ
โ count=$((count + 1)) โ
โ done < file โ
โ echo "$count" โ correct count (loop runs in current shell)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
while condition | Runs while the command returns 0 |
until condition | Runs while the command returns non-zero |
| Line input | while IFS= read -r line; do ...; done < file |
IFS= | Disable word splitting |
-r | Disable backslash interpretation |
| File redirection | < file keeps the loop in the current shell |
| Alternative fd | -u 3 and 3< file |
| Loop control | break, continue |
| Pipeline trap | cat file | while runs in a subshell |
Key takeaways:
whileanduntilare mirror images.whileruns while the condition succeeds;untilruns while the condition fails. The condition is a command, and the loop branches on its exit status .while IFS= read -r lineis the correct line-reading pattern. TheIFS=disables word splitting, the-rdisables backslash interpretation, and the file is redirected into the loop with< fileto keep the loop in the current shell.- A pipeline runs the loop in a subshell.
cat file | while read linemodifies variables in a subshell, and the parent shell does not see the changes. Redirect the file into the loop instead. untilis the natural retry construct.until ping -c 1 host; do sleep 2; doneretries until the command succeeds. Thewhile ! cmdform is equivalent but less readable .breakandcontinuework in all loop types.breakexits immediately, andcontinueskips to the next iteration. The optional numeric argument operates on nested loops.read -u Nallows the body to use stdin. When the loop reads from file descriptor 3, the body can read from stdin without interfering with the loop’s file input.
Remember: Conditional loops run on exit status. The while loop runs while a command succeeds, and the until loop runs while it fails. The read command is the standard condition for line-oriented input, and the IFS= read -r pattern is the correct way to preserve the line’s content. Redirect the file into the loop, not through a pipe, or the loop runs in a subshell and loses its variable changes. Use break and continue for loop control, and use until for retry logic. The loop is the construct that processes input until there is no more input, and the condition is what defines “no more.”
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!