Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 17 · Loops and Iteration

break, continue and Leaving a Loop Early

Two keywords, almost no syntax, and one consequence that catches careful people: a loop that broke out early reports success, exactly like a loop that ran to the end. So the statement that stops your search cannot also tell the caller whether the search succeeded.

Intermediate17 min readLinuxPart IBashLoopsExit StatusScripting

Part I · Module 17 · Chapter 17.6 · break, continue and Leaving a Loop Early

Every loop so far ran to the end of its list or until its condition flipped. Often you want out sooner — you found what you were looking for, or this item is not worth processing.

1. The Two Keywords

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in 1 2 3 4 5; do [ "$i" -eq 3 ] && break; printf '%s ' "$i"; done; echo
1 2

break leaves the loop immediately. Nothing after it in the body runs, and no further iterations happen.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in 1 2 3 4 5; do [ "$i" -eq 3 ] && continue; printf '%s ' "$i"; done; echo
1 2 4 5

continue abandons this iteration and starts the next one. Nothing after it in the body runs for this pass; the loop itself carries on.

Both work identically in for, for (( )), while and until. And both are only meaningful inside a loop:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ bash -c 'break'
bash: line 0: break: only meaningful in a `for', `while', or `until' loop

2. continue Is the Filter Idiom

By far the most common use. Rather than wrapping the whole body in an if, reject the items you do not want at the top and let the rest fall through:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf '# comment\n\nreal_test\n# another\nsecond_test\n' > tl.txt
$ while IFS= read -r l; do
>   case $l in ""|\#*) continue ;; esac
>   echo "RUN $l"
> done < tl.txt
RUN real_test
RUN second_test

Compare it with the alternative:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
while IFS= read -r l; do
  if [ -n "$l" ] && [ "${l:0:1}" != "#" ]; then
    echo "RUN $l"              # the real work, now indented inside a condition
  fi
done < tl.txt

Both are correct. The first is better for the same reason an early return is better than a nested if: the guards are listed at the top, each on its own line, and the body's real work sits at the outer indentation level where it belongs. Chapter 17.2's [ -e "$f" ] || continue is the same pattern, and so is the whole top of Chapter 16.4's summarise.sh.

