Bash Loops: for, while and until with Practical Examples
On this page
Loops are where the shell starts saving you real time: rename 300 photos, check 20 servers, process every line of a log file. Bash has three loop types, for, while and until, and a few patterns that keep them safe when file names contain spaces.
All the examples work both in scripts and typed directly at the prompt. If you are new to scripts, start with Bash scripting basics.
for loops over a list
The basic for loop runs its body once for each word in a list:
for fruit in apple banana cherry; do
echo "I like $fruit"
done
I like apple
I like banana
I like cherry
At the prompt you can write it on one line, with semicolons where the line breaks were:
$ for host in web01 web02 db01; do ping -c 1 "$host"; done
Loop over a script's arguments with "$@", or over an array with "${array[@]}". Keep the quotes, so items containing spaces stay whole:
servers=("web01" "web02" "backup server")
for s in "${servers[@]}"; do
echo "Checking $s"
done
Number ranges
Brace expansion generates sequences:
$ echo {1..5}
1 2 3 4 5
$ echo {01..10}
01 02 03 04 05 06 07 08 09 10
$ echo {0..20..5}
0 5 10 15 20
$ for i in {1..3}; do echo "Run $i"; done
Brace expansion happens before variables are expanded, so {1..$n} does not work; it stays as the literal text {1..3}. When the limit is in a variable, use a C-style loop:
n=10
for ((i = 1; i <= n; i++)); do
echo "Item $i of $n"
done
seq also works (for i in $(seq 1 "$n")), and is handy for decimals like seq 0 0.5 2.
Looping over files safely
To process files, loop over a glob pattern directly:
for f in *.jpg; do
echo "Processing $f"
done
Bash expands *.jpg into a proper list of file names, one item per file, even when names contain spaces. Inside the loop, always quote "$f".
One catch: if nothing matches, the pattern stays as the literal text *.jpg, and the loop runs once with that. Either guard against it or turn on nullglob, which makes a pattern with no matches expand to nothing:
# Option 1: skip the literal pattern
for f in *.png; do
[[ -e $f ]] || continue
echo "Found $f"
done
# Option 2: nullglob
shopt -s nullglob
for f in *.png; do
echo "Found $f"
done
A practical example, renaming every .jpeg file to .jpg:
for f in *.jpeg; do
[[ -e $f ]] || continue
mv -n -- "$f" "${f%.jpeg}.jpg"
done
${f%.jpeg} removes .jpeg from the end of the name, and mv -n refuses to overwrite a file that already exists. For files in subdirectories, use shopt -s globstar and the pattern **/*.jpg, or use find as shown below.
Reading a file line by line
To process a file one line at a time, use while read:
while IFS= read -r line; do
echo "Got: $line"
done < names.txt
Each part matters:
IFS=stopsreadfrom trimming spaces at the start and end of each line.-rkeeps backslashes as they are instead of treating them as escapes.< names.txtafterdonefeeds the file into the whole loop.
If the last line of the file has no trailing newline, read returns failure for it and the loop skips it. Add a check to catch that line too:
while IFS= read -r line || [[ -n $line ]]; do
echo "Got: $line"
done < names.txt
read can also split each line into fields. Set IFS to the separator and list several variable names; _ is a common name for fields you do not need:
$ while IFS=: read -r user _ uid _; do
> (( uid == 0 )) && echo "$user has uid $uid"
> done < /etc/passwd
root has uid 0
To load a whole file into an array instead, use mapfile -t lines < names.txt.
Looping over command output
To loop over the output of a command, feed it to while read with process substitution, <(...):
count=0
while IFS= read -r user; do
count=$((count + 1))
done < <(cut -d: -f1 /etc/passwd)
echo "$count users"
Why not pipe into the loop with cut ... | while read? That works for printing, but each part of a pipeline runs in a subshell, so variables set inside the loop (like count) are lost when the loop ends. Process substitution keeps the loop in your current shell.
For file names from find, use null separators, which are safe for any name, including ones with newlines:
while IFS= read -r -d '' f; do
echo "Found: $f"
done < <(find . -name "*.log" -print0)
The find guide shows -exec, which is often simpler still.
while and until loops
while repeats as long as its condition succeeds; until repeats as long as it fails. They are useful for counters, retries and waiting for something:
i=1
while (( i <= 3 )); do
echo "Attempt $i"
i=$((i + 1))
done
# Wait until a web server answers
until curl -fs http://localhost:8080/ >/dev/null; do
echo "Waiting for server..."
sleep 2
done
echo "Server is up"
An endless loop is while true; do ...; done. Stop it with Ctrl+C, or leave it from the inside with break:
while true; do
df -h /
sleep 60
done
break and continue
continue skips to the next iteration; break leaves the loop entirely:
for i in {1..10}; do
(( i % 2 == 0 )) && continue # skip even numbers
(( i > 7 )) && break # stop after 7
echo "$i"
done
This prints 1, 3, 5 and 7. In nested loops, break 2 and continue 2 act on the outer loop.
Common mistakes
for f in $(ls)
This is the most common loop bug. The output of ls is split on spaces, so a file called holiday photo.jpg becomes two items, holiday and photo.jpg:
$ for f in $(ls *.jpg); do echo "[$f]"; done
[beach.jpg]
[holiday]
[photo.jpg]
$ for f in *.jpg; do echo "[$f]"; done
[beach.jpg]
[holiday photo.jpg]
Use a glob for files and while read for lines. The same problem applies to for line in $(cat file), which loops over words, not lines.
Unquoted variables
Inside the loop, rm $f breaks on spaces just like $(ls) does. Write rm -- "$f".
Commands that read standard input
In a while read loop, any command that reads standard input, such as ssh, eats the rest of your file, and the loop stops after one line. Give that command its own input:
while IFS= read -r host; do
ssh -n "$host" uptime # -n: don't read stdin
# or: some_command < /dev/null
done < hosts.txt
Quick reference
| Pattern | Use for |
|---|---|
for x in a b c; do ...; done | A fixed list |
for i in {1..10}; do ...; done | A literal range |
for ((i=0; i<n; i++)); do ...; done | A range from a variable |
for f in *.txt; do ...; done | Files in a directory |
while IFS= read -r line; do ...; done < file | Lines of a file |
while ...; done < <(command) | Lines of command output |
until cmd; do sleep 2; done | Wait or retry |
break / continue | Leave loop / skip to next |
The Looping Constructs page of the Bash manual has the formal syntax.