awk Part I · Module 4 · Patterns, Conditions, and Regular Expressions
Conditional Filtering
Chapter 4.1 showed where a pattern looks and Chapter 4.2 gave you the operators to combine several tests. Both were about getting one condition right. This chapter is about a decision neither of them raises, and it changes what your script is able to report. A condition can live in the pattern, where it selects which records the action runs on, or inside the action, where every record arrives and the condition decides what to do with it. Those are not two spellings of the same thing: a pattern filter discards the records it rejects, so nothing later in the program can count them. That is the difference between reporting two violations and reporting two violations out of four paths, and only one of those is a number anybody should act on.
Foundation16 min readawkPart IPatternsFilteringEDA Reports
Part I · Module 4 · Chapter 4.6 · Conditional Filtering
1. Why This Matters
You can now write any condition you like. What this chapter adds is where to put it.
A condition in the pattern decides which records the action sees. A condition inside the action sees every record and decides what to do with each one.
Both are correct awk. They produce different programs, and the difference shows up not in the filtered output but in what you can say about it.
The report for this chapter:
u_core/u_alu setup -0.124 VIOLATED
u_core/u_regfile setup 0.081 MET
u_pad/u_io hold -0.052 VIOLATED
u_pad/u_clk hold 0.410 MET2. The Rule You Actually Want
A real selection rule usually combines a text test with a numeric one, because the two check different things. Here is the registry's own example for this chapter:
awk '/VIOLATED/ && $3 < 0 { print $1, $3 }' timing.rptu_core/u_alu -0.124
u_pad/u_io -0.052Two conditions: the verdict says the tool thinks this failed, and the slack says the arithmetic agrees. On a well-formed report they always agree, which makes the conjunction look redundant.
It is not redundant — it is a cross-check. Chapter 1.2 drew the distinction between a recorded result and a measurement, and this rule demands both. A record where the tool said VIOLATED and the slack is positive would be excluded, and that exclusion is information: it means the report is internally inconsistent and worth looking at by hand.
Writing it with the tighter forms from Chapter 4.1 and the forced arithmetic from Chapter 4.2:
awk '$4 == "VIOLATED" && $3 + 0 < 0 { print $1, $3 }' timing.rptu_core/u_alu -0.124
u_pad/u_io -0.052Same answer, and now the verdict is anchored to its column and the comparison cannot silently become textual.
3. Filtering in the Pattern
The form you have used throughout. The condition goes before the braces and the action runs only on matching records:
awk '$3 + 0 < -0.1 { print $1 }' timing.rptu_core/u_aluOne record is worse than -0.1. The other three were read, split into fields, tested, and discarded.
This is the right default. It is short, it reads as a sentence, and the output contains exactly what you asked for.
4. Filtering in the Action
The same selection, with the condition moved inside:
awk '{ if ($3 + 0 < -0.1) print $1 }' timing.rptu_core/u_aluIdentical output. The rule now has no pattern, so it runs on every record, and an if inside decides whether to print. (if is Chapter 5.4's subject; it appears here because this chapter cannot make its point without it.)
So far this looks like a pointless extra four characters. The difference appears the moment the program needs to say anything about the records it did not select.
5. The Difference: What END Can See
Count the violations with a pattern filter:
awk '$3 + 0 < 0 { n++ } END { print "pattern selected", n }' timing.rptpattern selected 2Two. Now the same count with the filter inside the action, keeping a second counter for everything seen:
awk '{ if ($3 + 0 < 0) n++; total++ } END { print "action saw", total, "selected", n }' timing.rptaction saw 4 selected 2Two out of four.
| condition in the pattern | condition in the action | |
|---|---|---|
| Runs on | matching records only | every record |
| Output | only selected records | whatever you choose per record |
| Can count what was rejected | no | yes |
| Can report a subset denominator | no | yes |
| Can label every record | no | yes |
| Length | shorter | a few characters more |
| Reach for it when | you want the matching rows | you want to say something about the whole file |
6. Labelling Instead of Dropping
Once every record reaches the action, you can classify rather than discard. The ternary operator does it in one expression — condition ? value-if-true : value-if-false:
awk '{ print $1, ($3 + 0 < 0 ? "FAIL" : "ok") }' timing.rptu_core/u_alu FAIL
u_core/u_regfile ok
u_pad/u_io FAIL
u_pad/u_clk okFour input records, four output rows, each carrying a verdict this script derived itself.
No pattern filter can produce that table, because the passing rows would never reach an action. And a table with one row per test is exactly what a regression summary or a per-path review wants — the absent rows in a filtered list are indistinguishable from tests that were never run, which Chapter 1.2 warned about from the other direction.
7. Layering Conditions
Real rules accumulate conditions. Each one narrows the selection, and && is the join:
awk '$2 == "setup" && $3 + 0 < 0 && $4 == "VIOLATED" { print $1 }' timing.rptu_core/u_aluThree tests: the check type, the arithmetic, and the tool's verdict. One record satisfies all three.
Order them with Chapter 4.2's short-circuit rule in mind — the cheapest and most selective test first. $2 == "setup" is a single field comparison that eliminates half the file, so it belongs at the front; a regex over the whole record belongs at the back.
And remember Chapter 4.2's parenthesis habit the moment an || appears:
awk '($2 == "setup" || $2 == "hold") && $3 + 0 < 0 { print $1, $2 }' timing.rptu_core/u_alu setup
u_pad/u_io hold8. A Threshold From Outside the Script
A rule containing a hard-coded number is a rule that needs editing to be reused. -v passes one in:
awk -v lim=-0.1 '$3 + 0 < lim { print $1, $3 }' timing.rptu_core/u_alu -0.124The same command answers a different question by changing one word on the command line, which is what makes it usable from a shell script. Chapter 8.2 covers -v properly, including why the value arrives as text and why the + 0 on the field is still worth having.
9. Counting Each Category in One Pass
Several rules over one file, each counting its own category, with the totals in END:
awk '$4 == "VIOLATED" { v++ } $4 == "MET" { m++ } END { print "violated", v+0, "met", m+0 }' timing.rptviolated 2 met 2Two rules fire on each record — at most one of the category rules, then nothing else — and the counts sum to 4, which is NR. That sum is the cross-check: if it did not equal NR, some record has a verdict that is neither word, and the categories are not exhaustive.
Making that check explicit rather than mental:
awk '$4 == "VIOLATED" { v++; next } $4 == "MET" { m++; next } { other++ } END { print "violated", v+0, "met", m+0, "unclassified", other+0, "of", NR }' timing.rptviolated 2 met 2 unclassified 0 of 4The next calls make the rules exclusive — Chapter 2.3's lesson — and the catch-all rule counts anything the named categories missed. unclassified 0 of 4 is a stronger statement than violated 2 met 2, because it proves nothing was silently skipped.
10. An Engineering Example
A wider report, and the two shapes of question a timing review actually asks.
u_core/u_alu setup clk_main -0.124 VIOLATED
u_core/u_regfile setup clk_main 0.081 MET
u_pad/u_io hold clk_io -0.052 VIOLATED
u_pad/u_clk hold clk_io 0.410 MET
u_core/u_mul setup clk_main -0.310 VIOLATEDThe filtered list — which paths need work, sorted by nothing, just listed:
awk -v lim=-0.1 '$5 == "VIOLATED" && $4 + 0 < lim { print $1, $3, $4 }' timing_full.rptu_core/u_alu clk_main -0.124
u_core/u_mul clk_main -0.310A pattern filter is right here. The question is "which rows", the answer is those rows, and nothing is being claimed about the rest.
The summary — which needs the denominator, so the filter moves inside:
awk '{ total++; if ($5 == "VIOLATED") { v++; if ($4 + 0 < worst) { worst = $4 + 0; wp = $1 } } } END { printf "%d of %d checks violated; worst %s at %s\n", v+0, total, worst+0, (wp == "" ? "none" : wp) }' timing_full.rpt3 of 5 checks violated; worst -0.31 at u_core/u_mulEvery record reached the action. total counted all of them, v counted the violations, and the worst case was tracked only among the violations. The output is a claim with a denominator attached, which is the form a report can carry.
Note worst -0.31 rather than -0.310 — the + 0 made it a number and a trailing zero is not part of a number's value, which Chapter 4.5 met as well. And the wp == "" guard produces none rather than an empty space on a clean report, which is Chapter 2.4's empty-case discipline.
The caveats are unchanged and are the reason the denominator matters. VIOLATED is the tool's own verdict, not an independent check. These five rows are checks, so a path appearing under both setup and hold counts twice — which is exactly why 3 of 5 checks is the honest phrasing and 3 violations is not.
11. Common Mistakes
Filtering in the pattern and then wanting a denominator. Section 5. The rejected records are gone.
Reporting a count without its total. 2 against 2 of 4. Chapter 1.2's rule, and section 5 is the mechanism.
Using a ternary where the branches do different things. Section 6's callout. It chooses between values, not actions.
Putting the expensive test first in a chain. Chapter 4.2's short-circuit rule — order the cheap, selective test to the left.
Mixing && and || without parentheses. Section 7, and Chapter 4.2 section 6 for why it changes the answer.
Assuming two category rules are exhaustive. Section 9. Add a catch-all and prove the counts sum to NR.
Forgetting + 0 on a numeric field from a file you did not write. Chapter 4.2 section 3.
Producing a filtered list and calling it a per-test table. The absent rows look like tests that never ran. Section 6 is the fix.
12. Exercises
Exercise 1 — Predict the output
u_core/u_alu setup -0.124 VIOLATED
u_core/u_regfile setup 0.081 MET
u_pad/u_io hold -0.052 VIOLATED
u_pad/u_clk hold 0.410 METawk '$4 == "MET" && $3 + 0 > 0.2 { print $1 }' timing.rptu_pad/u_clkOne record: MET and comfortably positive. u_core/u_regfile is MET but its 0.081 is below the threshold.
Exercise 2 — Modify a command
Turn the filtered list below into a table with one row per record, labelling each as violation or clean.
awk '{ print $1, ($4 == "VIOLATED" ? "violation" : "clean") }' timing.rptu_core/u_alu violation
u_core/u_regfile clean
u_pad/u_io violation
u_pad/u_clk cleanThe pattern was removed so every record reaches the action, and the ternary supplies the label. Four rows in, four rows out.
Exercise 3 — Trace what END can see
Predict both outputs, paying attention to which program can report a total.
awk '$3 + 0 < 0 { n++ } END { print n+0, NR }' timing.rpt2 4awk '$2 == "setup" { s++ } $2 == "setup" && $3 + 0 < 0 { sv++ } END { print sv+0, "of", s+0, "setup checks" }' timing.rpt1 of 2 setup checksThe first works because NR is available in END and the denominator happens to be the whole file. The second needs a subset denominator — setup checks only — and NR cannot supply it, so a second rule has to count them. That is section 5's limit: the pattern form is fine until the denominator stops being NR.
Exercise 4 — Debug a misleading summary
An engineer reports "4 violations" from the five-row report and a colleague says there are three. Their command is below. Explain the discrepancy.
u_core/u_alu setup clk_main -0.124 VIOLATED
u_core/u_regfile setup clk_main 0.081 MET
u_pad/u_io hold clk_io -0.052 VIOLATED
u_pad/u_clk hold clk_io 0.410 MET
u_core/u_mul setup clk_main -0.310 VIOLATEDawk '/VIOLATED/ { n++ } END { print n+0, "violations" }' timing_full.rpt3 violationsThat command gives 3, so the engineer's 4 came from something else — most likely a whole-record match on a report where a path name contained the word, which is Chapter 4.1 section 4 exactly. The fix is to anchor the test and to report the denominator so the claim is checkable:
awk '{ total++ } $5 == "VIOLATED" { n++ } END { print n+0, "of", total, "checks violated" }' timing_full.rpt3 of 5 checks violatedNow a reader can verify the arithmetic against the file, and the word "checks" says what is being counted.
Exercise 5 — Engineering application
Report, for clk_main only, how many checks violated out of how many were analysed.
awk '$3 == "clk_main" { t++; if ($5 == "VIOLATED") v++ } END { print v+0, "of", t+0, "clk_main checks violated" }' timing_full.rpt2 of 3 clk_main checks violatedThe outer condition is in the pattern because it selects the population being described; the inner condition is in the action because both halves of the ratio are needed. That split — pattern for the population, action for the classification — is the practical shape of section 5's advice.
Exercise 6 — Challenge: one pass, three numbers, one verdict
Write a single command that reports the violation count, the total check count, the worst slack, and a one-word verdict, and that behaves sensibly on a report with no violations at all.
awk '{ total++ } $5 == "VIOLATED" { v++; if ($4 + 0 < worst) { worst = $4 + 0; wp = $1 } } END { printf "%d/%d violated, worst %s at %s, verdict %s\n", v+0, total, (v ? worst+0 : "n/a"), (v ? wp : "n/a"), (v ? "FAIL" : "CLEAN") }' timing_full.rpt3/5 violated, worst -0.31 at u_core/u_mul, verdict FAILAnd on a report where everything met its timing:
printf 'u_a setup clk_main 0.100 MET\nu_b hold clk_io 0.200 MET\n' > clean.rpt
awk '{ total++ } $5 == "VIOLATED" { v++; if ($4 + 0 < worst) { worst = $4 + 0; wp = $1 } } END { printf "%d/%d violated, worst %s at %s, verdict %s\n", v+0, total, (v ? worst+0 : "n/a"), (v ? wp : "n/a"), (v ? "FAIL" : "CLEAN") }' clean.rpt0/2 violated, worst n/a at n/a, verdict CLEANThree design choices worth naming:
totalis counted in a pattern-less rule, so it is a true denominator regardless of what else matched.worstis only tracked inside the violation rule, so a clean report leaves it unset — andworst = 0would have been a bug, since a slack of 0 is better than any violation and would have masked the real worst case.- Every output field is guarded by
v, so a clean report printsn/arather than an empty space or a misleading0. Chapter 2.4 made this point aboutENDrunning on empty input; here the input is fine and the category is empty, which is the same problem one level in.
The printf gives control over the layout; Chapter 7.2 covers it properly.
13. Summary
- A condition in the pattern selects which records the action sees. A condition in the action sees every record and decides what to do with each.
- A pattern filter discards what it rejects, so nothing later in the program can count it.
2and2 of 4are different claims and only the second is reportable. NRrescues the simple case but not a subset denominator — "2 of the 3 setup checks" needs its own counter, which only the action form can keep while filtering.- The ternary labels rather than drops, turning a filter into a classifier so every input record produces an output row. A filtered list is not a per-test table; its absent rows look like tests that never ran.
- The ternary is an expression. Use it to choose a value, and
if/elseto choose an action. - Layer conditions with
&&, cheapest and most selective first, and parenthesise the moment an||appears. - Pass thresholds in with
-vso a rule is reusable without editing. - Add a catch-all when counting categories and prove the counts sum to
NR. - Guard every reported value against its category being empty, or a clean report prints something misleading.
You can now write the selection rules a report script is actually made of, and choose a structure that can still report a denominator.
Every example in this module has run over one small file written to isolate one idea. The last chapter puts the whole module to work at once, over several files, in a single pass — which is what a nightly triage actually looks like. Chapter 4.7 — Filtering Real Logs and Reports is Module 4's capstone.
Where this fits
Part of the awk curriculum.
