# Stopping Early: break, continue and Counters

A counter or running total is a variable a loop adds to each time round with $(( )); continue skips the rest of one pass and goes on with the next item, and break leaves the loop early.

Apprentice · Unix · Linux · Updated November 10, 2026

## After this lesson you can

- Keep a count or a running total in a loop with $(( ))
- Skip the rest of one pass with continue, and leave a loop early with break

## Key takeaways

- Start a counter at 0 before the loop and add to it inside the loop: count=$((count + 1)).
- continue skips the rest of this pass and goes on with the next item.
- break leaves the loop at once, and the script carries on after done.
- [[ -e $f ]] || continue skips the pattern itself when a wildcard matched no files, so a loop is safe to run twice.

The drive to the coast takes 20 minutes, and you want music that fills it without running over, skipping the songs you crossed out. A cashier's register keeps a running total as each item is scanned; a script that adds up an expense file or counts the errors in a log does the same. Real files also have notes, blank lines and a point where you've seen enough. This lesson gives a loop three tools: a counter, a way to skip one item, and a way to stop.

Make a practice folder with a playlist. Each line is a song's length in minutes, then its title; one song is crossed out with a `#`, and there's a blank line:

```bash
mkdir -p ~/terminal-practice/road-trip
cd ~/terminal-practice/road-trip
printf '%s\n' '4 Highway Tea' '3 Paper Lanterns' '# 6 Rainy Tuesday (too slow for the car)' '5 Window Down' '' '4 Gas Station Sunrise' '6 Last Exit Waltz' '3 Home by Dark' > playlist.txt
```

Given two names, `read -r minutes title` splits a line: the first word goes into `minutes` and the rest into `title`.

```bash
read -r minutes title <<< '4 Highway Tea'
echo "$minutes minutes: $title"
```

```text
4 minutes: Highway Tea
```

There's no `IFS=` in this `read` (*Line by Line: while read*), because this time you want the split. `<<<` (read "here-string") gives `read` one line of text to read.

## Counters: keeping count as you go

A **counter** is a variable a loop adds to each time round, like a tally on a scorecard. Start it at 0 before the loop, and add to it inside with `$(( ))` (*Values from Commands: $( ) and $(( ))*). A running total works the same way:

```bash
count=0
total=0
while read -r minutes title; do
  count=$((count + 1))
  total=$((total + minutes))
done < playlist.txt
echo "$count songs, $total minutes"
```

```text
bash: #: arithmetic syntax error: operand expected (error token is "#")
3 songs, 7 minutes
```

Something broke. `$(( ))` adds numbers, and the third line's first word is `#`. The error stopped the loop right there, after three songs, and the blank line would have tripped it too. The loop needs to skip lines that aren't songs.

## continue: skip this one

`continue` skips the rest of the commands for this item and goes on with the next one, the way a cashier sets aside a coupon that doesn't scan and carries on with the next item. Add a check at the top of the loop:

```bash
count=0
total=0
while read -r minutes title; do
  if [[ $minutes == "" || $minutes == "#"* ]]; then
    continue
  fi
  count=$((count + 1))
  total=$((total + minutes))
done < playlist.txt
echo "$count songs, $total minutes"
```

```text
6 songs, 25 minutes
```

Read the test aloud: "minutes is empty, or minutes starts with #". Inside `[[ ]]`, `||` means "or", and `==` with a `*` outside the quotes matches a pattern, the way a wildcard matches file names: `"#"*` is "a # and then anything".

## break: stop here

`break` leaves the loop at once, and the script carries on after `done`. Turn the picker into a small application, `pick-songs.sh`, that takes the drive's length in minutes as `$1` (*Arguments: $1, $# and "$@"*) and stops when the next song wouldn't fit. Write it with `nano pick-songs.sh`:

```bash
#!/usr/bin/env bash
# Picks songs from playlist.txt, in order, until the drive is full.
# Usage: ./pick-songs.sh MINUTES
limit=$1
count=0
total=0
while read -r minutes title; do
  if [[ $minutes == "" || $minutes == "#"* ]]; then
    continue
  fi
  if [[ $((total + minutes)) -gt $limit ]]; then
    break
  fi
  count=$((count + 1))
  total=$((total + minutes))
  echo "$count. $title ($minutes min)"
done < playlist.txt
echo "$count songs, $total of $limit minutes"
```

Make it runnable, and plan two drives:

```bash
chmod +x pick-songs.sh
./pick-songs.sh 20
./pick-songs.sh 12
```

```text
1. Highway Tea (4 min)
2. Paper Lanterns (3 min)
3. Window Down (5 min)
4. Gas Station Sunrise (4 min)
4 songs, 16 of 20 minutes
1. Highway Tea (4 min)
2. Paper Lanterns (3 min)
3. Window Down (5 min)
3 songs, 12 of 12 minutes
```

On the 20-minute drive, *Last Exit Waltz* would make 22 minutes, so the loop stops there and *Home by Dark* is never read. `break` and `continue` work the same in `for`, `while` and `until` loops.

## Back at Mission Control: safe to run twice

The renaming loop in *For Every File: for Loops* complained when there was nothing left to rename, because bash handed it the pattern itself. `continue` fixes that: when no file matched, `$f` is the pattern, which isn't a real file, so skip it. A new telemetry file has just come in:

```bash
cd ~/terminal-practice/mission-control
touch telemetry/orbit-13.tmp
for f in telemetry/*.tmp; do
  [[ -e $f ]] || continue
  mv "$f" "${f%.tmp}.log"
  echo "Renamed $f"
done
```

```text
Renamed telemetry/orbit-13.tmp
```

Read `[[ -e $f ]] || continue` as "the file exists, or else skip it" (*Checking Before You Act: File Tests*). Run the same loop again and it prints nothing at all, with no error: the pattern matched no file, so `continue` skipped the `mv`.

## Check yourself

2 questions to try on the lesson page.

## So what

Counters, `continue` and `break` turn a loop that does the same thing to everything into one that keeps score, skips what doesn't belong and knows when it's done, which is what makes a script a tool you can trust with real files.

## Key terms

- [Loop Counter](https://www.ask-codey.com/glossary/loop-counter-unix)
- [break](https://www.ask-codey.com/glossary/break-unix)
- [continue](https://www.ask-codey.com/glossary/continue-unix)
- [Shell Script](https://www.ask-codey.com/glossary/shell-script)

## Cheat sheets

- [Stopping Early: break, continue and Counters](https://www.ask-codey.com/cheat-sheets/bash-loop-control-cheat-sheet)

## Related

- [For Every File: for Loops](https://www.ask-codey.com/lessons/bash-for-loops)
- [Line by Line: while read](https://www.ask-codey.com/lessons/bash-while-read)
- [Values from Commands: $( ) and $(( ))](https://www.ask-codey.com/lessons/bash-command-substitution)
- [Arguments: $1, $# and "$@"](https://www.ask-codey.com/lessons/bash-script-arguments)

Source: https://www.ask-codey.com/lessons/bash-loop-control
