A Bash while loop runs a block of commands repeatedly as long as a test condition is true. Where the for loop iterates over a known list, while is the right choice when you DON'T know the iteration count in advance: read each line of a file, retry a command until it succeeds, wait for a service to come up, poll until a state changes. The condition is re-evaluated every pass, so the loop continues until the condition turns false (or you break out). Below is the full reference: the four most common patterns, the cat … | while read subshell-scoping bug that catches everyone once, retry-with-backoff, wait-for-ready, the until and select siblings, and the three test contexts ([ ], [[ ]], (( ))) that Bash makes you choose between.
How do I write a while loop in Bash?
A Bash while loop runs a block of commands repeatedly as long as a test condition evaluates true. The syntax is while CONDITION; do COMMANDS; done. The condition is checked BEFORE each iteration, so if it's false to begin with, the body never runs. The most common forms: while [ "$count" -lt 10 ] for numeric comparison (POSIX), while [[ "$status" == "ok" ]] for richer string and pattern tests (Bash builtin), while (( count < 10 )) for arithmetic (compact, no quoting), while IFS= read -r line for reading a file line by line, and while true for unbounded polling (exit via break). Use continue to skip to the next iteration and break to exit the loop entirely. For known iteration counts or fixed lists, prefer Bash for loops instead.
Jump to:
- The four most common while-loop patterns
- Test contexts:
[ ]vs[[ ]]vs(( )) - Reading a file line by line
- The
cat file | while readsubshell-scoping bug - Retry with exponential backoff
- Wait until a service is ready
break,continue, and labelled breaks- Reading input with a timeout
- The
untilandselectsiblings - Common pitfalls
- FAQ
The four most common while-loop patterns
# 1. Counter — run N times when N comes from a variable
count=1
while (( count <= 10 )); do
echo "Iteration $count"
(( count++ ))
done
# 2. Read each line of a file
while IFS= read -r line; do
echo "$line"
done < file.txt
# 3. Retry until success
attempt=0
while ! curl -fsSL https://api.example.com/health; do
(( attempt++ )) && [ "$attempt" -ge 5 ] && exit 1
sleep 2
done
# 4. Wait for a condition to flip
while [ ! -f /tmp/ready.flag ]; do
sleep 1
doneThese four cover ~85% of real-world while-loop use. The rest of this page covers the gotchas that turn working scripts into scripts that don't break in subtle ways.
Test contexts: [ ] vs [[ ]] vs (( ))
Bash gives you three test contexts. Knowing which to use when is the difference between robust scripts and shell scripts that break on the first edge case.
| Context | Use case | Example |
|---|---|---|
[ ] (POSIX test) | Maximum portability; works in sh as well as Bash | while [ "$x" -lt 10 ]; do ... |
[[ ]] (Bash builtin) | Rich string ops, glob and regex matching, no word splitting on the LHS | while [[ "$status" == ok* ]]; do ... |
(( )) (arithmetic) | Numeric comparison without quoting, C-style operators | while (( x < 10 )); do ... |
Practical guidance:
- Numeric: prefer
(( ))— no quoting needed, supports++,--,+=,-=,<=,>=,<,>directly. - String equality, pattern, regex: prefer
[[ ]]— supports==,!=,=~(regex match),<,>(lexicographic), and globs on the right side. - POSIX compatibility required: stick to
[ ]and quote everything ([ "$x" -lt 10 ]not[ $x -lt 10 ]).
Mixing them in one script is fine — pick the context that matches the test you're doing.
Reading a file line by line
The canonical pattern:
while IFS= read -r line; do
# process "$line"
echo ">> $line"
done < file.txtWhat every flag does:
IFS=(empty IFS) — preserves leading and trailing whitespace on each line. Without this,readtrims them.-r(raw) — disables backslash interpretation. Without-r, a\at the end of a line escapes the newline and reads the next line as a continuation.< file.txt— redirects file content as the loop's stdin.
This is the only correct way to read a file in Bash. Every other pattern (the for line in $(cat file) trap, the cat file | while subshell trap) has a known bug.
To read line-by-line from a command's output (not a file), use process substitution:
while IFS= read -r line; do
echo "Found: $line"
done < <(find . -name "*.log")< <(command) runs the command in the parent shell's context. Variables you set inside the loop persist after the loop ends — unlike command | while read (next section).
The cat file | while read subshell-scoping bug
The #1 while-loop bug:
# ❌ BAD — total stays at 0 after the loop
total=0
cat numbers.txt | while IFS= read -r line; do
total=$(( total + line ))
done
echo "Total: $total" # prints 0Why: each side of a pipe runs in a subshell. Variables set inside while are scoped to that subshell and discarded when the pipe closes. The outer total is never modified.
Three fixes:
# ✅ A. Process substitution (cleanest)
total=0
while IFS= read -r line; do
total=$(( total + line ))
done < <(cat numbers.txt)
echo "Total: $total" # works
# ✅ B. Redirect from the file directly (best when input IS a file)
total=0
while IFS= read -r line; do
total=$(( total + line ))
done < numbers.txt
# ✅ C. lastpipe option (Bash 4.2+)
shopt -s lastpipe
total=0
cat numbers.txt | while IFS= read -r line; do
total=$(( total + line ))
doneOption C requires set +m (job control off) to actually take effect — on macOS Bash 3.2 it doesn't work at all. Use option A or B for portable code.
Retry with exponential backoff
The pattern I reach for every time a network call needs to handle transient failures:
#!/usr/bin/env bash
MAX_ATTEMPTS=8
attempt=0
delay=1
while (( attempt < MAX_ATTEMPTS )); do
if curl -fsSL https://api.example.com/health; then
echo "Success on attempt $((attempt + 1))"
exit 0
fi
(( attempt++ ))
echo "Attempt $attempt failed, sleeping ${delay}s..."
sleep "$delay"
delay=$(( delay * 2 )) # 1, 2, 4, 8, 16, 32, 64, 128
done
echo "Failed after $MAX_ATTEMPTS attempts" >&2
exit 1Three things this pattern gets right:
curl -fmakes curl exit non-zero on HTTP 4xx/5xx (it returns 0 by default for any HTTP response).- Exponential backoff (
delay *= 2) avoids hammering the server during outages. Total wait time after 8 attempts is 255 seconds. exit 1after the loop signals the calling script that the operation failed. Without it, the script silently continues.
For randomised backoff (jitter, to avoid thundering-herd on shared retries):
sleep "$(( delay + (RANDOM % delay) ))" # delay to 2*delay rangeWait until a service is ready
Common in CI/CD and Docker entrypoints — block until a database or web service accepts connections, then continue:
#!/usr/bin/env bash
# Wait up to 60 seconds for PostgreSQL to accept connections
DEADLINE=$(( SECONDS + 60 ))
while ! pg_isready -h db -p 5432 -U postgres > /dev/null 2>&1; do
if (( SECONDS >= DEADLINE )); then
echo "Postgres did not start within 60s" >&2
exit 1
fi
sleep 1
done
echo "Postgres is ready, running migrations..."
psql -h db -U postgres -f /migrations/init.sqlSECONDS is a Bash builtin that returns the number of seconds since the shell started — a free monotonic timer. The pattern: compute a deadline once, loop while the current SECONDS is below it.
Same pattern for HTTP services:
DEADLINE=$(( SECONDS + 30 ))
while ! curl -fsS http://localhost:3000/health > /dev/null; do
(( SECONDS < DEADLINE )) || { echo "Timeout" >&2; exit 1; }
sleep 1
doneUse this in Dockerfiles, Kubernetes init containers, GitHub Actions steps, deploy scripts — anywhere one process needs to wait for another before proceeding.
break, continue, and labelled breaks
# break — exit the loop entirely
while true; do
read -p "> " input
[ "$input" = "quit" ] && break
echo "You typed: $input"
done
# continue — skip to the next iteration
count=0
while (( count < 10 )); do
(( count++ ))
(( count % 2 == 0 )) && continue # skip even numbers
echo "$count"
done
# break N — exit N levels of nested loop
while read -r outer; do
while read -r inner; do
if [ "$inner" = "STOP" ]; then
break 2 # exit BOTH loops, not just the inner one
fi
done < inner.txt
done < outer.txtbreak N and continue N accept a level argument. break 2 exits two loops at once; continue 2 advances the outer loop's iteration without running the rest of the inner pass. Useful in nested polling and stream-processing patterns.
Reading input with a timeout
read -t SECONDS returns an error if the user doesn't type within the deadline:
while true; do
if read -t 10 -p "> " input; then
[ "$input" = "quit" ] && break
echo "Got: $input"
else
echo "No input in 10s, exiting"
break
fi
doneEquivalent for stream-reading with timeout (per-line):
while IFS= read -r -t 5 line; do
echo "Got: $line"
doneWhen read -t times out, the loop exits cleanly. Useful for tailing a stream where you want to break out if data stops arriving.
The until and select siblings
while has two close cousins:
until — runs while the condition is FALSE (the inverse of while):
# Wait for a file to appear
until [ -f /tmp/ready.flag ]; do
sleep 1
done
# Equivalent while form:
while [ ! -f /tmp/ready.flag ]; do
sleep 1
doneThe two are interchangeable. Pick whichever reads more naturally: until for "wait for X to be true" patterns, while for everything else.
select — interactive numbered menu:
PS3="Choose an action: "
select choice in "start" "stop" "restart" "quit"; do
case "$choice" in
start) systemctl start myapp ;;
stop) systemctl stop myapp ;;
restart) systemctl restart myapp ;;
quit) break ;;
*) echo "Invalid choice: $REPLY" ;;
esac
doneselect displays a numbered menu, reads user input into $REPLY, and sets $choice to the matched option (or empty if invalid). The loop runs until break.
For loop-choice guidance: known list or count → for; condition-driven → while; "do until success" → until; interactive menu → select. Same scoping rules, same break/continue semantics across all four.
Common pitfalls
1. The pipe subshell bug. cat file | while read x; do VAR=...; done — VAR is lost after the loop. Use done < <(cat file) or done < file instead. Covered in detail above.
2. Infinite loops from missing increment. while (( count < 10 )); do echo "$count"; done with no (( count++ )) runs forever. Always trace the variable that controls the condition.
3. while read without -r mangles backslashes. A line containing path\to\file reads as pathtofile. Always use -r.
4. while read without IFS= trims whitespace. Leading and trailing spaces in each line are silently dropped. Always set IFS= for the read.
5. Numeric comparison with the wrong operator. [ "$x" < 10 ] does a STRING comparison (then redirects, because < is also redirection). Use [ "$x" -lt 10 ] for POSIX or (( x < 10 )) for arithmetic.
6. Unbounded retry without a circuit-breaker. while ! curl ...; do sleep 1; done blocks forever if the service is permanently down. Always add a max-attempts or deadline check.
7. Disk-space regex that misses 100%. df / | awk 'NR==2 {print $5}' | grep -q '^[8-9][0-9]%' matches 80-99% but NOT 100%. Use grep -qE '^([8-9][0-9]|100)%' to include the full state.
8. while true without break. Most infinite loops should have at least one break path. Without it, the loop is unreachable to exit cleanly and you need SIGTERM to kill it.
9. Forgetting to quote in [ ]. [ $x -eq 10 ] breaks if $x is empty (syntax error: [ -eq 10 ]). Always quote: [ "$x" -eq 10 ]. Or switch to [[ ]]/(( )) which don't have this problem.
Debugging while loops
# Trace every command
bash -x script.sh
# Trace one section
set -x
while ...; do
...
done
set +x
# Fail fast on errors and unset variables
set -euo pipefail
# Static analysis — catches every common while-loop pitfall above
shellcheck script.shshellcheck flags the missing -r on read, the unquoted variable in [ ], the pipe-subshell scoping issue, and most of the pitfalls listed above. Run it on every script you write. Install via brew install shellcheck on macOS or apt install shellcheck on Debian/Ubuntu.
What to do next
- Bash For Loops — the known-list iteration sibling. Same
break/continuesemantics, different control structure. - How to ZIP Multiple Directories into Individual Files —
find+while readfor safe batch archiving. - How to Optimize JPEG Images Using jpegoptim — a worked example of a while-driven batch image-optimisation script.
- How to Export All MySQL Databases with mysqldump — the cron-friendly backup script demonstrates retry, deadline, and notification patterns.
- How to Increase Google Cloud VM Disk Size Without Rebooting — uses a
whilepolling loop to wait for the resized disk to become visible on the VM. - External: the Bash Reference Manual: Looping Constructs is the authoritative spec.

![Bash While Loops: Syntax, read-line, Retry Patterns (2026) Bash while loop reference: read files with while IFS= read -r, retry-with-backoff, wait-for-service polling, the subshell-scoping bug fix, the until and select siblings, plus [[ ]] vs [ ] vs (( )) test contexts.](https://techearl.com/cdn-cgi/image/width=1536,quality=90,format=auto/https://images.techearl.com/bash-while-loop/bash-while-loop.jpg?v=2026-04-25T13%3A18%3A00Z)


![Bash conditionals reference: if/elif/else syntax, [ ] vs [[ ]] vs (( )) test contexts, numeric, string, and file operators, the case statement, and the unquoted-variable pitfall.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=90/https://images.techearl.com/bash-if-else/bash-if-else.jpg?v=2026-01-08T11%3A24%3A00Z)
![Bash arrays reference: declaration, indexing, [@] vs [*] quoting, iteration, appending, slicing, mapfile/readarray for lines, IFS-based string splitting, plus macOS Bash 3.2 limits.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=90/https://images.techearl.com/bash-arrays/bash-arrays.jpg?v=2026-02-12T14%3A18%3A00Z)