while and until Loops
A while loop repeats a block of commands for as long as a condition keeps succeeding; an until loop does the opposite, repeating for as long as a condition keeps failing. Unlike a for loop, which walks through a fixed list of items, while and until are driven by a live condition that is re-checked every time through the loop — which makes them the right tool whenever you don’t know in advance how many times you’ll need to repeat something, such as reading a file until it runs out of lines, or retrying a command until it succeeds.
Overview: How while and until Loops Work
In Bash, “condition” doesn’t mean a boolean the way it does in most programming languages — it means a command. Every command that finishes in Linux returns an exit status: a number from 0 to 255 stored by the shell in the special variable $?. By convention, 0 means success and anything non-zero means failure. This isn’t enforced by the kernel; it’s just a convention that every well-behaved program follows, and it’s the entire mechanism that while and until are built on.
A while loop runs its body as long as its condition command exits with status 0 (true). An until loop runs its body as long as its condition command exits non-zero (false) — it stops the moment the condition finally succeeds. They are mirror images of each other, and anything written with until can be rewritten with while by negating the condition (with !), and vice versa. Most Bash code uses while; until exists purely for readability in cases where “keep going until X becomes true” reads more naturally than “keep going while X is false”.
Before each pass through the loop body, Bash runs the condition command fresh, waits for it to exit, and inspects its exit status. If the condition is a builtin or keyword like [[ ... ]], no new process is created — the shell evaluates it internally. If the condition is an external command, such as ping or grep, the shell forks a new child process for every single check, which is worth remembering if you’re looping thousands of times and care about performance. Because the condition is re-evaluated every pass, the loop body is responsible for eventually making the condition false (for while) or true (for until) — forgetting to do that is the single most common bug with these loops, covered below.
One of the most important uses of while in real scripts is reading input line by line, usually with while read -r line; do ... done < file. Here the “condition” is the read builtin itself: it returns exit status 0 each time it successfully reads a line, and returns non-zero once it hits end-of-file, which naturally ends the loop. This pattern is how Bash scripts process log files, config files, and command output without needing a separate iteration construct.
Syntax
while condition-command; do
commands
done
until condition-command; do
commands
done
| Part | Meaning |
|---|---|
while |
Run the body repeatedly as long as condition-command exits with status 0. |
until |
Run the body repeatedly as long as condition-command exits with a non-zero status. |
condition-command |
Any command or pipeline — commonly [[ expression ]], (( arithmetic )), read, or an external command like ping or grep. |
do / done |
Delimit the loop body, the same way { } would in C-family languages. |
break |
Exits the loop immediately, skipping any remaining iterations. |
continue |
Skips the rest of the current iteration and jumps straight back to re-checking the condition. |
Examples
Example 1: a simple counting loop
#!/usr/bin/env bash
count=1
while [[ $count -le 5 ]]; do
echo "Count: $count"
count=$((count + 1))
done
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
The condition [[ $count -le 5 ]] is checked before every iteration. Each pass prints the current value and then increments count by 1. Once count reaches 6, the condition becomes false and the loop stops. If the count=$((count + 1)) line were missing, count would stay at 1 forever and the loop would never end.
Example 2: reading a file line by line
#!/usr/bin/env bash
log_file="/var/log/app.log"
line_number=1
while IFS= read -r line; do
echo "$line_number: $line"
line_number=$((line_number + 1))
done < "$log_file"
1: 2026-08-04 09:12:01 INFO server started on port 8080
2: 2026-08-04 09:12:03 INFO connected to database
3: 2026-08-04 09:14:47 WARN slow query took 812ms
The < "$log_file" redirection at the end feeds the file into the loop's standard input, and read -r line pulls one line at a time into the variable line, returning success until the file is exhausted. IFS= stops read from trimming leading and trailing whitespace, and -r stops it from treating backslashes in the file as escape characters — both are standard habits when reading real-world files.
Example 3: until loop for retry logic
#!/usr/bin/env bash
attempt=1
max_attempts=5
until ping -c 1 -W 1 "server1.example.com" &> /dev/null; do
if (( attempt >= max_attempts )); then
echo "Giving up after $max_attempts attempts." >&2
exit 1
fi
echo "Attempt $attempt: server unreachable, retrying..."
attempt=$((attempt + 1))
sleep 2
done
echo "Server is up!"
Attempt 1: server unreachable, retrying...
Attempt 2: server unreachable, retrying...
Server is up!
Here until reads naturally: "keep retrying until the ping succeeds." The condition is the ping command itself — its output is thrown away with &> /dev/null since only its exit status matters. The loop body only runs while ping keeps failing, and it stops as soon as ping exits with status 0.
How It Works Step by Step
Walking through Example 2's while IFS= read -r line; do ... done < "$log_file":
- Bash opens
/var/log/app.logfor reading and connects it to the loop's standard input via the<redirection — this happens once, before the loop starts. - The condition,
read -r line, reads characters from that input until it hits a newline, strips the newline, and stores the result inline. If it read at least a partial line, it exits with status0. - Because the exit status is
0, Bash enters the loop body: it prints the line number and content, then incrementsline_number. - Control returns to the top of the loop, and
read -r lineruns again on the next line. - When there is nothing left to read,
readreturns a non-zero exit status, thewhilecondition is false, and Bash exits the loop, moving on to whatever comes afterdone.
The same evaluate-then-branch cycle applies to every while/until loop, whether the condition is read, an arithmetic test, or an external program — check the exit status, decide whether to run the body, repeat.
Common Mistakes
Mistake 1: forgetting to update the loop variable
count=1
while [[ $count -le 5 ]]; do
echo "Count: $count"
done
Because count is never incremented inside the body, [[ $count -le 5 ]] is true forever, and the loop never terminates — it will print Count: 1 until you interrupt it with Ctrl+C. Always double-check that something inside the loop body moves the condition toward becoming false.
count=1
while [[ $count -le 5 ]]; do
echo "Count: $count"
count=$((count + 1))
done
Mistake 2: piping into while read loses your variables
count=0
cat "/var/log/app.log" | while read -r line; do
count=$((count + 1))
done
echo "Total lines: $count"
Total lines: 0
Every command in a pipeline runs in its own subshell, so the while loop here executes in a child process with its own private copy of count. Incrementing it inside the loop has no effect on the count variable in the parent shell, so after the pipeline finishes, the original count is still 0. Redirecting the file directly into the loop instead avoids the subshell entirely:
count=0
while read -r line; do
count=$((count + 1))
done < "/var/log/app.log"
echo "Total lines: $count"
Mistake 3: leaving a variable unquoted in a test
status=""
while [ $status != "running" ]; do
echo "waiting..."
status="running"
done
When status is empty, the unquoted $status disappears entirely after word-splitting, leaving [ != "running" ] — a malformed test that Bash reports as an error (unary operator expected) instead of behaving as intended. Quoting the expansion, and preferring [[ ]] which doesn't word-split unquoted variables in the first place, avoids this:
status=""
while [[ "$status" != "running" ]]; do
echo "waiting..."
status="running"
done
Best Practices
- Prefer
[[ ]]over[ ]for conditions in Bash scripts — it doesn't word-split or glob unquoted variables and supports&&/||directly inside the brackets. - Always quote variable expansions inside conditions and loop bodies (
"$line","$status") to avoid word-splitting surprises. - Use
while IFS= read -r line; do ... done < "$file"to process a file, notcat file | while read line; do ... done— it avoids an unnecessary subshell and an unnecessarycatprocess. - For a deliberate infinite loop (a long-running daemon or monitor script), write
while true; do ... doneand usebreakto exit on a real condition, rather than crafting an always-true numeric test. - When retrying a flaky operation, always cap the number of attempts (as in Example 3) so a persistent failure doesn't retry forever.
- Use
breakandcontinuesparingly and only when they make the logic clearer than restructuring the condition — overusing them can make a loop's exit behavior hard to follow.
Practice Exercises
- Write a script called
countdown.shthat starts at 10 and counts down to 1 using awhileloop, printing each number, then printsLiftoff!. Don't forgetchmod +x countdown.shbefore running it. - Write a script that reads
/etc/hostsline by line with awhile readloop and prints only the lines that don't start with#(hint: use[[ $line == \#* ]]to test for a leading#, andcontinueto skip such lines). - Write a script that uses an
untilloop to wait for a file named/tmp/ready.flagto appear, checking every 2 seconds withsleep 2, and printingReady!once[[ -f /tmp/ready.flag ]]becomes true. Test it by creating the file in another terminal withtouch /tmp/ready.flag.
Summary
whilerepeats its body as long as the condition command exits with status0;untilrepeats as long as the condition exits non-zero.- The condition is re-evaluated fresh before every iteration — the loop body must eventually change the outcome, or the loop runs forever.
while IFS= read -r line; do ... done < "$file"is the standard, subshell-free way to process a file line by line in Bash.- Piping into
while readruns the loop in a subshell, so variables set inside it are lost once the pipeline ends — redirect from the file instead. - Always quote variable expansions and prefer
[[ ]]over[ ]to avoid word-splitting bugs in conditions. breakexits a loop immediately;continueskips straight to the next condition check.