Stack them freely — one guard per line, each with a reason:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for f in ./*.log; do
  [ -e "$f" ] || continue          # the glob matched nothing
  [ -f "$f" ] || continue          # a directory named *.log
  [ -s "$f" ] || continue          # the tool died before writing
  [ -r "$f" ] || continue          # someone else's file
  process "$f"
done

Five lines, five distinct reasons to skip, and the eye goes straight to process "$f".

3. break N and continue N

Both take an optional count saying how many enclosing loops to act on. Measured over a two-by-three nest:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in a b; do for j in 1 2 3; do [ "$j" -eq 2 ] && break;      printf '%s%s ' "$i" "$j"; done; done; echo
a1 b1

$ for i in a b; do for j in 1 2 3; do [ "$j" -eq 2 ] && break 2;    printf '%s%s ' "$i" "$j"; done; done; echo
a1

$ for i in a b; do for j in 1 2 3; do [ "$j" -eq 2 ] && continue 2; printf '%s%s ' "$i" "$j"; done; done; echo
a1 b1

Read those three carefully, because the first and third have identical output for different reasons:

  • break left the inner loop only. The outer loop went on to b, which ran its own inner loop and also stopped at j=2. So: a1 b1.
  • break 2 left both loops. Nothing after a1 ran at all.
  • continue 2 skipped the rest of the outer iteration, which is the rest of the inner loop plus anything after it. Same output as plain break here — and different the moment there is code after the inner loop:
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in a b; do
>   for j in 1 2 3; do [ "$j" -eq 2 ] && continue 2; printf '%s%s ' "$i" "$j"; done
>   printf '[end-%s] ' "$i"
> done; echo
a1 b1
$ for i in a b; do
>   for j in 1 2 3; do [ "$j" -eq 2 ] && break; printf '%s%s ' "$i" "$j"; done
>   printf '[end-%s] ' "$i"
> done; echo
a1 [end-a] b1 [end-b]

Now they differ: continue 2 skipped the [end-] line, break did not.

4. A Loop That Broke Reports Success

Here is the measurement that matters.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in 1 2 3; do [ "$i" -eq 2 ] && break; done; echo "status=$?"
status=0

Fine — but so does this, where the body's real work failed on every pass:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in 1 2 3; do if [ "$i" -eq 2 ]; then break; fi; false; done; echo "status=$?"
status=0

The last thing to run was break, which succeeded. A loop's exit status after break is 0, unconditionally.

So break cannot report an outcome, and the natural-looking search loop is wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# WRONG — reports success whether or not it found anything
for f in "$@"; do
  if grep -q "$pattern" "$f"; then break; fi
done

That exits 0 for a match and 0 for no match. The caller cannot tell.

The fix is a flag, and a test after the loop:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ found=0
$ for f in a b target d; do if [ "$f" = target ]; then found=1; break; fi; done
$ [ "$found" -eq 1 ]; echo "status=$? (0 means found)"
status=0 (0 means found)

$ found=0
$ for f in a b c d; do if [ "$f" = target ]; then found=1; break; fi; done
$ [ "$found" -eq 1 ]; echo "status=$? (1 means not found)"
status=1 (1 means not found)

The bare [ "$found" -eq 1 ] on the last line is Chapter 16.7's ending: in a script it is the return value, with no exit needed.

This is the same lesson as Chapter 17.1 §8 — a loop's status tells you about its last iteration and nothing about its work — arriving from the other direction. Whenever a loop's outcome matters to the caller, carry it in a variable. break controls flow; it does not carry meaning.

5. A Complete Script

A search across a filelist, with every guard and a real answer at the end:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
# find-first.sh — report the first file in a list containing a pattern
# usage: find-first.sh <pattern> <filelist>
#
# exit: 0 found (path on stdout) · 1 not found · 2 unusable arguments

set -o nounset

pattern=${1:-}
list=${2:-}

if [ -z "$pattern" ] || [ -z "$list" ]; then
  echo "usage: ${0##*/} <pattern> <filelist>" >&2
  exit 2
fi
if ! [ -f "$list" ] || ! [ -r "$list" ]; then
  echo "${0##*/}: $list is not a readable file" >&2
  exit 2
fi

found=""
skipped=0
searched=0

while IFS= read -r -u 3 f || [ -n "$f" ]; do

  # the filter guards: blank lines and comments are not files
  case $f in ""|\#*) continue ;; esac

  # a named file that is not usable is worth reporting, not hiding
  if ! [ -f "$f" ]; then
    printf '%s: no such file: %s\n' "${0##*/}" "$f" >&2
    skipped=$(( skipped + 1 ))
    continue
  fi
  if ! [ -r "$f" ]; then
    printf '%s: not readable: %s\n' "${0##*/}" "$f" >&2
    skipped=$(( skipped + 1 ))
    continue
  fi

  searched=$(( searched + 1 ))

  if grep -q -- "$pattern" "$f"; then
    found=$f
    break
  fi
done 3< "$list"

if (( skipped > 0 )); then
  printf '%s: %d of %d listed files were skipped\n' \
    "${0##*/}" "$skipped" "$(( searched + skipped ))" >&2
fi

if [ -n "$found" ]; then
  printf '%s\n' "$found"
  exit 0
fi

printf '%s: no match in %d files\n' "${0##*/}" "$searched" >&2
exit 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf 'UVM_ERROR : x\n' > a.log
$ printf 'TEST PASSED\n'   > b.log
$ printf 'UVM_ERROR : y\n' > c.log
$ printf '# the logs\n\na.log\nb.log\nmissing.log\nc.log\n' > files.txt

$ ./find-first.sh TEST files.txt
b.log
$ echo $?
0

