awk Part II · Module 5 · Variables, Operators, and Control Flow
next and exit
Every rule so far ran to completion. This chapter is about stopping early — abandoning the current record with next, or ending the whole run with exit. Both are simple and both have a consequence that is not: next skips the remaining rules, which is what makes a chain of filters mutually exclusive, and exit stops reading input while still running END, which means a summary written in END can report a partial count in a sentence that reads exactly like a finished one. The chapter closes on the exit status, and on the reason a script that prints the word FAIL four times still tells the shell that everything succeeded.
Intermediate16 min readawkPart IIControl FlowExit StatusAutomation
Part II · Module 5 · Chapter 5.6 · next and exit
1. Why This Matters
Chapter 5.5 covered break and continue, which control a loop. This chapter covers the two statements that control record processing, which is a different scope and the source of most of the confusion:
nextabandons the current record and moves to the next one.exitstops reading input altogether and goes toEND.
Both earn their place in this curriculum for reasons beyond syntax. next is what made Chapter 5.4's three-category classifier mutually exclusive rather than merely careful. And exit interacts with END in a way that can turn a correct summary into a confidently wrong one — section 8 — which is the most consequential thing in the chapter.
The last section is about the exit status, which is how awk talks to the script that called it, and which almost nothing you print has any effect on.
2. next — Abandon This Record
next stops processing the current record and fetches the following one:
alu PASS
fifo FAIL
uart PASS
spi FAILawk '$2 == "PASS" { next } { print "failing:", $1 }' results.txtfailing: fifo
failing: spiReading it: for a passing record the first rule matches and next fires, so the second rule is never reached. For a failing record the first rule does not match, so execution falls through to the second.
The effect is "skip the records I do not care about", and it reads better than the negated condition in many cases — especially when there are several reasons to skip.
3. What next Actually Skips
The precise meaning is "skip the remaining rules for this record", and a three-rule program makes it visible:
awk '{ print "rule1:", $1 } $2 == "PASS" { next } { print "rule3 (failures only):", $1 }' results.txtrule1: alu
rule1: fifo
rule3 (failures only): fifo
rule1: uart
rule1: spi
rule3 (failures only): spiRule 1 ran for all four records — it comes before the next. Rule 3 ran only for the two failing ones.
So next does not skip the record entirely; it skips whatever is below it in the program. Rules already executed have already had their effect, which matters when one of them incremented a counter.
This is Chapter 2.3's source-order model again, and next is the one statement that interrupts it.
There is also nextfile, which abandons the rest of the current file and moves to the next one on the command line. This awk supports it, it is genuinely useful for "check the first record of each file and move on", and it is not in POSIX — so a script using it is not portable, and this chapter does not build on it.
4. The Skip-Comments Idiom
The most common real use of next is discarding lines that are not data. Tool reports are full of them:
# comment
alu PASS
fifo FAILawk '/^#/ { next } NF == 0 { next } { print NR, $0 }' messy.txt2 alu PASS
4 fifo FAILTwo guard rules, each ending in next, then the real work. The record numbers are 2 and 4, which confirms the skipped lines were read and counted — NR counts every record awk reads, not every record that survived the filters (Chapter 5.2).
That distinction is worth holding on to. A summary dividing by NR here would divide by 4, not 2 — the Chapter 5.3 denominator problem arriving through a different door. If you need a count of data records, count them:
awk '/^#/ { next } NF == 0 { next } { data++ } END { printf "%d data records of %d lines read\n", data+0, NR }' messy.txt2 data records of 4 lines readBoth numbers, so neither can be mistaken for the other.
The alternative to next is one compound condition:
awk '!/^#/ && NF > 0 { print NR, $0 }' messy.txt2 alu PASS
4 fifo FAILIdentical output. The next version is preferable when the skip reasons are numerous or when each one needs its own counter — a report that says how many comment lines and how many blanks it discarded cannot be written as a single negated condition.
5. exit — Stop Reading
exit ends input processing immediately:
awk 'NR == 2 { print "stopping at NR =", NR; exit }' results.txtstopping at NR = 2Records 3 and 4 were never read. Chapter 2.1 used exactly this to prove that awk reads one record at a time rather than slurping the file.
The use case is a search that can stop once it has an answer. On a 2 GB timing report, finding the first violation and stopping is a different operation from reading the whole file.
6. exit Still Runs END
This is the part that surprises people, and it is the foundation of section 8:
awk 'NR == 2 { print "stopping at NR =", NR; exit } END { print "END ran, NR =", NR }' results.txtstopping at NR = 2
END ran, NR = 2END ran. And NR is 2 — the number of records read before the exit, not the 4 records in the file.
So exit means "stop reading and go to END", not "stop everything". That is a deliberate and useful design: it lets a script that stopped early still print a summary, close a report, or set a status.
It is also exactly how a correct-looking summary becomes wrong.
7. exit in BEGIN
The extreme case. exit in BEGIN skips input entirely, and END still runs:
awk 'BEGIN { print "in BEGIN"; exit 7 } { print "read a record" } END { print "END ran, NR =", NR }' results.txt
echo "rc=$?"in BEGIN
END ran, NR = 0
rc=7No record was read — the middle rule never produced a line — and END ran with NR at 0. The status 7 was preserved.
This is the natural place for a precondition check: validate arguments or environment in BEGIN, and exit with a status if something is missing. Just be aware that END will still run, so an END that prints a summary needs the NR == 0 guard Chapter 5.1 introduced, or it will print a summary of nothing.
8. The Partial Count
Now the danger, and it is the sharpest form of a problem this module keeps returning to.
A correct failure summary over the whole file:
awk '$2 == "FAIL" { f++ } END { printf "%d failures of %d tests\n", f+0, NR }' results.txt2 failures of 4 testsTwo failures in four tests. Now the same summary in a script that was changed to stop at the first failure — a reasonable-sounding optimisation:
awk '$2 == "FAIL" { f++; exit } END { printf "%d failures of %d tests\n", f+0, NR }' results.txt1 failures of 2 tests1 failures of 2 tests. Both numbers are wrong. There are 2 failures, not 1, and 4 tests, not 2.
And the sentence is grammatically and structurally identical to the correct one. It names a numerator and a denominator, it is internally consistent — 1 really is the count of failures seen, and 2 really is the number of records read — and nothing in it indicates that the run stopped early. Pasted into a regression report, it is indistinguishable from a complete result.
9. What Actually Sets the Exit Status
The chapter's last idea, and it is the one that causes real incidents.
Here is a command reporting two failures:
awk '$2 == "FAIL" { print "FAIL:", $1 }' results.txt
echo "rc=$?"FAIL: fifo
FAIL: spi
rc=0Exit status 0. awk printed the word FAIL twice and told the shell that everything succeeded.
This is correct behaviour and it is worth being explicit about why: awk's exit status reports whether awk succeeded, not what awk found. It read its input, ran its program, and wrote its output, all without error. Status 0 is the truthful answer to that question. The content of stdout is not part of it.
On clean input the same command reports success:
alu PASS
uart PASSawk '$2 == "FAIL" { found = 1 } END { print "found =", found+0; exit (found ? 1 : 0) }' clean.txt
echo "rc=$?"found = 0
rc=0Zero failures, status 0. The two runs are now distinguishable by a script, which is the whole point.
Choosing status values is worth a moment. By convention 0 means success and non-zero means a problem, and the specific value is yours to define — but 1 is also what many tools use for their own errors, so a distinct value makes a script's own failure distinguishable from the condition it was testing for. Chapter 5.4's sign-off summary used 3 for "unscored tests present" for exactly that reason.
10. exit Inside END
exit in END stops END and sets the status. It also overrides a status set earlier:
awk 'END { print "in END"; exit 3 }' results.txt
echo "rc=$?"in END
rc=3And with an earlier exit as well:
awk 'NR == 1 { exit 5 } END { print "END ran" }' results.txt
echo "rc=$?"END ran
rc=5awk 'NR == 1 { exit 5 } END { print "END ran"; exit 9 }' results.txt
echo "rc=$?"END ran
rc=9The first command's exit 5 survived END, giving 5. The second's exit 9 inside END replaced it, giving 9.
So the status is whatever the last exit to execute specified. If a rule exits with a meaningful code and END then exits with its own, the rule's code is lost — which is a real bug in a script where END ends with an unconditional exit 0.
11. next Is Illegal in BEGIN and END
Unlike exit, next cannot appear in either phase:
awk 'END { next }' results.txt 2>&1
echo "rc=$?"awk: illegal break, continue, next or nextfile from END
input record number 4, file results.txt
source line number 1
rc=2awk 'BEGIN { next }' results.txt 2>&1
echo "rc=$?"awk: illegal break, continue, next or nextfile from BEGIN
source line number 1
rc=2A fatal error, status 2, with a clear message. The reason is structural: next means "go to the next record", and neither BEGIN nor END is processing a record, so the instruction is meaningless there.
This is a pleasant case — the error is loud and immediate, unlike most of what this module has covered.
12. break, continue, next, exit
The four-way comparison Chapter 5.5 promised. All four look like the same kind of statement and they act on three different scopes:
| Statement | Scope | Effect | Legal in BEGIN/END? |
|---|---|---|---|
break | the innermost loop | leave the loop, continue the action | yes, inside a loop |
continue | the innermost loop | skip to the loop's next iteration | yes, inside a loop |
next | the current record | skip the remaining rules, read the next record | no — fatal error |
exit N | the whole run | stop reading, run END, set status N | yes |
Reading the scope column top to bottom gives the mental model: loop, loop, record, run. A break inside a field loop does not abandon the record; a next inside a field loop abandons the record and therefore the loop as well.
That last point is worth demonstrating, since it is where the two get confused:
awk '{ for (i = 1; i <= NF; i++) { if ($i == "FAIL") next; print " field", i, "=", $i }; print "finished record", NR }' results.txt field 1 = alu
field 2 = PASS
finished record 1
field 1 = fifo
field 1 = uart
field 2 = PASS
finished record 3
field 1 = spiRecords 1 and 3 printed a finished record line. Records 2 and 4 did not — next fired on their second field and abandoned the record, so the print "finished record" statement after the loop never ran.
With break instead, the record is not abandoned:
awk '{ for (i = 1; i <= NF; i++) { if ($i == "FAIL") break; print " field", i, "=", $i }; print "finished record", NR }' results.txt field 1 = alu
field 2 = PASS
finished record 1
field 1 = fifo
finished record 2
field 1 = uart
field 2 = PASS
finished record 3
field 1 = spi
finished record 4All four records report finished. break left the loop and execution continued with the next statement in the same action.
13. Common Mistakes
Expecting next to skip rules above it. It skips what comes after — section 3.
Dividing by NR after filtering with next. NR counts lines read, not records kept — section 4.
Printing a total in END after an early exit. The total is partial and reads as complete — section 8.
Expecting exit to skip END. It does not, in a rule or in BEGIN.
Expecting printed text to set the exit status. Only exit N does — section 9.
Ending END with an unconditional exit 0. It overrides a meaningful status set earlier — section 10.
Using next in BEGIN or END. A fatal error, status 2.
Confusing next with break inside a loop. break leaves the loop; next abandons the whole record and everything after the loop — section 12.
Using 1 as a "found it" status without thought. Many tools use 1 for their own errors; a distinct value is distinguishable.
14. Exercises
Exercise 1 — Predict the output
awk 'NR == 2 { next } { print NR, $1 }' results.txt1 alu
3 uart
4 spiRecord 2 is skipped. Note NR still counts it — the output jumps from 1 to 3, which is the evidence that the record was read and discarded rather than never seen.
Exercise 2 — Skip non-data lines
Print only the data records of the messy file, with a count of each kind of line that was discarded.
# comment
alu PASS
fifo FAILawk '/^#/ { comments++; next } NF == 0 { blanks++; next } { data++; print NR, $0 } END { printf "%d data, %d comments, %d blanks, %d lines total\n", data+0, comments+0, blanks+0, NR }' messy.txt2 alu PASS
4 fifo FAIL
2 data, 1 comments, 1 blanks, 4 lines totalThe three categories sum to 4, which is the arithmetic check that no line was silently lost — the same self-auditing shape as Chapter 5.4's three-category classifier.
Exercise 3 — exit and END
Predict both lines before running.
awk 'NR == 1 { exit } END { print "END ran with NR =", NR }' results.txtEND ran with NR = 1END runs despite the exit, and NR is 1 — the single record that had been read.
Exercise 4 — Debug a partial summary
A nightly job reports 1 failures of 2 tests for a file containing four tests, two of which failed. Find the bug.
alu PASS
fifo FAIL
uart PASS
spi FAILawk '$2 == "FAIL" { f++; exit } END { printf "%d failures of %d tests\n", f+0, NR }' results.txt1 failures of 2 testsThe exit fires on the first failing record, so awk stops reading. f is 1 because one failure was seen and NR is 2 because two records were read. Both numbers are honest about what happened and both are wrong as a summary of the file.
Removing the exit restores the real answer:
awk '$2 == "FAIL" { f++ } END { printf "%d failures of %d tests\n", f+0, NR }' results.txt2 failures of 4 testsThe deeper lesson is that the broken output was plausible. If the file had happened to contain two tests with one failure, the two commands would agree and the bug would be invisible until the data changed.
Exercise 5 — Make the exit status meaningful
Write a check that a calling shell script can act on: status 0 if every test passed, status 1 if any failed.
awk '$2 == "FAIL" { found = 1 } END { exit (found ? 1 : 0) }' results.txt
echo "rc=$?"rc=1awk '$2 == "FAIL" { found = 1 } END { exit (found ? 1 : 0) }' clean.txt
echo "rc=$?"alu PASS
uart PASSrc=0Two different statuses from the same command on different data, which is what makes it usable in a conditional.
Contrast with the version that only prints:
awk '$2 == "FAIL" { print "FAIL:", $1 }' results.txt
echo "rc=$?"FAIL: fifo
FAIL: spi
rc=0Two failures reported and status 0. A CI step running this records success.
Exercise 6 — Engineering application
Produce a regression gate: a human-readable summary on stdout and a status a pipeline can branch on, distinguishing "all passed", "some failed", and "the file had no records at all".
awk '$2 == "FAIL" { f++ } END { if (NR == 0) { print "ERROR: no records read"; exit 2 } printf "%d failures of %d tests\n", f+0, NR; exit (f ? 1 : 0) }' results.txt
echo "rc=$?"2 failures of 4 tests
rc=1awk '$2 == "FAIL" { f++ } END { if (NR == 0) { print "ERROR: no records read"; exit 2 } printf "%d failures of %d tests\n", f+0, NR; exit (f ? 1 : 0) }' clean.txt
echo "rc=$?"0 failures of 2 tests
rc=0: > empty.txt
awk '$2 == "FAIL" { f++ } END { if (NR == 0) { print "ERROR: no records read"; exit 2 } printf "%d failures of %d tests\n", f+0, NR; exit (f ? 1 : 0) }' empty.txt
echo "rc=$?"ERROR: no records read
rc=2Three distinct statuses for three distinct situations. The third is the one most scripts omit, and it is the one that matters most — an empty results file means the regression did not run, which is a different problem from a regression that ran and failed, and a summary reading 0 failures of 0 tests with status 0 would report it as a clean pass.
Exercise 7 — Challenge: next against break in a loop
Predict both outputs, then explain the difference in terms of scope.
awk '{ for (i = 1; i <= NF; i++) { if ($i == "FAIL") next; print " field", i, "=", $i }; print "finished record", NR }' results.txt field 1 = alu
field 2 = PASS
finished record 1
field 1 = fifo
field 1 = uart
field 2 = PASS
finished record 3
field 1 = spiawk '{ for (i = 1; i <= NF; i++) { if ($i == "FAIL") break; print " field", i, "=", $i }; print "finished record", NR }' results.txt field 1 = alu
field 2 = PASS
finished record 1
field 1 = fifo
finished record 2
field 1 = uart
field 2 = PASS
finished record 3
field 1 = spi
finished record 4next abandons the record, so the print "finished record" after the loop is skipped too — only records 1 and 3 report finishing. break abandons only the loop, so execution continues with the next statement in the same action and all four records report.
The scope rule in one sentence: break ends the loop, next ends the record, exit ends the run. Choosing between them is choosing how much to abandon.
15. Summary
nextabandons the current record and skips the remaining rules — rules above it have already run.nextis the idiom for discarding non-data lines, and it is what makes a chain of classification rules mutually exclusive.NRcounts lines read, not records kept. After filtering withnext, it is the wrong denominator.exitstops reading input and still runsEND, withNRholding a partial count.exitinBEGINreads no input at all, andENDstill runs withNRat 0.- An early
exitinvalidates every total inEND— and1 failures of 2 testsreads exactly like a complete summary. - Nothing you print affects the exit status. Printing
FAILleaves it at 0, which is how a broken regression passes a CI gate. - Only
exit Nsets the status, and the lastexitto run decides it — so an unconditionalexit 0inENDdiscards a meaningful code set earlier. nextis a fatal error inBEGINorEND, status 2.breakends the loop,nextends the record,exitends the run. Three scopes, four statements.
That completes Module 5, and with it the control flow of the language. You can hold state across records, read what awk reports about the input, compute reliably and say how much data the computation had, branch on a condition without being fooled by a malformed field, repeat over a record of unknown width, and stop early — telling the calling script what happened.
Every accumulator so far has been a single variable, which means a per-category total needs a separate variable per category, named in advance. Module 6 — Arrays, Functions, and Aggregation removes that limit: one array indexed by any string at all, which is how awk counts occurrences of values it has never seen, groups records by a field, and produces the per-instance and per-corner breakdowns that col[i] in Chapter 5.5 only hinted at. It closes with functions of your own, where awk has rules that surprise everyone: arrays pass by reference, scalars by value, and the only local variables you get are extra parameters you declare and never pass.
Where this fits
Part of the awk curriculum.
