Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
d:42  i:42  f:3.141590  e:3.141590e+04  g:31415.9  s:str  c:A  x:ff  o:10  pct:%
ConversionMeaningNote
%dintegertruncates a fraction — section 6
%iintegernot POSIX; works here, but write %d
%ffixed-pointsix decimals by default
%escientificsix decimals by default
%gshorter of %e / %fsix significant digits
%sstringgoes through CONVFMT for a number — section 8
%cone charactertype-dependent — section 9
%x / %ohex / octalfor 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.

Azvya Education Pvt. Ltd.VLSI Mentor
timing.rpt — synthetic teaching data. An instance, a cell count, and a slack value.
u_alu 1820 -0.1245
u_regfile_bank_long_name 2410 0.5
u_io 640 -1.2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%-28s %8d %9.3f\n", $1, $2, $3 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_alu                            1820    -0.124
u_regfile_bank_long_name         2410     0.500
u_io                              640    -1.200

Three 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.

Azvya Education Pvt. Ltd.VLSI Mentor
wide.rpt — synthetic. One name much longer than the others.
short 1
u_a_very_long_instance_name_indeed 2
mid 3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%-20s %5d\n", $1, $2 }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
short                    1
u_a_very_long_instance_name_indeed     2
mid                      3

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%-20.20s %5d\n", $1, $2 }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
short                    1
u_a_very_long_instan     2
mid                      3

Alignment 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ n = $1; if (length(n) > 20) n = "..." substr(n, length(n) - 16); printf "%-20s %5d\n", n, $2 }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
short                    1
...tance_name_indeed     2
mid                      3

Aligned, 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ n = $1; if (length(n) > 20) n = "..." substr(n, length(n) - 16); printf "[%s] len=%d\n", n, length(n) }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[short] len=5
[...tance_name_indeed] len=20
[mid] len=3

Exactly 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
%d:   1 -1 2 -2
%.0f: 2 -2 2 -2
Value%d%.0f
1.912
−1.9−1−2
2.522
−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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { printf "%d\n%d\n%d\n", 2147483648, 1e18, 1e19 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2147483648
1000000000000000000
9223372036854775807

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { for (i = 0; i <= 6; i++) printf "%.1f -> %.0f\n", i + 0.5, i + 0.5 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0.5 -> 0
1.5 -> 2
2.5 -> 2
3.5 -> 4
4.5 -> 4
5.5 -> 6
6.5 -> 6

Not 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):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { x = 1234567.89; printf "%%s:    %s\n%%d:    %d\n%%.2f:  %.2f\n%%g:    %g\n", x, x, x, x }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
%s:    1.23457e+06
%d:    1234567
%.2f:  1234567.89
%g:    1.23457e+06

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { printf "number 65: [%c]   string \"65\": [%c]   string \"ABC\": [%c]\n", 65, "65", "ABC" }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { printf "[%*s] [%-*s]\n", 8, "x", 8, "y" }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[       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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
short                              |     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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { printf "[%5s][%-5s][%5.2f]\n", "ab", "ab", 3.14159 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[   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.

Azvya Education Pvt. Ltd.VLSI Mentor
timing.rpt
u_alu 1820 -0.1245
u_regfile_bank_long_name 2410 0.5
u_io 640 -1.2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%-26s %7d %8.3f\n", $1, $2, $3 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_alu                         1820   -0.124
u_regfile_bank_long_name      2410    0.500
u_io                           640   -1.200

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
wide.rpt
short 1
u_a_very_long_instance_name_indeed 2
mid 3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%-20s %5d\n", $1, $2 }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
short                    1
u_a_very_long_instance_name_indeed     2
mid                      3

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%-20.20s %5d\n", $1, $2 }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
short                    1
u_a_very_long_instan     2
mid                      3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "%5d  %s\n", $2, $1 }' wide.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    1  short
    2  u_a_very_long_instance_name_indeed
    3  mid

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { printf "%d %d %d\n", 89.7, 2.5, -2.5; printf "%.0f %.0f %.0f\n", 89.7, 2.5, -2.5 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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") }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
raw 89.5  displays 90  gate FAIL
raw 90.0  displays 90  gate PASS
raw 90.5  displays 90  gate PASS

All 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_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.
  • %.Ns sets a maximum, preserving alignment and silently losing data.
  • Putting the unpredictable column last removes the problem instead of managing it.
  • %d truncates toward zero; %.0f rounds. A percentage printed with %d reads half a point low.
  • %d saturates at the 64-bit maximum without complaint.
  • %.0f rounds 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.
  • %s on a number goes through CONVFMT and shows six significant digits.
  • %c is type-dependent: a number is a character code, a string contributes its first character.
  • %*s takes 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.