Bash Loops: for, while and until with Practical Examples

Scripting · 7 min read · Updated 25 Oct 2026

On this page
  1. for loops over a list
  2. Number ranges
  3. Looping over files safely
  4. Reading a file line by line
  5. Looping over command output
  6. while and until loops
  7. break and continue
  8. Common mistakes
  9. Quick reference

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= stops read from trimming spaces at the start and end of each line.
  • -r keeps backslashes as they are instead of treating them as escapes.
  • < names.txt after done feeds 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

PatternUse for
for x in a b c; do ...; doneA fixed list
for i in {1..10}; do ...; doneA literal range
for ((i=0; i<n; i++)); do ...; doneA range from a variable
for f in *.txt; do ...; doneFiles in a directory
while IFS= read -r line; do ...; done < fileLines of a file
while ...; done < <(command)Lines of command output
until cmd; do sleep 2; doneWait or retry
break / continueLeave loop / skip to next

The Looping Constructs page of the Bash manual has the formal syntax.