$linuxjunkies
>

Bash Loops: for, while and until

Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.

BeginnerUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • A terminal with Bash 4.0 or later (check with: bash --version)
  • Basic familiarity with running shell commands
  • A text editor for writing script files (nano, vim, or any editor)

Loops are the backbone of any non-trivial shell script. Bash gives you three distinct loop constructs — for, while, and until — each suited to different situations. for iterates over a fixed list or range; while runs as long as a condition is true; until runs as long as a condition is false. Master all three and you can automate almost any repetitive system task.

The for Loop

The for loop is the right choice when you already know the set of items to iterate over — a list of files, a range of numbers, or command output.

Iterating over a word list

The simplest form loops over a literal list of words:

for distro in debian ubuntu fedora arch; do
  echo "Processing: $distro"
done

Iterating over a numeric range with brace expansion

Use brace expansion for a compact numeric range. The third value is the step size.

# Count 1 to 10
for i in {1..10}; do
  echo "Step $i"
done
# Even numbers only (step of 2)
for i in {0..20..2}; do
  echo "$i"
done

C-style for loop

When you need a counter with more control, the C-style syntax is cleaner:

for (( i=0; i<5; i++ )); do
  echo "Index: $i"
done

Looping over files

Globbing inside a for loop is one of the most practical patterns in shell scripting. Always quote the variable when using the result.

# Rename every .txt file to .bak in the current directory
for file in *.txt; do
  mv -- "$file" "${file%.txt}.bak"
  echo "Renamed: $file"
done

Looping over command output

Use command substitution to feed output into a for loop. Be aware this splits on whitespace; for filenames with spaces, prefer a while read loop (shown later).

for user in $(cut -d: -f1 /etc/passwd); do
  echo "User found: $user"
done

The while Loop

while keeps executing its body as long as the test command returns exit code 0 (success). It is ideal when the number of iterations is not known in advance.

Basic while with a counter

count=1
while [ $count -le 5 ]; do
  echo "Count is $count"
  (( count++ ))
done

Reading a file line by line

This is the correct and safe way to process files that may contain spaces or special characters. It handles blank lines correctly and does not split on internal whitespace.

while IFS= read -r line; do
  echo "Line: $line"
done < /etc/hosts

IFS= prevents leading/trailing whitespace from being stripped. -r prevents backslash interpretation. These two options together are the standard safe idiom.

Reading command output safely

Pipe command output into while read to safely handle filenames or any data with spaces:

find /var/log -name "*.log" -print0 | while IFS= read -r -d '' logfile; do
  echo "Found log: $logfile"
done

-print0 and -d '' use null bytes as delimiters, making this safe for filenames containing spaces or newlines.

Waiting for a condition — polling pattern

A common sysadmin pattern: wait until a service becomes reachable before proceeding.

echo "Waiting for sshd on 192.168.1.50..."
while ! nc -z 192.168.1.50 22 2>/dev/null; do
  echo "Not ready yet, retrying in 5 seconds"
  sleep 5
done
echo "SSH is up"

Infinite loop with a break condition

Use while true for a deliberate infinite loop, then break out with break:

while true; do
  read -rp "Enter 'quit' to exit: " input
  if [[ $input == "quit" ]]; then
    echo "Exiting."
    break
  fi
  echo "You typed: $input"
done

The until Loop

until is the logical inverse of while: it loops as long as the condition is false (non-zero exit code). Use it when the natural phrasing of your logic is "keep going until something is ready." In practice many scripts use while ! instead, but until can read more clearly.

Basic until with a counter

count=1
until [ $count -gt 5 ]; do
  echo "Count is $count"
  (( count++ ))
done

Waiting for a file to appear

A classic use case: block a script until another process creates a lock or result file.

until [ -f /tmp/job_complete ]; do
  echo "Job not finished yet, sleeping..."
  sleep 3
done
echo "Job complete file detected, continuing."

Retry a command until it succeeds

attempts=0
until ping -c1 -W2 8.8.8.8 &>/dev/null; do
  (( attempts++ ))
  echo "Attempt $attempts: network unreachable, waiting 10s"
  sleep 10
done
echo "Network is up after $attempts retries"

Loop Control: break and continue

Both break and continue work in all three loop types. They accept an optional numeric argument to target an outer loop in nested situations.

# continue skips even numbers; break stops at 8
for i in {1..10}; do
  if (( i % 2 == 0 )); then
    continue
  fi
  if (( i == 9 )); then
    break
  fi
  echo "$i"
done
# Output: 1 3 5 7

Practical Example: Batch User Account Creation

This brings several concepts together: reading a file with while read, checking conditions, and acting on each record.

#!/usr/bin/env bash
# users.txt contains one username per line
while IFS= read -r username; do
  if id "$username" &>/dev/null; then
    echo "Skipping $username — already exists"
    continue
  fi
  useradd -m -s /bin/bash "$username"
  echo "Created user: $username"
done < users.txt

Verification

Test any loop safely before running it on real data by replacing destructive commands with echo (a dry-run). For scripts, run them through bash -n to check syntax without execution, and bash -x to trace execution line by line:

# Syntax check only
bash -n myscript.sh

# Trace execution
bash -x myscript.sh

Troubleshooting

  • Infinite loop accidentally created: Press Ctrl+C to interrupt. Add a counter and a break as a safety valve in loops that wait on external conditions.
  • Loop over files misses names with spaces: Never use for file in $(ls). Use a glob (for file in *.txt) or while IFS= read -r -d '' file; do ... done < <(find ... -print0).
  • Variable not incrementing: Ensure you are using (( count++ )) or count=$(( count + 1 )). Using count++ without the double parentheses does nothing in Bash.
  • Loop body runs in a subshell: When piping into a loop (cmd | while read), variable changes inside the loop are not visible outside on Bash versions before 4.2. Use process substitution instead: while read line; do ...; done < <(cmd).
tested on:Ubuntu 24.04Fedora 40Debian 12Arch rolling

Frequently asked questions

What is the difference between while and until in Bash?
while loops as long as its condition returns exit code 0 (true/success). until loops as long as its condition returns a non-zero exit code (false/failure). They are logical inverses; anything you can write with one you can write with the other by negating the condition.
Why should I avoid 'for file in $(ls)'?
Command substitution splits output on whitespace, so filenames containing spaces or newlines are broken into multiple tokens. Use a glob pattern like 'for file in *' instead, which Bash expands safely without word-splitting.
Why do variable changes inside a piped while loop disappear after the loop?
When you write 'cmd | while read', Bash runs the while loop body in a subshell, so variable assignments are lost when the subshell exits. Use process substitution instead: 'while read line; do ...; done < <(cmd)' to keep the loop in the current shell.
How do I loop over an array in Bash?
Use the '@' expansion: 'for item in "${myarray[@]}"; do ... done'. The double quotes around '${myarray[@]}' ensure each element is treated as a single word even if it contains spaces.
How do I add a timeout to a while or until loop that polls for a condition?
Track elapsed time with a counter or compare against a deadline using the SECONDS built-in variable. For example: 'while ! condition && (( SECONDS < 60 )); do sleep 2; done' will give up after 60 seconds.

Related guides