awk Part I · Module 4 · Patterns, Conditions, and Regular Expressions
Comparison and Logical Operators
Chapter 4.1 showed where a pattern looks. This chapter is about building a real selection rule once you have several things to test at once. Two parts of it are genuinely surprising. awk decides whether to compare as numbers or as text based on what is on both sides of the operator, so the same field can be greater than 9 and not greater than the quoted string nine — Chapter 1.2's unit-suffix trap was one symptom of a rule that is finally stated here. And the logical operators have a precedence that changes which records you select: one pair of parentheses turns a three-record answer into a two-record one, with no error either way.
Foundation16 min readawkPart IOperatorsComparisonsEDA Reports
Part I · Module 4 · Chapter 4.2 · Comparison and Logical Operators
1. Why This Matters
Chapter 4.1 ended with two rules that used && and || on trust. They are easy to use and easy to use wrongly, and both failure modes are silent.
Two facts carry this chapter:
awk decides numeric-or-text comparison from what is on both sides of the operator. And
&&binds tighter than||.
Neither produces an error when you get it wrong. Both change which records you select.
2. The Six Comparison Operators
The usual set, and each yields 1 for true or 0 for false:
awk 'BEGIN { a = 5; b = 9; print (a<b), (a<=b), (a>b), (a>=b), (a==b), (a!=b) }'1 1 0 0 0 1| Operator | True when |
|---|---|
< | left is less than right |
<= | less than or equal |
> | greater than |
>= | greater than or equal |
== | equal |
!= | not equal |
The fact that a comparison is a value — 1 or 0 — is why a bare comparison works as a pattern. $3 < 0 as a pattern means "run this rule when that expression is true", and truth in awk is any non-zero, non-empty value.
A synthetic timing report for the rest of the 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 METawk '$3 < 0 { print $1 }' timing.rptu_core/u_alu
u_pad/u_io3. What Counts as True
Section 2 ended by asserting that "truth in awk is any non-zero, non-empty value". That deserves explaining, because it has a sharp edge.
a 1
b 0
c
d 0.0
e textA bare field used as a pattern asks "is this value true?":
awk '$2 { print NR, $1, "[" $2 "]" }' truth.txt1 a [1]
5 e [text]Two records. And the complement:
awk '!$2 { print NR, $1, "[" $2 "]" }' truth.txt2 b [0]
3 c []
4 d [0.0]So a field holding 0 is false, a missing field is false, and 0.0 is false too — but the word text is true.
The rule is that a value from the input is judged as a number if it looks like one, and as a string otherwise. 0 and 0.0 look numeric and are numerically zero, so they are false. text is not numeric, so it is judged as a string and a non-empty string is true.
The practical use of truthiness is as a guard. NF as a pattern selects records that have any fields at all, which is the tidiest way to skip blank records:
awk 'NF { print NR, $0 }' bl.txta 1
b 21 a 1
3 b 2Record 2 is blank, so NF is 0, so the pattern is false. Chapter 2.2 introduced NF == 0 for this; the bare NF says the same thing more briefly.
An unset variable is false, which is why a half-written rule selects nothing:
awk 'BEGIN { print (nope ? "true" : "false") }'false4. Numeric or Text? The Rule
This is the section to remember. Chapter 1.2 showed a comparison "silently changing kind" and Chapter 1.5 showed Python refusing to guess. Neither stated the rule, because stating it needs this chapter's vocabulary.
Here it is: awk compares numerically when both sides look like numbers, and as text otherwise. A field that looks numeric counts as a number; a quoted constant never does.
The demonstration. One file, two fields:
10 9
9 10Two numeric-looking fields compare numerically, as you would hope:
awk '{ print $1, ($1 > $2 ? ">" : "<="), $2 }' nums.txt10 > 9
9 <= 10Now compare the same $1 against a numeric constant:
awk 'NR == 1 { print ($1 > 9) ? "10 > 9 true" : "10 > 9 false" }' nums.txt10 > 9 trueAnd against a quoted constant:
awk 'NR == 1 { print ($1 > "9") ? "10 > \"9\" true" : "10 > \"9\" false" }' nums.txt10 > "9" falseThe other half of the rule: when the field itself is not numeric-looking, the comparison is textual however you write the other side:
awk '{ print $1, ($1 > $2 ? ">" : "<="), $2 }' units.txt10ns 9ns10ns <= 9ns10ns is "less than" 9ns as text, which as a statement about time is nonsense.
5. Forcing the Comparison You Meant
Two idioms, both already seen in passing and now explicable.
Force numeric by adding zero:
awk '{ print ($1 + 0 > $2 + 0) ? "numeric: 10 > 9" : "numeric: not" }' units.txtnumeric: 10 > 9+ 0 converts each side to a number, discarding the suffix, so the comparison is arithmetic and correct.
Force text by concatenating an empty string:
awk 'NR == 1 { print ($1 "" > $2 "") ? "string: 10 > 9" : "string: 10 <= 9" }' nums.txtstring: 10 <= 9$1 "" makes a string of it, so even two numeric-looking fields compare as text.
6. The Three Logical Operators
&& for and, || for or, ! for not.
Both conditions must hold:
awk '/setup/ && $3 < -0.1 { print $1, $3 }' timing.rptu_core/u_alu -0.124Only one record is both a setup check and worse than -0.1. The other setup record has positive slack and the other negative-slack record is a hold check.
Either condition:
awk '$4 == "VIOLATED" || $3 > 0.4 { print $1 }' timing.rptu_core/u_alu
u_pad/u_io
u_pad/u_clkThe two violations plus the one comfortable path.
Negation, applied to a whole-record pattern:
awk '!/VIOLATED/ { print $1 }' timing.rptu_core/u_regfile
u_pad/u_clk7. Precedence: && Binds Tighter Than ||
Now the part that changes answers. These two commands differ by one pair of parentheses:
awk '$2 == "hold" || $2 == "setup" && $3 < 0 { print $1 }' timing.rptu_core/u_alu
u_pad/u_io
u_pad/u_clkawk '($2 == "hold" || $2 == "setup") && $3 < 0 { print $1 }' timing.rptu_core/u_alu
u_pad/u_ioThree records against two. The first includes u_pad/u_clk, whose slack is a healthy 0.410.
The reason is precedence. && binds tighter than ||, so the unbracketed form groups like this:
$2 == "hold" || ( $2 == "setup" && $3 < 0 )Which is "every hold check, or any setup check with negative slack". Every hold record qualifies regardless of slack, which is why the comfortable u_pad/u_clk appeared.
The bracketed form says what the author almost certainly meant: "any check of either type, and negative slack".
8. Short-Circuit Evaluation
awk stops evaluating a logical expression as soon as the answer is settled. You can see it with an assignment as the side effect — if the right-hand side runs, n changes:
awk 'BEGIN { n = 0; if (0 && (n = 1)) ; print "n =", n }'n = 0awk 'BEGIN { n = 0; if (1 && (n = 1)) ; print "n =", n }'n = 1awk 'BEGIN { n = 0; if (1 || (n = 1)) ; print "n =", n }'n = 0awk 'BEGIN { n = 0; if (0 || (n = 1)) ; print "n =", n }'n = 1| Expression | Right side evaluated? | Why |
|---|---|---|
0 && … | no | a false left side settles && |
1 && … | yes | the right side decides |
1 || … | no | a true left side settles || |
0 || … | yes | the right side decides |
(The if here is Chapter 5.4's subject; it is used only to make the expression evaluate. The bare ; is an empty statement — there is nothing to do when the test is true, because the point is the side effect.)
This matters practically in one way: put the cheap, most-selective test first. $2 == "setup" && /some long pattern/ skips the pattern match entirely on every non-setup record. It is also why ordering a guard before the thing it guards works — NF == 5 && $4 + 0 < 0 will not evaluate $4 on a malformed record.
9. Chained Comparisons Are Rejected
In mathematics you can write one expression for "between". In awk you cannot, and the good news is that it is caught rather than misread:
awk 'BEGIN { print (3 > 2 > 1) }'awk refuses to parse this and exits with a non-zero status, reporting a syntax error at that line. The same is true of 1 < 2 < 3.
That is a kinder failure than it could have been — in some languages such an expression parses as (3 > 2) > 1, which is 1 > 1, which is false, and you get a wrong answer instead of a complaint. Here you get told.
The way to express a range is two comparisons joined by &&:
awk '$3 > -0.1 && $3 < 0.1 { print $1, $3 }' timing.rptu_core/u_regfile 0.081
u_pad/u_io -0.052The two paths whose slack is within a tenth of a nanosecond of zero in either direction — the marginal ones, which are often the interesting set.
10. A Threshold From Outside the Script
A rule is more reusable when the number in it is not baked in. -v passes one in:
awk -v lim=-0.1 '$3 + 0 < lim { print $1, $3 }' timing.rptu_core/u_alu -0.124The same command with a different limit needs no edit. Note the + 0 on the field: lim arrives as text, and although -0.1 is numeric-looking and would compare numerically here, forcing the field is the habit from section 5 and costs nothing. Chapter 8.2 covers -v properly.
11. An Engineering Example
A selection rule a timing review would actually want: setup violations worse than a threshold, reported with their clock and slack.
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 -v lim=-0.1 '$2 == "setup" && $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.310Three conditions joined with && — an exact check type, an exact verdict, and a forced-numeric threshold. No parentheses are needed because there is no || in the rule, which is section 7's habit stated the other way round.
And the marginal set, where the mixture does need brackets:
awk '($2 == "setup" || $2 == "hold") && $4 + 0 < 0 { print $1, $2, $4 }' timing_full.rptu_core/u_alu setup -0.124
u_pad/u_io hold -0.052
u_core/u_mul setup -0.310The caveats from Chapter 1.2 are unchanged and matter here more than usual, because this chapter is about building rules precise enough to act on. VIOLATED is the tool's own verdict rather than an independent check. A real report lists each physical path once per check and per corner, so counting these rows counts checks, not paths. And the threshold -0.1 is a judgement about what is worth looking at, not a property of the design.
12. Common Mistakes
Comparing a field against a quoted number. Section 4. $3 < "0" is a text comparison; $3 < 0 is arithmetic.
Trusting a numeric comparison on a field with a unit suffix. Section 4's second half, and Chapter 1.2's trap. Use + 0.
Mixing && and || without parentheses. Section 7. Three records where you meant two, silently.
Writing a chained comparison. Section 9. awk rejects it; use &&.
Using = instead of ==. $2 = "hold" assigns to field 2, rebuilds the record (Chapter 3.6) and is always true. awk will not stop you.
Putting the expensive test first. Section 8. Short-circuiting only helps if the selective test is on the left.
Assuming ! reaches across &&. !/a/ && /b/ is "(not a) and b", because ! binds tighter. If you meant "not (a and b)", bracket it.
Comparing a field to an unset variable. $1 == x where x was never set compares against the empty string, matches nothing, and reports no error — Chapter 1.6's quiet failure in a new place.
13. 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 '$3 > 0 && $4 == "MET" { print $1 }' timing.rptu_core/u_regfile
u_pad/u_clkThe two records with positive slack and a MET verdict. On this file either condition alone would select the same two, which is worth noticing — the redundancy is a cheap cross-check that the verdict agrees with the arithmetic.
Exercise 2 — Modify a command
Select the records whose check is hold and whose slack is negative.
awk '$2 == "hold" && $3 + 0 < 0 { print $1, $3 }' timing.rptu_pad/u_io -0.052One record. The + 0 is not strictly needed on this clean file and is the habit section 5 argues for.
Exercise 3 — Trace the precedence
Work out by hand how awk groups the rule below, say which records it selects, then check.
awk '$4 == "MET" || $4 == "VIOLATED" && $3 < -0.1 { print $1 }' timing.rptu_core/u_alu
u_core/u_regfile
u_pad/u_clk&& binds tighter, so awk reads it as $4 == "MET" || ($4 == "VIOLATED" && $3 < -0.1) — every MET record, plus violations worse than -0.1. That is three records including both passing paths, which is almost certainly not what someone writing this would have wanted.
awk '($4 == "MET" || $4 == "VIOLATED") && $3 < -0.1 { print $1 }' timing.rptu_core/u_aluWith the brackets, one record. Same two conditions, same data, a different question.
Exercise 4 — Debug a comparison that selects nothing
An engineer filters a slack column and gets no output, though the report clearly has negative values. Their file and command are below. Diagnose it.
path_a -0.124ns
path_b 0.081ns
path_c -1.200nsawk '$2 < "0" { print $1 }' slack_units.rptpath_a
path_cThat one does produce output — but by accident, and it is worth understanding why before trusting it. The field is not numeric-looking, and the constant is quoted, so this is a text comparison: -0.124ns sorts before "0" because - precedes 0 in the character order. It gives the right two rows on this data for the wrong reason, which is Chapter 1.2's warning exactly.
awk '$2 + 0 < 0 { print $1, $2 }' slack_units.rptpath_a -0.124ns
path_c -1.200nsThe forced-numeric version answers the question that was asked, and it keeps working on a value like +0.05ns where the text comparison would wrongly select.
Exercise 5 — Engineering application
From the five-column report, select every violation on clk_main, regardless of check type, and print the path and slack.
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 '$3 == "clk_main" && $5 == "VIOLATED" { print $1, $4 }' timing_full.rptu_core/u_alu -0.124
u_core/u_mul -0.310Two exact field tests joined with &&. No ||, so no brackets needed.
Exercise 6 — Challenge: say what each rule asks
Three rules over timing_full.rpt. For each, state in English what it selects, predict the count, then check. One of them is a trap.
awk '$2 == "setup" || $2 == "hold" && $4 + 0 < 0 { n++ } END { print n+0 }' timing_full.rpt4awk '($2 == "setup" || $2 == "hold") && $4 + 0 < 0 { n++ } END { print n+0 }' timing_full.rpt3awk '$2 == "setup" && $4 + 0 < 0 || $2 == "hold" && $4 + 0 < 0 { n++ } END { print n+0 }' timing_full.rpt3The first is the trap: "every setup check, or any hold check with negative slack". All three setup records qualify regardless of slack, plus the one negative hold — four. It reads like a range filter and is not one.
The second is "either check type, and negative slack" — three.
The third says the same thing as the second by distributing the && over both branches, and gets the same three. It is longer but needs no parentheses, which some people prefer; the bracketed form is shorter and, once you know the precedence, just as clear.
The lesson is that all three are valid awk and only one of them is wrong — and the wrong one is the one you get by typing what you meant without brackets.
14. Summary
- A comparison is a value, 1 or 0, which is why a bare comparison works as a pattern.
- awk compares numerically when both sides look like numbers, and as text otherwise. A numeric-looking field counts; a quoted constant never does.
- The same field can be greater than
9and not greater than"9". The quotes decide, and nothing warns you. + 0forces arithmetic;""forces text. Use+ 0on any quantity in a file you did not write.&&,||and!— and&&binds tighter than||, so one pair of parentheses changed three selected records into two.- Parenthesise any rule containing both
&&and||. The unbracketed form is not malformed, it answers a different question. - Evaluation short-circuits, so put the cheap, most-selective test first — and a guard placed before what it guards genuinely protects it.
- Chained comparisons are a syntax error, not a silent misread. Use two comparisons and
&&. =is not==. Assigning to a field is always true and rebuilds the record.
You can now write a selection rule that says precisely what you mean, over any combination of text and numeric tests.
Every rule so far has judged each record on its own contents. That is not enough for a report whose logical unit spans several lines — Chapter 3.4 made such a block into one record by changing RS, but sometimes you cannot or should not change what a record is, and you still need "everything from here to there". Chapter 4.3 — Range Patterns is a pattern with memory: it turns on at one match and off at another, and it is the first construct in this track that carries state between records.
Where this fits
Part of the awk curriculum.
