Reading a file? Start there
Read a File Line by Line in Bash covers the file-reading loop in depth (encodings, CSV fields, empty lines). This page is about the loop itself: what runs where, what eats your input, and how to make sure it ends. For loops over lists, see Bash For Loop Examples.
A while loop is three lines and four classic bugs. Variables set inside it vanish because a pipe put it in a subshell. Backslashes and indentation disappear because read was called without -r and IFS=. A loop over a host list stops after the first host because ssh read the rest of the list. And a polling loop waits forever for a service that is never coming back. Every one of them is silent.
Why Is My Variable Empty After the Loop?
Reproduced here on 2026-10-03 (Kali, bash 5.3.15):
Each side of a pipe runs in its own process. The loop counted to 3 in a subshell that exited with the pipe, and the parent's count never changed. done < <(cmd) feeds the loop from a process substitution, so the loop runs in the current shell. shopt -s lastpipe runs the last command of a pipeline in the current shell; it works in scripts, and in an interactive shell only when job control is off.
What Does read -r Actually Change?
Without -r, read treats backslashes as escapes and deletes them. Without IFS=, it strips leading and trailing whitespace. Both are wrong for almost any real file, which is why ShellCheck flags a bare read as SC2162.
A file whose last line has no trailing newline loses that line, because read returns failure at end of file even when it filled the variable:
Why Does My Loop Stop After the First Line?
hosts.txt holds web1, web2 and db1. cat stands in for any command that reads stdin, such as ssh:
The body inherits the loop's stdin, which is the file. cat read web2 and db1 before the loop's next read got a chance, and the loop ended after one host with exit status 0. Give the inner command its own input: ssh -n, < /dev/null, or read the file on another descriptor (while read -r host <&3; do …; done 3< hosts.txt). Run Commands on a Remote Server over SSH hits the same trap with real ssh and passes -n for it.
What Do until and Counter Loops Look Like?
until runs while its condition fails. (( … )) is bash arithmetic: no $ needed on names, and < means less-than instead of a redirect. Now the same counter loop under set -e:
(( i++ )) evaluates to the old value of i, which is 0 on the first pass, and an arithmetic command that evaluates to 0 returns status 1. set -e sees a failed command and ends the script after one iteration. Write i=$((i + 1)) or (( i += 1 )) in any script that uses strict mode.
How Do I Stop a Polling Loop From Waiting Forever?
while ! curl -sf http://localhost:8080/health; do sleep 1; done is fine until the service never starts, and then a deploy or a cron job hangs until someone kills it. Count against a deadline instead. Save as wait-until.sh:
Prerequisites
bash 4 or later. The command you poll can be anything that exits 0 when ready: test -e, curl -sf, pg_isready, systemctl is-active --quiet, nc -z.
How Does the Script Work?
SECONDScounts seconds since the shell started. Comparing it to a deadline makes the timeout real even when each check itself takes a while;for i in {1..30}withsleep 1can run far longer than 30 seconds.- The check inside
ifis whyset -edoes not end the script on the first failed attempt. A failing command in anifcondition is not an error, which is also the fix for the(( i++ ))trap above. </dev/nullgives the check its own stdin, the same fix as thesshloop.- Exit 124 matches what
timeout(1)returns, so callers can treat both the same way:./wait-until.sh 60 2 … || exit 1. "$@"passes the command and its arguments through intact, quotes and spaces included.
What Does the Script Print?
A flag file that appears after 3 seconds, a port where nothing listens, and a bad interval:
For a command that fails and should be retried with growing delays, rather than polled until something becomes ready, use Retry With Exponential Backoff. For one command that must not run longer than a limit, the timeout command. The Production Bash Toolkit ships these patterns ShellCheck-clean.
Frequently Asked Questions
Why is my variable empty after a while loop in bash?
Because the loop ran in a subshell. In cmd | while read -r line; do count=$((count+1)); done, each side of the pipe runs in its own process, so the loop's count disappears when the pipe ends and the parent still has its old value. Feed the loop with process substitution instead: while read -r line; do …; done < <(cmd). In a script you can also enable shopt -s lastpipe, which runs the last part of a pipeline in the current shell.
Why does my while read loop stop after the first line?
A command inside the loop is reading the loop's standard input. ssh, cat, ffmpeg and anything else that reads stdin will swallow the remaining lines of the file the loop is reading, so the next read hits end of file. Give that command its own input: ssh -n host cmd, cmd < /dev/null, or read the file on another descriptor with while read -r line <&3; do …; done 3< file.
What is the difference between while and until in bash?
while repeats as long as its condition command succeeds (exit status 0); until repeats as long as it fails, stopping at the first success. until ping -c1 host; do sleep 2; done is the same as while ! ping -c1 host; do sleep 2; done. Pick whichever reads more naturally; both need a way out, such as a counter or a deadline, if the condition may never change.
How do I write an infinite loop in bash, and how do I stop it?
while true; do …; done or while :; do …; done loops forever. Leave it with break when a condition is met, or exit to end the script. Always put a sleep in a polling loop, or it spins a CPU core at 100%. For anything that waits on another system, use a deadline: compute DEADLINE=$((SECONDS + 60)) before the loop and break or exit once SECONDS passes it, so a service that never comes up cannot hang your script.
Why does set -e exit my script inside a while loop?
The arithmetic command (( expr )) returns exit status 1 when the expression evaluates to 0. (( i++ )) evaluates to the old value of i, so on the first pass, when i is 0, it fails and set -e stops the script. Write i=$((i + 1)) or (( i += 1 )) instead, or (( ++i )), which evaluates to the new value. The same applies to (( count-- )) reaching zero.
Part of the bash snippets collection
Related Scripts
- Read a File Line by Line — the file-reading loop in depth
- Bash For Loop Examples — loops over lists and globs
- Retry With Exponential Backoff — retrying a failing command with growing delays
- The timeout Command — capping how long one command may run