Shell by Example: While Loops POSIX

The while loop executes commands as long as a condition is true.

The condition is tested BEFORE each iteration. If false initially, the loop body never runs at all.

Edit
#!/bin/sh
i=1
while [ "$i" -le 5 ]; do
    echo "Count: $i"
    i=$((i + 1))
done
Output:
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

While loops shine when you don’t know in advance how many iterations you need. Unlike for loops that iterate over a fixed list, while loops continue until a condition changes.

Use while when:

  • Waiting for something to happen (polling)
  • Processing input until end-of-file
  • Retrying operations until success
  • Any situation where the iteration count is unknown
Edit
#!/bin/sh
# For loop: you know the items ahead of time
echo "For loop (fixed list):"
for item in a b c; do
    echo "  $item"
done

# While loop: continue until a condition is met
echo "While loop (condition-based):"
n=1
while [ "$n" -le 3 ]; do
    echo "  iteration $n"
    n=$((n + 1))
done
Output:
For loop (fixed list):
  a
  b
  c
While loop (condition-based):
  iteration 1
  iteration 2
  iteration 3

Counting is a common use of while loops, especially in POSIX sh which lacks the {1..5} range syntax. You can count in either direction by adjusting the condition and increment.

Edit
#!/bin/sh
# Counting up: start low, increment, stop when limit exceeded
echo "Counting up:"
i=1
while [ "$i" -le 3 ]; do
    echo "  $i"
    i=$((i + 1))
done

# Counting down: start high, decrement, stop when limit reached
echo "Counting down:"
n=3
while [ "$n" -gt 0 ]; do
    echo "  $n"
    n=$((n - 1))
done
echo "  Liftoff!"
Output:
Counting up:
  1
  2
  3
Counting down:
  3
  2
  1
  Liftoff!

The until loop is the opposite of while - it runs until the condition becomes true. Use until when thinking about the stopping condition is more natural than the continuation condition.

These two forms are equivalent:

  • while [ ! CONDITION ]; do ...: continue while NOT true
  • until [ CONDITION ]; do ...: continue until true
Edit
#!/bin/sh
# With while: "keep going while num is NOT greater than 3"
echo "Using while (with negated condition):"
num=1
while [ "$num" -le 3 ]; do
    echo "  num is $num"
    num=$((num + 1))
done

# With until: "keep going until num is greater than 3"
echo "Using until (same result):"
num=1
until [ "$num" -gt 3 ]; do
    echo "  num is $num"
    num=$((num + 1))
done
Output:
Using while (with negated condition):
  num is 1
  num is 2
  num is 3
Using until (same result):
  num is 1
  num is 2
  num is 3

You can combine multiple conditions using && (AND) and || (OR) operators. The loop continues while the combined condition is true.

  • &&: ALL conditions must be true to continue
  • ||: ANY condition being true continues the loop
Edit
#!/bin/sh
# AND: both conditions must be true
echo "Using && (both must be true):"
x=0
y=10
while [ "$x" -lt 3 ] && [ "$y" -gt 7 ]; do
    echo "  x=$x, y=$y"
    x=$((x + 1))
    y=$((y - 1))
done

# OR: loop continues if either condition is true
echo "Using || (either can be true):"
a=0
b=0
while [ "$a" -lt 2 ] || [ "$b" -lt 3 ]; do
    echo "  a=$a, b=$b"
    a=$((a + 1))
    b=$((b + 1))
done
Output:
Using && (both must be true):
  x=0, y=10
  x=1, y=9
  x=2, y=8
Using || (either can be true):
  a=0, b=0
  a=1, b=1
  a=2, b=2

As with for loops, break exits a loop early and continue skips to the next iteration. These are especially useful with while true for loops that run until some condition inside the loop triggers an exit.

Edit
#!/bin/sh
# Using break: exit when a condition is met
echo "Break example (exit on condition):"
count=0
while true; do
    count=$((count + 1))
    echo "  Iteration $count"
    if [ "$count" -ge 3 ]; then
        echo "  Breaking out"
        break
    fi
done

# Using continue: skip certain iterations
echo "Continue example (skip even numbers):"
i=0
while [ "$i" -lt 6 ]; do
    i=$((i + 1))
    if [ $((i % 2)) -eq 0 ]; then
        continue
    fi
    echo "  Odd: $i"
done
Output:
Break example (exit on condition):
  Iteration 1
  Iteration 2
  Iteration 3
  Breaking out
Continue example (skip even numbers):
  Odd: 1
  Odd: 3
  Odd: 5

Processing command output line by line is a common pattern. Pipe the command into a while read loop to handle each line individually.

Important: Variables modified inside a piped while loop won’t persist outside because pipes create a subshell.

Edit
#!/bin/sh
echo "Processing lines from a command:"
printf '%s\n' "first" "second" "third" | while read -r line; do
    echo "  Got: $line"
done

# Demonstrating the subshell issue:
count=0
printf '%s\n' "a" "b" "c" | while read -r item; do
    count=$((count + 1))
done
echo "Count after piped loop: $count (still 0 due to subshell)"
Output:
Processing lines from a command:
  Got: first
  Got: second
  Got: third
Count after piped loop: 0 (still 0 due to subshell)

The shift command removes $1 and shifts remaining arguments down ($2 becomes $1, $3 becomes $2, etc.). Combined with while, this is the standard pattern for processing an unknown number of arguments.

Each shift decreases $# by 1. The loop continues while arguments remain.

Edit
#!/bin/sh
set -- apple banana cherry date
echo "Processing $# arguments:"

while [ $# -gt 0 ]; do
    echo "  Current arg: $1 (remaining: $#)"
    shift
done

echo "Done. Arguments remaining: $#"
Output:
Processing 4 arguments:
  Current arg: apple (remaining: 4)
  Current arg: banana (remaining: 3)
  Current arg: cherry (remaining: 2)
  Current arg: date (remaining: 1)
Done. Arguments remaining: 0

Retry logic is a common pattern for operations that may fail temporarily, like network requests or file locks. The loop attempts the operation up to a maximum number of times, exiting early on success.

In real scripts, you would typically add sleep between retries to avoid hammering a resource.

Edit
#!/bin/sh
max_retries=3
attempt=0
success=false

while [ "$attempt" -lt "$max_retries" ]; do
    attempt=$((attempt + 1))
    echo "Attempt $attempt of $max_retries"

    # Simulate: fail twice, succeed on third try
    if [ "$attempt" -eq 3 ]; then
        success=true
        echo "  Success!"
        break
    fi

    echo "  Failed, will retry..."
done

if [ "$success" = true ]; then
    echo "Operation completed successfully"
else
    echo "All $max_retries attempts failed"
fi
Output:
Attempt 1 of 3
  Failed, will retry...
Attempt 2 of 3
  Failed, will retry...
Attempt 3 of 3
  Success!
Operation completed successfully

Reading lines safely requires two techniques:

  • IFS= prevents trimming leading/trailing whitespace
  • -r prevents backslash interpretation

The || [ -n "$line" ] handles files that don’t end with a newline - without it, the last line would be skipped.

Note: This example uses a here-document (<<'EOF') to provide input. Here-documents are covered in a later section.

Edit
#!/bin/sh
while IFS= read -r line || [ -n "$line" ]; do
    echo "Line: '$line'"
done <<'EOF'
  indented line
line with trailing space
normal line
EOF
Output:
Line: '  indented line'
Line: 'line with trailing space'
Line: 'normal line'

« For Loops | Functions »