$ ./find-first.sh TEST files.txt 2>/dev/null
b.log

$ ./find-first.sh NEVERMATCH files.txt
find-first.sh: no such file: missing.log
find-first.sh: 1 of 4 listed files were skipped
find-first.sh: no match in 3 files
$ echo $?
1

$ ./find-first.sh UVM_ERROR files.txt
a.log
$ echo $?
0

$ ./find-first.sh TEST /nope
find-first.sh: /nope is not a readable file
$ echo $?
2

Note what the TEST search did not print: it found b.log on the second file and broke, so it never reached missing.log and never reported the skip. That is correct — a search that succeeded has no business complaining about files it did not need to look at — and it is only correct because the skip message lives after the loop, guarded by a counter, rather than inside it.

Four decisions from this chapter:

case $f in ""|\#*) continue ;; esac filters the two kinds of line that are not files. Chapter 16.6's pattern branch, used as a guard, which is cheaper to read than two string tests.

A missing file is reported, not silently skipped. The §2 callout's rule: a continue for something that is legitimately not your business is silent; a continue for something wrong gets a line on stderr and a counter.

found=$f before break. The flag carries the answer and doubles as the found/not-found signal, so there is no separate boolean. [ -n "$found" ] after the loop is the real test — because §4 measured that the loop's own status is 0 regardless.

grep -q -- "$pattern" has -- before the pattern so a pattern beginning with a dash is a pattern and not an option — Chapter 1.5's convention, and the same defence Chapter 17.2 applied to file names.

Practice

  1. Run the break and continue loops over 1 2 3 4 5 and confirm 1 2 and 1 2 4 5. Then move the printf before the test in each and explain both new outputs.
  2. Build the two-by-three nest and run plain break, break 2 and continue 2. Then add a printf '[end-%s] ' after the inner loop and re-run all three. Which two were identical before and differ now?
  3. Run bash -c 'break'. What does the message tell you about where break is resolved?
  4. Write the wrong search loop from §4 — if grep -q ...; then break; fi with no flag — and show that it exits 0 for both a match and no match.
  5. Fix it with a flag. Then make the script print the matching file's name as well, and say why one variable can do both jobs.
  6. Write a loop with five continue guards over a hostile directory. Now add a counter to exactly one of them and justify which.
  7. Take find-first.sh and move the skip-report printf inside the loop. Run the TEST search and describe what the user now sees that they should not.
  8. Replace find-first.sh's break with exit 0 directly. What still works, what breaks, and what would break if this were a function instead of a script? Name the module that covers it.

Check Your Understanding

  1. What is the difference between break and continue?
  2. In a two-deep nest, plain break and continue 2 printed the same thing. Construct the one-line change that makes them differ.
  3. What does the number in break 2 count?
  4. A loop breaks out early. What is its exit status?
  5. Why can't break report whether a search succeeded?
  6. When should a continue be silent, and when should it print?
  7. Why grep -q -- "$pattern" rather than grep -q "$pattern"?

6. Remember

  • break leaves the loop; continue starts the next iteration. Both abandon the rest of the body, and both work in every loop form.
  • continue at the top is the filter idiom. One guard per line keeps the real work at the outer indentation level.
  • A silent continue hides a problem. Skip quietly only what is legitimately not your business; otherwise print to stderr and count it.
  • break N and continue N count enclosing loops, not if or case levels.
  • Beyond break 2, use a function. A numeric jump out of three loops is a goto.
  • A loop that broke reports status 0 — unconditionally, because break itself succeeded.
  • So break cannot report an outcome. Carry it in a variable and test it after done.
  • One variable can hold both the answer and the signal: found=$f, then [ -n "$found" ].
  • Report a skip after the loop, not inside it, so a successful early break does not complain about files it never needed.

Chapter 17.7 closes the module with the behaviour that makes experienced people doubt their own eyes: a counter that increments correctly on every pass and reads zero the moment the loop ends.

Where this fits

Part of the Linux & Bash curriculum.