CodeRigor

LESSON

Repeat steps with for and while

Repeat a block for each value in a list or range with for, repeat while a condition holds with while, keep counters and running totals, and avoid off-by-one mistakes.

Computers are good at doing the same thing many times without getting bored or making slips. A loop is a statement that runs a block of statements repeatedly. Bash has two kinds you will use all the time: for, which repeats once for each value in a list, and while, which repeats as long as a condition is true.

for: once for each value in a list

Bash
for name in Ada Grace Alan; do
  echo "Hello, $name!"
done

This prints:

Text
Hello, Ada!
Hello, Grace!
Hello, Alan!

Read the pieces in order:

  • for name in names a variable, here name, called the loop variable.
  • Ada Grace Alan is the list of values, separated by spaces.
  • ; do starts the block and done ends it. The block between them is called the body of the loop.

Bash sets name to the first value and runs the body, then sets it to the second value and runs the body again, and so on. When the list is used up, the script continues after done. As with if, indent the body so that a reader can see what is repeated.

Counting with a range

To repeat a fixed number of times, let Bash write the list of numbers. {1..5} is replaced by 1 2 3 4 5.

Bash
for i in {1..5}; do
  echo "Lap $i"
done
echo "Finished"

This prints Lap 1 to Lap 5, one per line, and then Finished. A variable that counts the rounds of a loop is called a counter, and i is the traditional short name for one. Finished is printed once, because that line is after done and so is not part of the body.

Running totals

A loop becomes powerful when a variable is carried from one round to the next. To add up numbers, start a total at 0 before the loop and add to it inside:

Bash
total=0
for n in 4 5 6; do
  total=$(( total + n ))
done
echo "Total: $total"

This prints Total: 15. Step through the diagram to watch total grow.

A running total, one round at a time
start / 5 ⇄ code
total starts at 0 before the loop. Each round adds the current value of n to it. After the last round the loop ends and line 5 prints the result.
A running total, one round at a time
total starts at 0 before the loop. Each round adds the current value of n to it. After the last round the loop ends and line 5 prints the result.

Where the starting line goes matters. If total=0 were inside the body, the total would be set back to 0 at the start of every round.

while: repeat as long as a condition is true

A while loop has a condition, written exactly like the condition of an if. Bash tests it; if it is true, the body runs and Bash goes back and tests again. When the condition is false, the loop ends.

Bash
count=3
while [[ "$count" -gt 0 ]]; do
  echo "$count"
  count=$(( count - 1 ))
done
echo "Lift off!"

This prints 3, 2, 1 and then Lift off!.

A while loop goes back and tests again
start / 6 ⇄ code
The condition is tested before every round. While it is true the body runs and Bash goes back to the test. When it is false the loop ends and the script continues after done.
A while loop goes back and tests again
The condition is tested before every round. While it is true the body runs and Bash goes back to the test. When it is false the loop ends and the script continues after done.

The last line of the body, count=$(( count - 1 )), is what makes the loop end. Something in the body must change the value that the condition tests. If you delete that line, count stays 3, the condition stays true and the loop prints 3 for ever. A loop that never ends is called an infinite loop. On this site a script is stopped after a few seconds; at a terminal, hold the Ctrl key and press C to stop one.

Counting up to a number held in a variable

Often the number of rounds is not known when you write the script: the user types it. The range form cannot do this. {1..$n} is not replaced by a list of numbers:

Bash
n=3
for i in {1..$n}; do
  echo "Lap $i"
done

This prints the single line Lap {1..3}. Bash turns ranges into lists before it replaces variables, so at that moment it sees $n where a number should be and leaves the text alone. There is no error message.

A while loop with a counter does the job. Start the counter at 1, repeat while it is less than or equal to the limit, and add 1 at the end of each round:

Bash
n=3
i=1
while [[ "$i" -le "$n" ]]; do
  echo "Lap $i"
  i=$(( i + 1 ))
done

This prints Lap 1, Lap 2 and Lap 3.

Counting is so common that for has a second form that puts the three counter parts on one line:

Bash
n=3
for (( i = 1; i <= n; i++ )); do
  echo "Lap $i"
done

This prints the same three lines. Between the double round brackets are three parts separated by semicolons: where the counter starts (i = 1), the condition for another round (i <= n), and what to do after each round (i++, which adds 1 to i). These brackets follow the rules of $(( )): spaces are allowed, names need no $, and numbers are compared with <, <=, > and >=. Use whichever of the two forms you find clearer.

Off-by-one mistakes

The most common loop mistake is running one round too few or one too many. It is called an off-by-one mistake, and it comes from the choice between -lt and -le and from where the counter starts.

Bash
i=1
while [[ "$i" -lt 3 ]]; do
  echo "Lap $i"
  i=$(( i + 1 ))
done

This prints only Lap 1 and Lap 2: when i reaches 3, "3 is less than 3" is false. For three laps the test must be -le 3. When a loop is wrong by one, check the first round and the last round by hand: what is the counter the first time the body runs, and what is it the last time the test is true?

Repeating until the user says stop

A condition can depend on what the user types. This script adds up numbers until the user types 0:

Bash
total=0
echo "Number? (0 to stop)"
read -r number
while [[ "$number" -ne 0 ]]; do
  total=$(( total + number ))
  echo "Number? (0 to stop)"
  read -r number
done
echo "Total: $total"

With 5, 7 and 0 typed, it asks three times and prints Total: 12. Notice the shape: read once before the loop so that the condition has something to test, and read again at the end of the body so that the next test sees a new value. Try it in the Bash playground with one number per line of Standard input, ending with 0.

Error messages you will meet

A missing done.

Bash
for i in {1..3}; do
  echo "Lap $i"
Text
solution.sh: line 3: syntax error: unexpected end of file from `for' command on line 1

This is the same message as a missing fi. The second line number, line 1, points at the loop that was never closed.

A missing do.

Bash
i=1
while [[ "$i" -le 3 ]]
  echo "Lap $i"
  i=$(( i + 1 ))
done
Text
solution.sh: line 5: syntax error near unexpected token `done'
solution.sh: line 5: `done'

Bash met done while it was still waiting for do. As with a missing then, the named line is fine and the mistake is above it: line 2 needs ; do at its end.

What you can do now

You can repeat a block for each value in a list or range, repeat while a condition holds, keep a counter and a running total, and check the first and last rounds for off-by-one mistakes. In the exercises you will predict what loops print, then write a script that adds up as many numbers as the user asks for.

Next: write functions and finish a complete program.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →