awk Part II · Module 5 · Variables, Operators, and Control Flow
if and else
Chapters 5.1 and 5.3 kept reaching for if before it had been introduced — to seed a tracker, to guard a divisor, to reject a malformed measurement. This chapter covers it properly: the branch forms, the chain, the boundary condition that decides whether a score of exactly 80 passes, and the missing pair of braces that changes which statements are conditional while leaving the printed lines identical. Then the finding that makes this the most important chapter in the module to get right — a test with no recorded score, classified by a perfectly ordinary comparison as PASS.
Foundation18 min readawkPart IIControl FlowConditionalsData Quality
Part II · Module 5 · Chapter 5.4 · if and else
1. Why This Matters
Module 4 taught you to select records with patterns. A pattern decides whether an action runs. What it cannot do is choose which of two actions runs.
That is what if is for, and the difference is the subject of section 10. Everything that classifies rather than filters needs it: PASS or FAIL, within budget or over, analysed or skipped, usable measurement or not.
The chapter has two hazards worth the price of admission. One is a pair of braces whose absence changes a count from 2 to 4 while the printed output stays identical. The other is section 9, where an ordinary comparison against an ordinary report column reports a test that was never scored as having passed — and where the direction of the error turns out to depend on which character the tool vendor chose to mean "no data".
2. The Simplest Form
if (condition) statement runs the statement when the condition is true:
awk 'BEGIN { s = 95; if (s >= 80) print "PASS" }'PASSThe parentheses around the condition are required — unlike in some languages, awk will not accept if s >= 80.
If the condition is false, nothing happens and there is no output. Chapter 5.1's rule about truth applies here in full: zero and the empty string are false, everything else is true, and a field containing 0 is false while the string "0" is true.
3. if and else
else supplies the alternative:
alu 95
fifo 72
uart 88
spi 45awk '{ if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores.txtalu PASS
fifo FAIL
uart PASS
spi FAILEvery record produces exactly one line, because the two branches are mutually exclusive and together cover every case.
Note the semicolon before else. awk needs a statement terminator there; print $1, "PASS" else … is a syntax error. This is the single most common typo in awk conditionals.
4. The else if Chain
More than two categories means chaining. else if is not special syntax — it is an if that happens to be the statement belonging to an else:
awk '{ if ($2 >= 90) print $1, "A"; else if ($2 >= 80) print $1, "B"; else if ($2 >= 70) print $1, "C"; else print $1, "F" }' scores.txtalu A
fifo C
uart B
spi F95 → A, 72 → C, 88 → B, 45 → F.
The chain is tested top to bottom and stops at the first match, which is why the order of the thresholds matters and why they are written descending. Reverse them and everything collapses into the first branch:
awk '{ if ($2 >= 70) print $1, "C"; else if ($2 >= 80) print $1, "B"; else if ($2 >= 90) print $1, "A"; else print $1, "F" }' scores.txtalu C
fifo C
uart C
spi FThree Cs. alu scored 95, which is >= 70, so the first branch claimed it and the >= 90 test was never reached. The A and B branches are now dead code — unreachable for any input.
That is a genuine class of bug rather than a curiosity. It produces plausible output, every record is still classified, and nothing warns you that two of your four categories can never occur. The check is mechanical: in a descending chain each threshold must be strictly higher than the one below it.
5. Braces: What Is Actually Conditional
This is the bug most worth internalising, because the symptom is a wrong number rather than a wrong line.
Count the passing tests while printing their names:
awk '{ if ($2 >= 80) print $1; count++ } END { print "count =", count+0 }' scores.txtalu
uart
count = 4Two names printed, and a count of 4. Four records, so count counted all of them.
With braces:
awk '{ if ($2 >= 80) { print $1; count++ } } END { print "count =", count+0 }' scores.txtalu
uart
count = 2Same two names. Count of 2, which is the answer that was wanted.
Without braces, if governs exactly one statement — the print. The count++ after the semicolon is simply the next statement in the action, run unconditionally for every record. awk is doing precisely what was written.
Indentation does not affect this. awk ignores whitespace entirely, so a count++ indented to look like it belongs to the if still runs unconditionally. Only braces group statements.
6. The Boundary
A threshold comparison has to decide what happens at exactly the threshold, and this is where specifications are usually silent.
alu 80
fifo 80awk '{ if ($2 > 80) print $1, "gt80-PASS"; else print $1, "gt80-fail" }' boundary.txtalu gt80-fail
fifo gt80-failawk '{ if ($2 >= 80) print $1, "ge80-PASS"; else print $1, "ge80-fail" }' boundary.txtalu ge80-PASS
fifo ge80-PASSA score of exactly 80 fails a > 80 test and passes a >= 80 test. Both commands are correct awk; they implement different policies.
This matters because "80% coverage required" is ambiguous in English and unambiguous in code. If the sign-off criterion is 80% and a block achieves exactly 80.0%, the two commands above disagree about whether it ships.
The engineering habit: write the boundary into the output so the policy is auditable.
awk '{ if ($2 >= 80) print $1, $2, "PASS (>= 80)"; else print $1, $2, "FAIL (< 80)" }' boundary.txtalu 80 PASS (>= 80)
fifo 80 PASS (>= 80)Now a reader can see which rule produced the verdict without reading the script. On a real sign-off report that is the difference between a number somebody can check and a number they have to trust.
A related trap worth naming: a coverage figure of 79.95% rounds to 80.0% for display and fails a >= 80 test. If the displayed value and the compared value go through different formatting, the report will appear to contradict itself. Compare the raw value and display the raw value, or round both.
7. Conditions Are Expressions
Anything that evaluates to a value can be a condition. The comparison operators from Chapter 4.2 are the common case:
u_a comb
u_b seq
u_c combawk '{ if ($2 == "comb") c++; else s++ } END { print c+0, s+0 }' kinds.txt2 1Two combinational, one sequential.
The logical operators from Chapter 4.3 compose conditions:
awk '{ if ($2 >= 80 && $2 < 90) print $1, "borderline" }' scores.txtuart borderlineAnd a regex match is a condition, from Chapter 4.4:
awk '{ if ($1 ~ /^u_/) print $1, "has the instance prefix" }' kinds.txtu_a has the instance prefix
u_b has the instance prefix
u_c has the instance prefixA bare value is also a condition, and this is where Chapter 5.1's truth rule earns its place:
awk '{ if (NF) print NR, "has", NF, "fields"; else print NR, "is blank" }' kinds.txt1 has 2 fields
2 has 2 fields
3 has 2 fieldsif (NF) reads as "if this record has any fields", because 0 is false. It is idiomatic and compact. Note that it is safe here precisely because NF is a count awk computed — Chapter 5.1's Exercise 6 showed that a bare if (v) on a field asks a different question than you probably mean.
8. The Conditional Expression
condition ? a : b is an expression that yields one of two values. It is not a statement and cannot replace if, but it is cleaner when the branches differ only in a value:
awk '{ print $1, ($2 >= 80 ? "PASS" : "FAIL") }' scores.txtalu PASS
fifo FAIL
uart PASS
spi FAILIdentical output to section 3, one print instead of two.
The parentheses around it are not required by the grammar but are worth writing, because ?: has very loose precedence — looser than concatenation — and an unparenthesised ternary inside a print list is easy to misread.
Use if/else when the two branches do different things. Use ?: when they supply different values to the same action. Chapter 5.1's final exercise used it for exactly that.
9. When a Condition Meets a Malformed Field
Now the finding this chapter exists for.
Chapter 5.3 showed that a malformed measurement corrupts a mean while leaving the sum intact. A comparison fails differently, and worse.
The same scores file with one unusable value — a test that was never scored:
alu 95
fifo n/a
uart 88The classification from section 3, unchanged:
awk '{ if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores_bad.txtalu PASS
fifo PASS
uart PASSfifo passed. A test with no score at all is reported as having passed — not failed, not flagged, not skipped. It is in the PASS list, indistinguishable from alu and uart, which genuinely passed.
The mechanism is Chapter 4.2's comparison rule. $2 holds n/a, which is not a numeric string, so awk cannot compare numerically and compares as text instead. Text comparison is character by character:
awk 'BEGIN { print ("n/a" >= "80") ? "\"n/a\" >= \"80\" is TRUE" : "FALSE" }'"n/a" >= "80" is TRUEn is character code 0x6e. 8 is 0x38. So "n/a" sorts after "80", the comparison is true, and the branch prints PASS.
And here is the part that removes any defence of this behaviour. Change only the placeholder's spelling:
alu 95
fifo -
uart 88awk '{ if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores_dash.txtalu PASS
fifo FAIL
uart PASSNow fifo fails. Same bug, same data, same command, opposite verdict — because - is 0x2d, which sorts before 8.
The repair is to validate the field's shape before comparing it, using Chapter 5.3's positive guard, and to give unusable data a category of its own:
awk '{ if ($2 !~ /^-?[0-9]+(\.[0-9]+)?$/) print $1, "NO_SCORE"; else if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores_bad.txtalu PASS
fifo NO_SCORE
uart PASSThree categories, each record in the right one, and fifo is now visibly something that needs attention rather than something that passed.
The guard has to come first in the chain. Put it last and the >= 80 test gets to the malformed field before the validation does, which is the original bug with extra code around it.
It is worth confirming the classifier separately from the verdict, because a guard that silently over-rejects is as bad as one that under-rejects:
awk '{ print $1, "[" $2 "]", ($2 ~ /^-?[0-9]+(\.[0-9]+)?$/ ? "numeric" : "NOT numeric") }' scores_bad.txtalu [95] numeric
fifo [n/a] NOT numeric
uart [88] numericExactly one record rejected, and it is the right one.
Adding a count makes the report self-auditing:
awk '$2 !~ /^-?[0-9]+(\.[0-9]+)?$/ { nodata++; next } $2 >= 80 { pass++; next } { fail++ } END { printf "%d pass, %d fail, %d unscored, of %d tests\n", pass+0, fail+0, nodata+0, NR }' scores_bad.txt2 pass, 0 fail, 1 unscored, of 3 testsThe three categories sum to the record count, which is the arithmetic check that no record was silently dropped. next is Chapter 5.6's subject; here it is what keeps the three rules mutually exclusive.
10. Pattern or if?
The question you have been carrying since Module 4. These two produce the same output:
awk '$2 >= 80 { print $1, "PASS" }' scores.txtalu PASS
uart PASSawk '{ if ($2 >= 80) print $1, "PASS" }' scores.txtalu PASS
uart PASSWith only a true branch, the pattern form is shorter and more idiomatic — it is the awk way to say "for records like this, do that".
The distinction is what each one controls:
| Decides | |
|---|---|
| pattern | whether the action runs at all |
if | which statement runs, inside an action that is already running |
So the moment you need an else, you need if — a pattern has no else:
awk '{ if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores.txtalu PASS
fifo FAIL
uart PASS
spi FAILYou can fake it with two patterns, and it is worth seeing why you should not:
awk '$2 >= 80 { print $1, "PASS" } $2 < 80 { print $1, "FAIL" }' scores.txtalu PASS
fifo FAIL
uart PASS
spi FAILIdentical output, and the conditions are now stated twice in a form that must stay each other's exact complement. Change the threshold in one and not the other and records fall into both categories or neither — with no error, because Chapter 2.3 established that every rule gets tested against every record.
The guidance: one condition with two outcomes is an if/else. Several independent conditions are patterns. Section 9's final command is the exception that proves the rule — its three patterns end in next, which is what makes them mutually exclusive by construction rather than by careful maintenance.
11. Nesting and Readability
if statements nest, and nesting past two levels is usually a sign the logic wants restructuring:
awk '{ if ($2 ~ /^[0-9]+$/) { if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" } else print $1, "NO_SCORE" }' scores_bad.txtalu PASS
fifo NO_SCORE
uart PASSCorrect, and harder to read than section 9's flat chain producing the same answer. The flat version tests the exceptional case first and gets it out of the way:
awk '{ if ($2 !~ /^[0-9]+$/) print $1, "NO_SCORE"; else if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores_bad.txtalu PASS
fifo NO_SCORE
uart PASSSame three categories, one level of nesting, and the guard is visibly the first thing that happens. Handle the unusable case first, then branch on the value — it flattens the structure and makes the validation impossible to overlook.
12. Common Mistakes
Omitting the parentheses. if s >= 80 is a syntax error; the parentheses are required.
Omitting the semicolon before else. Also a syntax error, and the most common awk typo.
Omitting braces on a multi-statement branch. Section 5. The extra statement runs unconditionally, the printed lines look right, and a count is wrong.
Trusting indentation. awk ignores it. Only braces group.
Ordering a chain ascending. Section 4. The loosest threshold claims everything and the later branches become dead code.
Leaving the boundary implicit. > 80 and >= 80 disagree about exactly 80. Write the rule into the output.
Comparing a field that might be malformed. Section 9. n/a is reported as PASS and - as FAIL, from the same bug.
Using a bare if (v) on a field whose zero is meaningful. Chapter 5.1, Exercise 6.
Faking else with two complementary patterns. They must stay exact complements forever, and nothing checks that they do.
Nesting three levels deep. Test the exceptional case first and flatten.
13. Exercises
Exercise 1 — Predict the output
awk 'BEGIN { x = 0; if (x) print "truthy"; else print "falsy" }'falsyThe number 0 is false. Chapter 5.1's truth rule, and the reason if (NF) works as a non-empty test.
Exercise 2 — Classify with a chain
Classify each score as HIGH at 90 or above, MID at 70 or above, and LOW below that.
alu 95
fifo 72
uart 88
spi 45awk '{ if ($2 >= 90) print $1, "HIGH"; else if ($2 >= 70) print $1, "MID"; else print $1, "LOW" }' scores.txtalu HIGH
fifo MID
uart MID
spi LOWThresholds descending, so each branch is reachable.
Exercise 3 — Debug a wrong count
An engineer counts passing tests and gets 4 from a file with two passes. Their command:
awk '{ if ($2 >= 80) print $1; count++ } END { print "count =", count+0 }' scores.txtalu
uart
count = 4The if governs only the print. count++ is a separate statement in the same action, so it runs for all four records. Note that the two printed names already contradict the count — the output contains its own disproof.
awk '{ if ($2 >= 80) { print $1; count++ } } END { print "count =", count+0 }' scores.txtalu
uart
count = 2Exercise 4 — The boundary
Both commands below are run against two tests that scored exactly 80. Predict each, then state which policy each implements.
alu 80
fifo 80awk '$2 > 80 { n++ } END { print n+0, "tests above 80" }' boundary.txt0 tests above 80awk '$2 >= 80 { n++ } END { print n+0, "tests at or above 80" }' boundary.txt2 tests at or above 80Zero against two, from the same data. The labels are doing real work here — each output states the rule it applied, so the two cannot be confused when they appear in a report.
Exercise 5 — Debug a dangerous classification
A regression sign-off report says all three tests passed. One of them was never scored. Find the bug.
alu 95
fifo n/a
uart 88awk '{ if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores_bad.txtalu PASS
fifo PASS
uart PASSfifo has no score and is reported as passing. "n/a" is not a numeric string, so awk compared as text, and n (0x6e) sorts above 8 (0x38).
Confirm the mechanism rather than assuming it:
awk 'BEGIN { print ("n/a" >= "80"), ("-" >= "80"), ("TBD" >= "80") }'1 0 1n/a and TBD compare as true; - compares as false. The placeholder's spelling decides the verdict.
awk '{ if ($2 !~ /^-?[0-9]+(\.[0-9]+)?$/) print $1, "NO_SCORE"; else if ($2 >= 80) print $1, "PASS"; else print $1, "FAIL" }' scores_bad.txtalu PASS
fifo NO_SCORE
uart PASSExercise 6 — Engineering application
Produce a sign-off summary with three categories, a visible denominator, and a non-zero exit status if any test was unscored — so a calling script cannot ignore it.
awk '$2 !~ /^-?[0-9]+(\.[0-9]+)?$/ { nodata++; next } $2 >= 80 { pass++; next } { fail++ } END { printf "%d pass, %d fail, %d unscored, of %d tests (threshold >= 80)\n", pass+0, fail+0, nodata+0, NR; if (nodata) exit 3 }' scores_bad.txt
echo "status: $?"2 pass, 0 fail, 1 unscored, of 3 tests (threshold >= 80)
status: 3Everything a reviewer needs: three categories that sum to the denominator, the threshold stated, and an exit status that makes the unscored test impossible to overlook in automation. exit is Chapter 5.6's subject.
On clean data the same command exits 0:
awk '$2 !~ /^-?[0-9]+(\.[0-9]+)?$/ { nodata++; next } $2 >= 80 { pass++; next } { fail++ } END { printf "%d pass, %d fail, %d unscored, of %d tests (threshold >= 80)\n", pass+0, fail+0, nodata+0, NR; if (nodata) exit 3 }' scores.txt
echo "status: $?"2 pass, 2 fail, 0 unscored, of 4 tests (threshold >= 80)
status: 0Exercise 7 — Challenge: prove a branch is reachable
An ascending chain makes later branches unreachable (section 4). Write a command that proves which branches a chain can actually reach, rather than inspecting it by eye.
awk '{ if ($2 >= 70) { b="C"; hit["C"]++ } else if ($2 >= 80) { b="B"; hit["B"]++ } else if ($2 >= 90) { b="A"; hit["A"]++ } else { b="F"; hit["F"]++ } } END { split("A B C F", g, " "); for (i=1; i<=4; i++) printf "%s: %d%s\n", g[i], hit[g[i]]+0, (hit[g[i]] ? "" : " <- NEVER TAKEN") }' scores.txtA: 0 <- NEVER TAKEN
B: 0 <- NEVER TAKEN
C: 3
F: 1Two of four branches were never taken on this input, which is the signature of the ascending-chain bug. The descending version reaches three of four:
awk '{ if ($2 >= 90) { hit["A"]++ } else if ($2 >= 80) { hit["B"]++ } else if ($2 >= 70) { hit["C"]++ } else { hit["F"]++ } } END { split("A B C F", g, " "); for (i=1; i<=4; i++) printf "%s: %d%s\n", g[i], hit[g[i]]+0, (hit[g[i]] ? "" : " <- NEVER TAKEN") }' scores.txtA: 1
B: 1
C: 1
F: 1All four reached. Arrays, split() and for are Modules 6 and 8 and Chapter 5.5 — the point here is the technique, which is the same one real coverage tools use: instrument the branches and count them, rather than arguing about the logic.
A branch showing 0 is not automatically a bug — it may simply be a case this input lacks. What it tells you is where to look, and a branch that is 0 across every input you have is either dead code or an untested path, both of which are worth knowing about.
14. Summary
if (condition) statement, parentheses required, and a semicolon beforeelse.else ifchains test top to bottom and stop at the first match. Order thresholds descending or the later branches become dead code.- Without braces,
ifgoverns exactly one statement. The next statement runs unconditionally — identical output lines, wrong count, no error. - Indentation means nothing to awk. Only braces group.
> 80and>= 80disagree about exactly 80. Write the rule into the output so the policy is auditable.- Any expression is a condition — comparisons,
&&/||, a regex match, or a bare value using Chapter 5.1's truth rule. ?:supplies a value;if/elsechooses an action.- A malformed field compared against a number is compared as text.
n/ais reported as PASS,-as FAIL — the same bug, with the verdict decided by the placeholder's spelling. - Validate the shape first and give unusable data its own category. Three outcomes, summing to the denominator.
- A pattern decides whether an action runs;
ifdecides which statement runs. Anything needing anelseneedsif.
You can now classify records, and keep a classification honest about the data it was given.
Every guard so far has examined one field at a time. Chapter 5.5 — for and while Loops covers repetition: walking every field of a record whatever its width, the off-by-one that drops the last field, the index that returns the entire record instead of a field, and the loop that runs once even though its condition was false from the start.
Where this fits
Part of the awk curriculum.
