awk Part I · Module 3 · Fields, Records, and Separators
Positional Fields: $1, $2, $3
Module 2 explained how awk splits a record into fields. This module is about controlling that process, and it starts with the simplest form of control: choosing columns by their position. On a two-column file that is barely a decision. On a timing report with five or six columns it becomes one, because you have to pick which numbers to trust and in what order to emit them. This chapter works on a report wide enough for that to matter — selecting a subset, reordering on the way out, filtering on one column while printing another — and then makes the uncomfortable point that a column number is a promise about the report's layout. When a tool upgrade inserts a column, every script that hard-coded a number keeps running and starts being wrong.
Foundation15 min readawkPart IFieldsSeparatorsEDA Reports
Part I · Module 3 · Chapter 3.1 · Positional Fields: $1, $2, $3
1. Why This Matters
Chapter 2.2 answered a mechanical question: given a record, how does awk decide where the fields are? The answer was the default whitespace rule, and it is doing all the work in this chapter too.
What it did not answer is the engineering question. Given a report with six columns, which ones do you read, and what happens to your script when the report changes?
That is this module's subject, and position is where it starts:
$4is not the name of a value. It is the fourth field, and it means what you think only while the report keeps that value fourth.
Everything up to section 6 is the straightforward practice. Sections 6 and 7 are why the chapter is worth reading.
2. A Wider Report
Two columns were enough for Module 2. They are not enough to make position interesting, so here is a synthetic timing report with five:
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 VIOLATEDFive fields per record: an endpoint path, the check type, the clock, the slack, and a verdict.
Notice the columns are padded with different numbers of spaces so they line up on screen. That does not affect anything — Chapter 2.2 established that a run of whitespace is one separator — which is exactly why the default rule is the right tool for a report that was formatted for a human.
| Field | Holds | On record 1 |
|---|---|---|
$1 | endpoint path | u_core/u_alu |
$2 | check type | setup |
$3 | clock | clk_main |
$4 | slack | -0.124 |
$5 | verdict | VIOLATED |
That table is the contract this chapter is about. Keep it in mind for section 7.
3. What Counts as One Field
Before relying on those positions, it is worth checking an assumption that trips people up on EDA reports specifically: an instance path looks like several things joined together, and awk treats it as one field.
awk 'NR == 1 { print "NF=" NF; print " $1=[" $1 "]" }' timing.rptNF=5
$1=[u_core/u_alu]Five fields, and the whole path is $1. Slashes are not whitespace, so they do not split anything. The same holds for the bracketed bit-selects that appear in real netlist paths:
printf 'u_core/u_alu/reg_q[3] setup -0.124\n' > brackets.rpt
awk '{ print NF, "[" $1 "]" }' brackets.rpt3 [u_core/u_alu/reg_q[3]]Three fields. The path survives intact despite slashes, an underscore and square brackets, because none of them is whitespace and Chapter 2.2's default rule only splits on whitespace.
4. Selecting Columns by Position
The path and its slack, which is the pair most timing questions want:
awk '{ print $1, $4 }' timing.rptu_core/u_alu -0.124
u_core/u_regfile 0.081
u_pad/u_io -0.052Two of the five columns, in the order written, separated by a space. The other three were read and discarded — awk split the whole record either way, and you simply did not ask for them.
Three columns, adding the clock:
awk '{ print $1, $3, $4 }' timing.rptu_core/u_alu clk_main -0.124
u_core/u_regfile clk_main 0.081
u_pad/u_io clk_io -0.052The comma between each pair is what inserts the space. Chapter 1.6 covered that; the variable controlling which character it inserts is OFS, and Chapter 3.3 is where you get to change it.
5. Reordering on the Way Out
The order you print in has nothing to do with the order in the file:
awk '{ print $4, $1 }' timing.rpt-0.124 u_core/u_alu
0.081 u_core/u_regfile
-0.052 u_pad/u_ioSlack first, path second. This is genuinely useful — putting the number first makes the output sortable by slack with a following sort -n, and it is the shape a spreadsheet usually wants.
Note what did not happen: the record itself was not modified. $0 still holds the original line in its original order. Rewriting the record rather than just printing a chosen order is a different operation with different consequences, and it is Chapter 3.6's subject.
6. Position Is a Contract
Everything above works, and it all rests on an assumption that is nowhere written down: that slack is in column 4.
That assumption is a contract with the report's layout. You did not negotiate it, the tool did not promise it, and nothing in your script states it. It is simply true of the file you happened to look at.
7. When a Column Is Inserted
Here is the contract being broken. The tool is upgraded and the new version reports the delay-check type as well, inserting one column before slack:
u_core/u_alu setup clk_main max -0.124 VIOLATED
u_core/u_regfile setup clk_main max 0.081 METNow run the command from section 4 — the one that was correct yesterday — completely unchanged:
awk '{ print $1, $4 }' timing_v2.rptu_core/u_alu max
u_core/u_regfile maxThe slack column is gone from the output. In its place is the word max, for every path.
What makes it worse is that the fix is invisible from inside the script. There is nothing wrong with $4. The report moved.
8. Which End Is Stable?
There is a partial defence, and it is worth seeing now because it sets up the next chapter.
In this particular change, a column was inserted in the middle. The verdict stayed last, and the slack stayed second-from-last. So counting from the end survives the change:
awk '{ print $1, $(NF-1) }' timing_v2.rptu_core/u_alu -0.124
u_core/u_regfile 0.081Correct slack from the new format, with no change to how it was addressed. And the same expression also works on the original:
awk '{ print $1, $(NF-1) }' timing.rptu_core/u_alu -0.124
u_core/u_regfile 0.081
u_pad/u_io -0.052One expression, both layouts.
9. Filtering on One Column, Printing Another
Position applies to patterns as well as to actions, and the two need not be the same column:
awk '$4 + 0 < 0 { print $1, $4 }' timing.rptu_core/u_alu -0.124
u_pad/u_io -0.052The pattern tests column 4 numerically; the action prints columns 1 and 4. The + 0 forces arithmetic, which Chapter 1.2 showed matters the moment a value carries a unit suffix.
A text comparison on a different column again:
awk '$5 == "VIOLATED" { print $1 }' timing.rptu_core/u_alu
u_pad/u_ioBoth commands select the same two records here, by two independent routes — one arithmetic on the slack, one exact text on the verdict. That redundancy is useful: if they ever disagree on a real report, the report is telling you something about itself.
10. Two Value Columns
Reports often carry more than one number per row, and position is how you keep them apart. A synthetic cell report with a count and an area:
DFF 120 1820
AND 45 180
MUX 18 144awk '{ print $1, $2, $3 }' cells.rptDFF 120 1820
AND 45 180
MUX 18 144Two independent totals from one pass:
awk '{ count += $2; area += $3 } END { print count, area }' cells.rpt183 2144183 cells and 2144 area units. Keeping them in separate accumulators is only possible because the two columns are separately addressable, and getting the positions the wrong way round would produce two plausible numbers with their meanings swapped.
Chapter 1.6's caveat stands and is the reason this example exists: 183 is not a size and 2144 is not a cell count. They are sums of two different columns in two different units, and the only thing keeping them straight is $2 and $3.
11. A Header Row Changes the Arithmetic
Many reports begin with a line naming the columns, and it is worth knowing exactly what that does to positional reading — because the answer is not what you would guess.
endpoint check clock slack verdict
u_core/u_alu setup clk_main -0.124 VIOLATED
u_pad/u_io hold clk_io -0.052 VIOLATEDSum the slack column without thinking about the header:
awk '{ s += $4 } END { print s }' hdr.rpt-0.176That total is correct. -0.124 + -0.052 is -0.176, and the header contributed nothing — because $4 on the header row is the word slack, which counts as 0 in arithmetic. The header was included and harmlessly added zero.
Now count the records:
awk 'END { print NR }' hdr.rpt3Three, for two paths. This number is wrong, and nothing about the sum being right warned you.
awk 'NR > 1 { s += $4; n++ } END { print n, s }' hdr.rpt2 -0.176NR > 1 skips the header, and now both numbers are right. Chapter 2.1 introduced NR; this is the commonest reason to use it.
12. Common Mistakes
Treating a column number as a name for the value. Section 6. $4 means "fourth field"; it means "slack" only by convention.
Assuming a command that worked last quarter still reads the right column. Section 7. A tool upgrade is a layout change, and nothing warns you.
Reaching for $(NF-1) as a general fix. Section 8. It trades one assumption for a different one.
Forgetting the comma in print. print $1 $4 concatenates with nothing between them, giving u_core/u_alu-0.124. The comma is what inserts a separator.
Counting from zero. $1 is the first field; $0 is the whole record. Chapter 1.6 flagged it and it is still the most common slip when switching from another language.
Comparing a numeric column as text. $4 < 0 works on clean numbers but becomes a string comparison the moment the field is not a clean number — Chapter 1.2's unit-suffix trap. $4 + 0 < 0 forces arithmetic.
Reading a column that does not exist. $7 on a five-field record is the empty string, which is 0 in arithmetic. Chapter 2.2 covered the rule; the consequence here is a total of 0 from a column you misnumbered.
13. Exercises
Exercise 1 — Predict the output
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 VIOLATEDawk '{ print $2 }' timing.rptsetup
setup
holdThe check type, one per record. If you expected the clock, recount — the path is a single field despite containing slashes, because slashes are not whitespace.
Exercise 2 — Modify a command
Print the clock and the verdict, in that order, for every record.
awk '{ print $3, $5 }' timing.rptclk_main VIOLATED
clk_main MET
clk_io VIOLATEDTwo positions, chosen independently of their order in the file. Note $5 and $NF would both work here; which one you should prefer is Chapter 3.2's question.
Exercise 3 — Trace a shift
Without running it, work out what this prints, then check. The file is the six-column version.
u_core/u_alu setup clk_main max -0.124 VIOLATED
u_core/u_regfile setup clk_main max 0.081 METawk '{ print $3, $4, $5 }' timing_v2.rptclk_main max -0.124
clk_main max 0.081Every position after the insertion has moved one to the right. $4 is now the delay type, $5 is the slack, and the slack that used to be $4 is nowhere in the places you expected. Tracing a shift by hand once is the fastest way to stop trusting hard-coded numbers unconditionally.
Exercise 4 — Debug a wrong total
An engineer sums the slack column of timing_v2.rpt with the command below and gets 0. Explain why, then produce the correct total.
awk '{ s += $4 } END { print s }' timing_v2.rpt0$4 on the six-column report is the word max, and max in arithmetic is 0. Adding 0 twice gives 0 — a perfectly valid sum of the wrong column, with no error and exit status 0.
awk '{ s += $5 } END { print s }' timing_v2.rpt-0.043-0.124 + 0.081 is -0.043. The lesson is not "use $5" — it is that the first command could not tell you anything was wrong, so the check has to come from outside it. Exercise 6 builds that check.
Exercise 5 — Engineering application
Using the five-column report, print the path and slack of every record whose verdict is VIOLATED, and report how many there were.
awk '$5 == "VIOLATED" { print $1, $4; n++ } END { print n + 0, "violations" }' timing.rptu_core/u_alu -0.124
u_pad/u_io -0.052
2 violationsThe pattern reads the verdict column and the action reads the path and slack columns — three positions, one pass. The + 0 keeps a clean report printing 0 violations rather than a blank, which Chapter 1.6 established and Chapter 2.4 explained.
Two caveats carried forward. VIOLATED is the tool's own verdict, not an independent check — Chapter 1.2's distinction between a recorded result and a measurement. And 2 counts records, so a report listing the same physical path under two checks counts it twice.
Exercise 6 — Challenge: state the contract in the script
Write a command that reads slack from $4 but refuses to produce a number if the report does not have the layout that assumption requires. Test it against both report versions.
awk 'NF != 5 { print "ERROR: expected 5 fields, found", NF, "on record", NR; bad++; next } { s += $4 } END { if (bad) exit 2; print "total slack:", s }' timing.rpttotal slack: -0.095awk 'NF != 5 { print "ERROR: expected 5 fields, found", NF, "on record", NR; bad++; next } { s += $4 } END { if (bad) exit 2; print "total slack:", s }' timing_v2.rptERROR: expected 5 fields, found 6 on record 1
ERROR: expected 5 fields, found 6 on record 2The second command printed no total at all and exited 2, which a calling script can test. The implicit contract from section 6 is now written down as NF != 5, and the shift from section 7 is caught on the first record instead of producing max.
This is the pattern the rest of the module keeps returning to: an assumption you can state is an assumption you can check. NF is what makes it checkable, and that is what the next chapter is about.
14. Summary
- A column number is a contract with the report's layout, not a name for the value.
$4means "the fourth field". - Selecting and reordering are independent of the file.
print $4, $1emits slack first without touching the record; rewriting the record is Chapter 3.6. - The comma in
printinserts the separator. Without it, values are concatenated with nothing between them. - One inserted column breaks every hard-coded number after it, with no error and exit status 0. The word
maxarriving where slack belonged counts as 0 in arithmetic, so a downstream negative-slack check finds nothing. - Counting from the end is not a general fix. It assumes the trailing columns are stable, just as counting from the start assumes the leading ones are. A report change breaks whichever assumption it contradicts.
- Patterns and actions can read different columns, which is how you filter on a verdict and print a slack.
- Separate value columns need separate accumulators, and only position keeps their meanings apart.
- An assumption you can state is an assumption you can check.
NF != 5turns an implicit layout requirement into a loud failure.
You can now pull any column out of a wide report and, more importantly, say what your command is assuming about that report.
Section 8 left a question open and Exercise 6 answered half of it. If counting from the start assumes the head is stable and counting from the end assumes the tail is, then the number that tells you which one you actually have is the field count itself. Chapter 3.2 — NF and the Last Field is about using NF as the shape of a record rather than just a number, and about choosing deliberately between a fixed position and $NF.
Where this fits
Part of the awk curriculum.
