| |

Bash Scripting 11 ๐Ÿง While & Until Loops โ€” Processing conditional loops and line reading

โฑ๏ธ Estimated read: 13 min | |

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

LoopCondition True WhenRuns While
while cmd; do ... donecmd returns 0Condition succeeds
until cmd; do ... donecmd returns 0Condition fails

The Read Pattern

ElementPurpose
IFS=Disable word splitting
-rDisable backslash interpretation
lineVariable that receives the line
< fileRedirect file into the loop
-u NRead from file descriptor N

Loop Control

CommandEffect
breakExit the loop immediately
continueSkip to the next iteration
break NExit N nested loops
continue NContinue the Nth enclosing loop

Comparison

ConstructUse Case
for x in listIterate over a known list
while readProcess a file line by line
while conditionLoop while a condition holds
until conditionLoop until a condition holds
C-style forNumeric 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

PitfallWhy It HappensFix
Variable lost after the loopUsed a pipelineRedirect into the loop
Whitespace strippedIFS not clearedIFS= read -r line
Backslash interpreted-r not usedread -r line
Infinite loopNo increment or condition changeIncrement inside the body
Last line not processedFile has no trailing newlineread still assigns the last line; verify the condition
Loop reads from stdin unexpectedlyBody uses read without a different fdUse -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

ItemValue
while conditionRuns while the command returns 0
until conditionRuns while the command returns non-zero
Line inputwhile IFS= read -r line; do ...; done < file
IFS=Disable word splitting
-rDisable backslash interpretation
File redirection< file keeps the loop in the current shell
Alternative fd-u 3 and 3< file
Loop controlbreak, continue
Pipeline trapcat file | while runs in a subshell

Key takeaways:

  • while and until are mirror images. while runs while the condition succeeds; until runs while the condition fails. The condition is a command, and the loop branches on its exit status .
  • while IFS= read -r line is the correct line-reading pattern. The IFS= disables word splitting, the -r disables backslash interpretation, and the file is redirected into the loop with < file to keep the loop in the current shell.
  • A pipeline runs the loop in a subshell. cat file | while read line modifies variables in a subshell, and the parent shell does not see the changes. Redirect the file into the loop instead.
  • until is the natural retry construct. until ping -c 1 host; do sleep 2; done retries until the command succeeds. The while ! cmd form is equivalent but less readable .
  • break and continue work in all loop types. break exits immediately, and continue skips to the next iteration. The optional numeric argument operates on nested loops.
  • read -u N allows 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.”


Was this helpful?


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!