Code of the Day
IntermediateScripting patterns

Loops In Depth

Master for over arrays and globs, C-style for loops, while read for line-by-line processing, break/continue, and until.

BashIntermediate11 min read
Recommended first
By the end of this lesson you will be able to:
  • Iterate over arrays, globs, and command output with for
  • Use C-style for ((i=0; i<n; i++)) for counted loops
  • Process a file line by line with while read
  • Control loop flow with break and continue
  • Use until as a "while not" construct

Conditionals tell a script what to do; loops tell it how many times. Bash has a richer set of loop forms than most people expect — each one fits a different problem. Knowing which form to reach for is what turns a stack of repeated commands into a clean, maintainable script.

for over lists, arrays, and globs

The basic for loop iterates over a whitespace-separated list:

for color in red green blue; do
  echo "$color"
done

More practically, iterate over an array:

files=("report.csv" "data.json" "config.yaml")
for f in "${files[@]}"; do
  echo "Processing $f"
done

Use "${files[@]}" (with the double quotes and @) to preserve filenames that contain spaces. ${files[*]} without quotes will split on spaces and break.

expand inline, making it easy to operate on a set of files:

for script in /usr/local/bin/*.sh; do
  [[ -x "$script" ]] && echo "$script is executable"
done

If no files match the glob, Bash passes the literal pattern string into the loop body. Guard against this with [[ -e "$script" ]] or set nullglob at the top of the script.

Iterating over command output

Command substitution feeds into a for loop:

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

Word splitting bites here. If any output line contains spaces, the loop treats each word as a separate iteration. For line-by-line handling of command output — especially when values can contain spaces — use while read instead (shown below).

C-style for loops

When you need a counted loop with an index, the C-style form is the clearest choice:

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

# Counting down
for ((i = 10; i > 0; i -= 2)); do
  echo "$i..."
done
echo "Done"

You can also declare multiple variables, use modulo, or reference array indices:

arr=(a b c d e)
for ((i = 0; i < ${#arr[@]}; i++)); do
  echo "arr[$i] = ${arr[$i]}"
done

while read for line-by-line processing

while read is the right tool whenever you need to process a file (or command output) one line at a time — especially when lines can contain spaces:

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

Two details matter:

  • IFS= prevents leading/trailing whitespace from being stripped.
  • -r prevents backslashes from being treated as escape sequences.

Feed command output the same way:

find /var/log -name "*.log" | while IFS= read -r logfile; do
  wc -l "$logfile"
done

while read in a runs in a subshell. Any variables you set inside the loop are not visible outside it. If you need the variable after the loop, use process substitution (while read ... done < <(command)) or redirect from a file.

break and continue

break exits the current loop immediately. continue skips the rest of the current iteration and moves to the next:

for f in *.log; do
  [[ ! -s "$f" ]] && continue       # skip empty files
  [[ "$f" = error.log ]] && break   # stop at error.log
  echo "Reading $f"
done

Both accept a numeric argument to break out of nested loops: break 2 exits two levels.

until

until is the mirror of while — it loops as long as the condition is false, stopping when it becomes true. It reads naturally for "wait until something is ready":

until [[ -f /tmp/service.ready ]]; do
  echo "Waiting for service..."
  sleep 2
done
echo "Service is up"

A while equivalent (while [[ ! -f ... ]]) works identically — until is just more expressive when the "done" condition is what you care about.

Check your understanding

  1. 1.
    You have arr=("file one.txt" "file two.txt"). Which expansion correctly iterates over both elements without splitting on spaces?
  2. 2.
    What does the -r flag do in while IFS= read -r line?
  3. 3.
    A variable set inside a while read loop that receives input via a pipe is visible after the loop ends.

Do it yourself

# Iterate over files with spaces in names
mkdir -p /tmp/looptest
touch "/tmp/looptest/file one.txt" "/tmp/looptest/file two.txt"
files=("/tmp/looptest/file one.txt" "/tmp/looptest/file two.txt")
for f in "${files[@]}"; do echo "Found: $f"; done

# C-style countdown
for ((i = 5; i > 0; i--)); do echo "$i"; done

# Process /etc/hosts line by line
while IFS= read -r line; do
  [[ "$line" =~ ^# ]] && continue   # skip comments
  echo "$line"
done < /etc/hosts

# until a file appears
touch /tmp/ready_signal
until [[ -f /tmp/ready_signal ]]; do sleep 1; done && echo "ready"
rm /tmp/ready_signal

Where to go next

With precise conditionals and every loop form in your toolkit, the next lesson — Functions — shows how to package reusable logic, manage local state, and compose scripts that don't repeat themselves.

Finished reading? Mark it complete to track your progress.

On this page