awk Part II · Module 7 · Input, Output, and Multi-File Processing
Output Formatting
Chapter 7.1 established that printf emits exactly the format string you wrote. This chapter is about writing it: the conversions, the width and precision modifiers, and the alignment that turns a list of values into a table somebody can read. Three behaviours cost real time to discover. A field width is a minimum rather than a maximum, so a single long instance name shunts every column after it out of line. %d and %.0f disagree about what to do with a fractional value. And %.0f does not round the way you were taught — it turns 2.5 into 2 and 3.5 into 4, by design.
Foundation16 min readawkPart IIprintfFormattingReports
Part II · Module 7 · Chapter 7.2 · Output Formatting
1. Why This Matters
A report nobody can read is a report nobody uses. Alignment is most of readability — a column of numbers with their decimal points in line can be scanned, and the same numbers ragged cannot.
printf does that, and it does it with a small set of conversions that behave slightly differently from the ones in C or Python. The three worth knowing before you write a table are in sections 4, 6 and 7.
2. The Conversions
Every conversion this awk supports, in one command:
awk 'BEGIN { printf "d:%d i:%i f:%f e:%e g:%g s:%s c:%c x:%x o:%o pct:%%\n", 42, 42, 3.14159, 31415.9, 31415.9, "str", 65, 255, 8 }'d:42 i:42 f:3.141590 e:3.141590e+04 g:31415.9 s:str c:A x:ff o:10 pct:%| Conversion | Meaning | Note |
|---|---|---|
%d | integer | truncates a fraction — section 6 |
%i | integer | not POSIX; works here, but write %d |
%f | fixed-point | six decimals by default |
%e | scientific | six decimals by default |
%g | shorter of %e / %f | six significant digits |
%s | string | goes through CONVFMT for a number — section 8 |
%c | one character | type-dependent — section 9 |
%x / %o | hex / octal | for a bit pattern or a mask |
%% | a literal % | not a conversion, takes no argument |
%f's default of six decimals is more than any engineering measurement warrants, which is why almost every real format string carries an explicit precision.
%% is worth noting because it is the one that breaks argument counting — Chapter 7.1's final exercise.
3. Width and Alignment
A number between % and the conversion letter is a field width. A minus sign makes it left-aligned; the default is right.
u_alu 1820 -0.1245
u_regfile_bank_long_name 2410 0.5
u_io 640 -1.2awk '{ printf "%-28s %8d %9.3f\n", $1, $2, $3 }' timing.rptu_alu 1820 -0.124
u_regfile_bank_long_name 2410 0.500
u_io 640 -1.200Three columns in line. %-28s left-aligns the name in 28 characters, %8d right-aligns the count in 8, and %9.3f right-aligns the slack in 9 with exactly 3 decimals.
Right-alignment for numbers and left for names is the convention worth following: digits line up by place value only when right-aligned, and names are easier to scan when their first letters line up.
Note -0.1245 printed as -0.124. %.3f rounds to three decimals — and which way it rounds is section 7.
4. A Field Width Is a Minimum, Not a Maximum
Here is the behaviour that quietly ruins tables.
short 1
u_a_very_long_instance_name_indeed 2
mid 3awk '{ printf "%-20s %5d\n", $1, $2 }' wide.rptshort 1
u_a_very_long_instance_name_indeed 2
mid 3The second row is not truncated to 20 characters — it is printed in full, and the number column that should have started at position 21 starts at 35 instead.
A field width guarantees a value will occupy at least that many characters. It promises nothing about the maximum.
5. Truncating Deliberately
A precision on %s sets a maximum:
awk '{ printf "%-20.20s %5d\n", $1, $2 }' wide.rptshort 1
u_a_very_long_instan 2
mid 3Alignment restored. %-20.20s means "at least 20, at most 20" — exactly 20 characters, padded or cut.
The cost is in the second row: u_a_very_long_instan is not a name anybody can look up. Truncation is right for a scannable overview and wrong for anything a reader must act on, because two different long names can truncate to the same string and become indistinguishable.
A compromise that keeps the distinguishing end of a path:
awk '{ n = $1; if (length(n) > 20) n = "..." substr(n, length(n) - 16); printf "%-20s %5d\n", n, $2 }' wide.rptshort 1
...tance_name_indeed 2
mid 3Aligned, and the informative end of the name is kept. The arithmetic is worth checking rather than trusting — "..." is 3 characters and substr(n, length(n) - 16) takes from 17 characters before the end, giving 17, for 20 in total:
awk '{ n = $1; if (length(n) > 20) n = "..." substr(n, length(n) - 16); printf "[%s] len=%d\n", n, length(n) }' wide.rpt[short] len=5
[...tance_name_indeed] len=20
[mid] len=3Exactly 20, so it fills the field without overflowing it. substr() and length() are Module 8's functions; the point here is that an off-by-one in that offset would silently reintroduce section 4's overflow, so print the length when you build a width-sensitive string.
Which end to keep is an engineering judgement. A hierarchical instance name is usually most distinguishable at its end — u_core/u_alu/u_adder and u_core/u_fifo/u_adder share a long prefix — so truncating the front and keeping the tail preserves more information than the reverse.
6. %d Truncates, %.0f Rounds
Two ways to print a fractional value as a whole number, and they disagree:
awk 'BEGIN { printf "%%d: %d %d %d %d\n", 1.9, -1.9, 2.5, -2.5; printf "%%.0f: %.0f %.0f %.0f %.0f\n", 1.9, -1.9, 2.5, -2.5 }'%d: 1 -1 2 -2
%.0f: 2 -2 2 -2| Value | %d | %.0f |
|---|---|---|
| 1.9 | 1 | 2 |
| −1.9 | −1 | −2 |
| 2.5 | 2 | 2 |
| −2.5 | −2 | −2 |
%d truncates toward zero — it discards the fraction, so 1.9 becomes 1. %.0f rounds.
For a count this rarely matters, because counts are already integers. For a derived figure it matters a lot: a coverage percentage of 89.7% printed with %d reads as 89, which is below a 90% gate that the real value passes.
%d also has a ceiling. It is a 64-bit integer conversion and it saturates rather than wrapping or erroring:
awk 'BEGIN { printf "%d\n%d\n%d\n", 2147483648, 1e18, 1e19 }'2147483648
1000000000000000000
9223372036854775807The first two are exact. 1e19 came back as 9223372036854775807 — the largest signed 64-bit integer — which is not the value given and is reported without complaint. No realistic cell count reaches that, but a product of two large numbers can, and the failure is silent.
7. %.0f Rounds Half to Even
The rounding rule is not the one most people were taught. Every half-way value:
awk 'BEGIN { for (i = 0; i <= 6; i++) printf "%.1f -> %.0f\n", i + 0.5, i + 0.5 }'0.5 -> 0
1.5 -> 2
2.5 -> 2
3.5 -> 4
4.5 -> 4
5.5 -> 6
6.5 -> 6Not 1, 2, 3, 4, 5, 6, 7. Every .5 goes to the nearest even integer — 0.5 down to 0, 1.5 up to 2, 2.5 down to 2, 3.5 up to 4.
This is round-half-to-even, the IEEE 754 default, and it is deliberate: rounding every half upward biases a long column of numbers upward, while rounding to even cancels out across many values.
8. %s on a Number Loses Precision
%s converts a number to a string using CONVFMT, which defaults to %.6g — six significant digits (Chapter 5.2, section 12):
awk 'BEGIN { x = 1234567.89; printf "%%s: %s\n%%d: %d\n%%.2f: %.2f\n%%g: %g\n", x, x, x, x }'%s: 1.23457e+06
%d: 1234567
%.2f: 1234567.89
%g: 1.23457e+06The value is intact internally. Only %.2f displays it faithfully.
So %s is the wrong conversion for a number you care about. It is convenient when a value might be either a number or text — a slack column that sometimes says VIOLATED — and in that case the precision loss is a real risk worth knowing about.
For a value that is definitely numeric, name the conversion: %d for a count, %.Nf for a measurement.
9. %c Is Type-Dependent
%c prints one character, and what it does depends on whether the argument is a number or a string:
awk 'BEGIN { printf "number 65: [%c] string \"65\": [%c] string \"ABC\": [%c]\n", 65, "65", "ABC" }'number 65: [A] string "65": [6] string "ABC": [A]A number is a character code — 65 is A. A string contributes its first character — "65" gives 6.
That is the same numeric-string distinction behind Chapter 5.1's truthiness ("0" is true, a field holding 0 is false) and Chapter 5.4's comparison rule. It appears here as a third face of one rule: awk decides what an operation means from the value's origin, not from a declared type.
%c is rarely needed in report processing. It is worth recognising because a field used with %c will behave differently depending on whether it came from input or from a literal.
10. A Width Computed at Run Time
A * takes the width from the argument list:
awk 'BEGIN { printf "[%*s] [%-*s]\n", 8, "x", 8, "y" }'[ x] [y ]Right-aligned in 8, then left-aligned in 8, with both widths supplied as arguments.
This is what makes section 11's measure-then-print approach possible: the width is not known when the format string is written.
11. An Aligned Table, Measured First
The complete answer to section 4 is to measure the data before formatting it. That needs two passes, which for a file means naming it twice:
awk 'NR == FNR { if (length($1) > w) w = length($1); next } { printf "%-*s | %5d\n", w, $1, $2 } END { printf "(measured %d characters)\n", w }' wide.rpt wide.rptshort | 1
u_a_very_long_instance_name_indeed | 2
mid | 3
(measured 34 characters)Perfectly aligned, nothing truncated, and the width reported so a reader knows it was measured rather than guessed.
The mechanism is Chapter 7.5's two-file idiom reading the same file twice: NR == FNR is true only during the first pass, which measures and discards; the second pass formats using the w the first pass found. %-*s takes that width as an argument, which is section 10's feature doing the only job it is really needed for.
The | marker is not decoration. Without it, a name that exactly fills its field abuts the next column and the row looks misaligned when it is not — a column separator makes the alignment checkable at a glance, which is worth one character per row while you are developing the format.
Two costs. The input is read twice, so it must be a real file rather than a pipe, and the I/O doubles. And the whole report is held hostage to its widest row — a single 200-character name gives every row a 200-character first column. When either cost bites, truncate with %.Ns and accept the loss, or move the column last.
12. Common Mistakes
Expecting a field width to truncate. It is a minimum — section 4.
Choosing a width from test data. Real instance names carry hierarchy and run long.
Using %d on a percentage. It truncates, so 89.7 reads as 89 — section 6.
Expecting %.0f to round halves up. It rounds to even: 2.5 becomes 2 — section 7.
Letting a rounded value decide a gate. Compare the raw value; round for display only.
Using %s for a number that matters. CONVFMT gives six significant digits — section 8.
Using %i. It works here and is not POSIX. Write %d.
Forgetting that %% takes no argument. It is the usual cause of an argument-count mismatch.
Putting a %-Ns last in the format. The padding becomes trailing whitespace on every line.
Assuming %c on a field gives a character code. A field is a string, so you get its first character — section 9.
13. Exercises
Exercise 1 — Predict the output
awk 'BEGIN { printf "[%5s][%-5s][%5.2f]\n", "ab", "ab", 3.14159 }'[ ab][ab ][ 3.14]Right-aligned in 5, left-aligned in 5, then 5 wide with 2 decimals.
Exercise 2 — Build an aligned table
Format the timing report with the name left-aligned, the count right-aligned, and the slack to three decimals.
u_alu 1820 -0.1245
u_regfile_bank_long_name 2410 0.5
u_io 640 -1.2awk '{ printf "%-26s %7d %8.3f\n", $1, $2, $3 }' timing.rptu_alu 1820 -0.124
u_regfile_bank_long_name 2410 0.500
u_io 640 -1.200The 24-character name fits inside 26, so every column lines up.
Exercise 3 — Debug a broken table
A per-instance report is misaligned for some rows.
short 1
u_a_very_long_instance_name_indeed 2
mid 3awk '{ printf "%-20s %5d\n", $1, $2 }' wide.rptshort 1
u_a_very_long_instance_name_indeed 2
mid 3The 34-character name exceeds the 20-wide field. A width is a minimum, so it printed in full and pushed the number column right.
Two fixes, with different trade-offs:
awk '{ printf "%-20.20s %5d\n", $1, $2 }' wide.rptshort 1
u_a_very_long_instan 2
mid 3awk '{ printf "%5d %s\n", $2, $1 }' wide.rpt 1 short
2 u_a_very_long_instance_name_indeed
3 midThe first truncates and loses the name. The second puts the unpredictable column last, where an overflow pushes nothing — no data lost and no misalignment, at the cost of a less conventional column order.
Exercise 4 — %d against %.0f
Predict both lines.
awk 'BEGIN { printf "%d %d %d\n", 89.7, 2.5, -2.5; printf "%.0f %.0f %.0f\n", 89.7, 2.5, -2.5 }'89 2 -2
90 2 -2%d truncates toward zero. %.0f rounds — and rounds 2.5 to 2, not 3, because halves go to even.
Exercise 5 — The rounding gate
Show that three different raw values can display identically and still produce different verdicts.
awk 'BEGIN { for (v = 89.5; v <= 90.5; v += 0.5) printf "raw %.1f displays %.0f gate %s\n", v, v, (v >= 90 ? "PASS" : "FAIL") }'raw 89.5 displays 90 gate FAIL
raw 90.0 displays 90 gate PASS
raw 90.5 displays 90 gate PASSAll three display 90. The first fails the gate. A reviewer given only the displayed column cannot tell which row failed or why — so print enough decimals to make the verdict reconstructible.
Exercise 6 — Engineering application
Produce a signoff table: instance, cell count, slack to three decimals, and a verdict — with the threshold stated and the unpredictable column placed so it cannot misalign the rest.
awk '{ printf "%8d %8.3f %-8s %s\n", $2, $3, ($3 + 0 < 0 ? "VIOLATED" : "MET"), $1 } END { printf "(threshold: slack >= 0; %d paths)\n", NR }' timing.rpt 1820 -0.124 VIOLATED u_alu
2410 0.500 MET u_regfile_bank_long_name
640 -1.200 VIOLATED u_io
(threshold: slack >= 0; 3 paths)Every numeric column aligns because the instance name — the only field whose width is unbounded — is last. The verdict is a fixed-width %-8s, so it aligns too, and the threshold is printed so the VIOLATED labels can be checked against it.
The engineering caveat Chapter 5.4 established applies: slack >= 0 is a policy, and a path at exactly 0.000 is reported MET by this command. Whether that is right is a signoff decision, not a formatting one — which is why the rule is in the output.
Exercise 7 — Challenge: a table that measures itself
Write a report that measures its widest instance name, formats every row to fit, and states the width it used.
awk 'NR == FNR { if (length($1) > w) w = length($1); next } { printf "%-*s | %8.3f\n", w, $1, $3 } END { printf "(name column measured at %d characters)\n", w }' timing.rpt timing.rptu_alu | -0.124
u_regfile_bank_long_name | 0.500
u_io | -1.200
(name column measured at 24 characters)w = 24 is the measured length of u_regfile_bank_long_name, not a guess, so the table cannot be broken by a name this file happens to contain.
It can still be broken by a name it does not contain, which is the limit worth naming: the measurement covers the rows in this input only. A second report with a longer name produces a different column width, so two such tables are not comparable side by side and a diff between last night's and tonight's shows every line changed when one name grew.
Fixing the width makes successive runs comparable at the cost of handling the overflow yourself:
awk 'NR == FNR { if (length($1) > w) w = length($1); next } { n = $1; if (length(n) > 24) n = "..." substr(n, length(n) - 20); printf "%-24.24s | %8.3f\n", n, $3 } END { printf "(fixed width 24; widest name seen was %d)\n", w }' timing.rpt timing.rptu_alu | -0.124
u_regfile_bank_long_name | 0.500
u_io | -1.200
(fixed width 24; widest name seen was 24)Same table, and now the trailer reports the widest name seen against the width used — so if a future run contains a 40-character name, the output still aligns and the footer says 40, which is the signal to widen the column deliberately.
That pattern generalises: a fixed format plus a measurement of what the data actually needed gives stable output and tells you when the format has become wrong. A self-measuring format gives perfect output and silently changes shape underneath you.
14. Summary
- A field width is a minimum, not a maximum. A long value prints in full and displaces every column after it.
%.Nssets a maximum, preserving alignment and silently losing data.- Putting the unpredictable column last removes the problem instead of managing it.
%dtruncates toward zero;%.0frounds. A percentage printed with%dreads half a point low.%dsaturates at the 64-bit maximum without complaint.%.0frounds halves to even — 2.5 to 2, 3.5 to 4. Not the rule you were taught.- Never let a rounded value decide a gate. Compare the raw value and round for display.
%son a number goes throughCONVFMTand shows six significant digits.%cis type-dependent: a number is a character code, a string contributes its first character.%*stakes its width as an argument, which is what makes a measured table possible.- Right-align numbers, left-align names, and state any threshold in the output.
Your output can now hold its shape. Chapter 7.3 — Redirecting Output sends it somewhere other than standard output: different records to different files, a pipe into another command, and the close() call that is both the fix for one problem and the cause of another — a > that re-truncates on every record.
Where this fits
Part of the awk curriculum.
