awk Part I · Module 3 · Fields, Records, and Separators
NF and the Last Field: $NF
Chapter 3.1 ended on an uncomfortable symmetry. Counting columns from the start assumes the leading columns stay put; counting from the end assumes the trailing ones do. A report change breaks whichever assumption it contradicts, and neither direction is inherently safer. This chapter resolves that by promoting NF from a number you can print to the shape of a record — something you can state as a requirement, compare between records, and use to find out that a tool upgrade has moved your columns. By the end you will choose between a fixed position and $NF on purpose rather than by habit, and you will have a short check that catches the change before it reaches anyone's dashboard.
Foundation15 min readawkPart IFieldsNFEDA Reports
Part I · Module 3 · Chapter 3.2 · NF and the Last Field: $NF
1. Why This Matters
You have met NF three times already. Chapter 1.6 warned you to keep it apart from $NF. Chapter 2.2 used it to recognise a blank record and to guard a ragged one. Chapter 3.1 used it to state a layout contract.
Each time it was a convenience. This chapter treats it as something more useful:
NFis the shape of the current record. A command that reads a column has an opinion about that shape, andNFis how you state and check it.
That reframing is what lets you answer 3.1's open question — which end of a record is stable — instead of guessing.
2. NF Is the Shape of a Record
The same synthetic timing report as the previous chapter:
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 NF, $NF }' timing.rpt5 VIOLATED
5 MET
5 VIOLATEDTwo different things on each line. 5 is how many fields the record has. VIOLATED is what is in the last one.
The 5 repeating three times is the interesting part. It says this file has one shape, and that is a property worth knowing before you write a command that depends on it.
3. NF Is a Count; $NF Is a Value
Said once more, because it is the single most common slip with these two and the rest of the chapter depends on it being automatic:
awk 'NR == 1 { print "NF=" NF, "$NF=" $NF }' timing.rptNF=5 $NF=VIOLATEDNF is 5. $NF is VIOLATED. The $ means "the field numbered by what follows", so $NF is "the field numbered 5".
That also means $(NF) and $NF are the same thing:
awk 'NR == 1 { print ($(NF) == $NF) ? "same" : "different" }' timing.rptsameThe parentheses are optional around a bare variable and required as soon as there is arithmetic, which is the next section.
4. Counting Back From the End
NF is a number, so you can do arithmetic with it and index by the result:
awk '{ print $(NF-1), $NF }' timing.rpt-0.124 VIOLATED
0.081 MET
-0.052 VIOLATEDThe last two fields — slack and verdict — without naming either position.
Two back:
awk '{ print $(NF-2) }' timing.rptclk_main
clk_main
clk_io5. The Design Decision
Now the question 3.1 left open. For slack in this report you can write $4 or $(NF-1). Both are correct today. They are not the same decision.
$4 — fixed position | $(NF-1) — from the end | |
|---|---|---|
| Assumes | slack is the 4th field | slack is 2nd from last |
| Survives | a column added after slack | a column added before slack |
| Breaks on | a column inserted before slack | a record that lost a trailing field |
| Reads as | "column 4", precise and literal | "the one before the verdict" |
| Best when | the report's leading columns are the stable part | the report's trailing columns are the stable part |
Neither row is the right answer in general. The right answer depends on the tool, and specifically on which end of its records it keeps stable across versions.
6. Cross-Checking the Two
If two addressing styles should point at the same column, you can simply ask awk whether they do:
awk '{ print ($5 == $NF) ? "agree" : "DIFFER" }' timing.rptagree
agree
agreeOn this file $5 and $NF are the same field, and the check says so on every record.
Now the mixed-shape file from Chapter 3.1 — the five-column report concatenated with the six-column one, which is exactly what you get when a regression spans a tool upgrade:
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_core/u_alu setup clk_main max -0.124 VIOLATED
u_core/u_regfile setup clk_main max 0.081 METawk '{ print NF, ($5 == $NF) ? "agree" : "DIFFER" }' mixed.rpt5 agree
5 agree
5 agree
6 DIFFER
6 DIFFERThe disagreement lines up exactly with the shape change. This is a cheap and surprisingly effective habit: when you have two ways of naming the column you want, assert that they agree, and you will hear about a layout change on the first record rather than from whoever read your output.
7. NF as a Format-Drift Detector
Two practical checks, in increasing order of usefulness.
The distribution of shapes in a file. How many different record shapes are in here at all?
awk '{ print NF }' timing.rpt | sort -u5One line of output means one shape, and a command that assumes five fields is safe on this file. Now the mixed file:
awk '{ print NF }' mixed.rpt | sort -u5
6Two shapes. No single hard-coded position is correct for this file, and knowing that took one command. The sort -u is the shell's, not awk's — counting shapes inside awk needs arrays, which is Module 6.
A self-calibrating check. Better still, let awk learn the expected shape from the first record and flag everything that disagrees:
awk 'NR == 1 { want = NF } NF != want { print "record", NR, "has", NF, "fields, expected", want }' mixed.rptrecord 4 has 6 fields, expected 5
record 5 has 6 fields, expected 5Nothing is hard-coded — not the expected count, not the column. It reports exactly which records broke the pattern established by the file's own first line, and on a clean file it prints nothing at all.
8. Degenerate Cases
Two shapes that are not errors but behave differently from what you might assume.
A blank record has NF of 0:
awk '{ print NR, NF }' bl.txta 1
b 21 2
2 0
3 2Chapter 2.2 introduced this; the consequence for this chapter is that $NF on such a record is $0, the empty whole record, so a last-field reader silently produces nothing rather than failing.
On a one-field record, $NF and $1 are the same field:
awk '{ print NF, ($NF == $1) ? "NF and 1 are the same field" : "differ" }' one.txtlonely1 NF and 1 are the same fieldThis is Chapter 2.2's ragged trap stated as a property rather than an accident. When a record collapses to one field, every "last field" reader starts returning the identifier. NF == 1 is the condition that catches it.
9. An Engineering Example
Report the worst slack in the clean five-column report, using $NF-relative addressing and a shape check together.
awk 'NR == 1 { want = NF } NF != want { bad++; next } $(NF-1) + 0 < worst { worst = $(NF-1) + 0; path = $1 } END { if (bad) print "WARNING:", bad, "records had an unexpected shape"; print "worst slack", worst, "at", path }' timing.rptworst slack -0.124 at u_core/u_aluAnd the same program on the mixed file, where the shape changes halfway through:
awk 'NR == 1 { want = NF } NF != want { bad++; next } $(NF-1) + 0 < worst { worst = $(NF-1) + 0; path = $1 } END { if (bad) print "WARNING:", bad, "records had an unexpected shape"; print "worst slack", worst, "at", path }' mixed.rptWARNING: 2 records had an unexpected shape
worst slack -0.124 at u_core/u_aluThe answer happens to be the same, and that is the point worth drawing out: the two skipped records contained the same two paths already seen, so skipping them cost nothing here. On a real report they might have held the worst path, which is why the warning exists rather than silent skipping.
The engineering caveats are unchanged from Chapter 1.2. worst slack is the minimum of one column of one report — not the design's worst path, because a real report lists each path once per check and per corner, and because VIOLATED is the tool's verdict rather than an independent measurement.
10. Common Mistakes
Writing $NF-1 for the second-to-last field. Section 4. You need $(NF-1).
Treating $NF as a name for a column. It is "the last field", which on a record of a different shape is a different column. Chapter 2.2's trap, and section 8 states it as a property.
Choosing $4 or $NF by habit. Section 5 is a decision, and the deciding factor is which end of the record the tool keeps stable.
Assuming one file has one shape. Section 7's distribution check takes one command and is worth running on any unfamiliar report.
Calibrating a shape check on a header row. The self-calibrating check learns from record 1, so a header makes it flag every data row. Know the file.
Forgetting that NF has no $. Still the most common slip. NF counts; $NF reads.
Expecting NF to be settable harmlessly. It is assignable, and assigning to it truncates the record. That is a tool, not an accident, and it belongs to Chapter 3.6.
11. 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 NF }' timing.rpt5
5
5Three lines, each the field count of one record — not a single total. NF is per record; the count of records is NR, and the total number of fields in the file would need an accumulator.
Exercise 2 — Modify a command
Print the last field and the one before it, in that order — verdict first, then slack.
awk '{ print $NF, $(NF-1) }' timing.rptVIOLATED -0.124
MET 0.081
VIOLATED -0.052Reversing the two expressions from section 4. Both need to be addressed relative to NF, and the second needs its parentheses.
Exercise 3 — Trace the decision
For the six-column report below, say what each of $4, $5, $NF and $(NF-1) evaluates to on record 1, without running anything.
u_core/u_alu setup clk_main max -0.124 VIOLATED
u_core/u_regfile setup clk_main max 0.081 METawk 'NR == 1 { print "$4=" $4, "$5=" $5, "$NF=" $NF, "$(NF-1)=" $(NF-1) }' timing_v2.rpt$4=max $5=-0.124 $NF=VIOLATED $(NF-1)=-0.124$4 is the inserted column. $5 and $(NF-1) both reach the slack, by different routes. $NF is the verdict. On the five-column report, $4 and $(NF-1) would have agreed instead — which is exactly the shift Chapter 3.1 demonstrated, now visible field by field.
Exercise 4 — Debug a last-field reader
A colleague's extractor is awk '{ print $1, $NF }' slack.rpt and they report that one row shows a word instead of a number. Use NF to find which record is responsible and explain it.
path_a setup -0.12
path_b setup
path_c setup -1.20awk 'NF != 3 { print "record", NR, "has", NF, "fields:", $0 }' slack.rptrecord 2 has 2 fields: path_b setupRecord 2 lost its slack value, so it has two fields instead of three, and $NF on it is setup — the check type, not a number. The NF check names the culprit immediately, where reading the output only told them something was wrong somewhere.
awk 'NF == 3 { print $1, $NF } NF != 3 { print $1, "NO_SLACK" }' slack.rptpath_a -0.12
path_b NO_SLACK
path_c -1.20Exercise 5 — Engineering application
Using mixed.rpt, report how many records have each shape, and name the first record where the shape changes.
awk 'NR == 1 { want = NF } NF == want { same++; next } { if (!first) first = NR; diff++ } END { print same, "records with", want, "fields;", diff, "with a different shape; first change at record", first }' mixed.rpt3 records with 5 fields; 2 with a different shape; first change at record 4Three rules and three variables, all from Module 2. The if (!first) records only the earliest changed record, because first is empty — and therefore false — until the first time it is set.
This is the diagnostic to run before writing any positional extractor against an unfamiliar report. It tells you whether a hard-coded column number is even meaningful for the file.
Exercise 6 — Challenge: one expression, two layouts
mixed.rpt contains both report shapes. Find the single expression that reads slack correctly from every record regardless of shape, and explain why it works. Then write the defensive alternative.
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_core/u_alu setup clk_main max -0.124 VIOLATED
u_core/u_regfile setup clk_main max 0.081 METawk '{ print $1, $(NF-1) }' mixed.rptu_core/u_alu -0.124
u_core/u_regfile 0.081
u_pad/u_io -0.052
u_core/u_alu -0.124
u_core/u_regfile 0.081One expression, correct slack from all five records across both layouts. It works because the verdict stayed last in both versions of the report, so slack is second-from-last in both — even though it is field 4 in one shape and field 5 in the other. This is section 5's argument demonstrated: the inserted column moved the head, and addressing relative to a stable tail was immune to it.
No fixed position can do the same. $4 is slack on the five-field records and max on the six-field ones; $5 is the reverse.
The defensive alternative handles each shape explicitly rather than relying on the tail staying put:
awk 'NF == 5 { print $1, $4; next } NF == 6 { print $1, $5; next } { print $1, "UNKNOWN_SHAPE" }' mixed.rptu_core/u_alu -0.124
u_core/u_regfile 0.081
u_pad/u_io -0.052
u_core/u_alu -0.124
u_core/u_regfile 0.081Same output, and a third rule that catches any shape you did not plan for instead of guessing. It is longer, and it states which layouts the script actually supports — which is worth the extra line in anything that runs unattended.
Finally, a warning about a cross-check that looks appealing and is not. You might try $5 == $(NF-1) to confirm the two styles point at the same column. Run it on this file and the five-field records report a difference while the six-field ones agree — the opposite of what you want, because on a five-field record $5 is the verdict and $(NF-1) is the slack, and they genuinely differ. Comparing values cannot tell you whether two expressions name the same position. Compare the positions instead — 5 == NF-1 — or check the shape directly with NF, as section 7 does.
12. Summary
NFis the shape of the current record, not just a number. A command that reads a column has an opinion about that shape.NFcounts;$NFreads.$(NF)and$NFare the same, and$NF-1is the last field minus one rather than the second-to-last field.$4or$(NF-1)is a design decision. A fixed position assumes the leading columns are stable; counting from the end assumes the trailing ones are. Reports usually grow in the middle, which favours the end — except where records can be truncated, where it is exactly wrong.- Two addressing styles can be cross-checked, and the disagreement lines up with the shape change.
- One command tells you how many shapes a file has —
awk '{ print NF }' file | sort -u. More than one line means no hard-coded position is safe. - A self-calibrating check needs nothing beyond Module 2: learn
NFfrom record 1, flag every record that disagrees. It assumes record 1 is representative, so a header row defeats it. - A blank record has
NFof 0 and a one-field record makes$NFthe identifier. Both make a last-field reader quietly return the wrong thing.
You can now choose how to address a column on purpose, and detect when the report stops agreeing with that choice.
Everything so far — this chapter, 3.1, and all of Module 2 — has relied on awk's default whitespace splitting. That default is why the padded, human-formatted reports in these examples parsed so cleanly. It is also why a comma-separated file does not, and why Chapter 2.2 kept promising that single-character separators behave differently. Chapter 3.3 — Field Separators: FS and OFS is where splitting stops being a default and becomes a setting.
Where this fits
Part of the awk curriculum.
