Skip to content

printf %n

 2 min C’s notorious write-through format directive lives on in bash, where one format string yields output and measured byte offsets.
Published

Jun 22, 2024

Bash’s printf builtin implements C’s %n:

printf '%nthen we went%n for a ride\n%n' o m g
echo $o $m $g   # 0 12 24

Instead of consuming an argument to print, %n writes the count of bytes emitted so far into the shell variable named by the corresponding argument. One format string yields the rendered output and measured byte offsets into it: o gets 0, m gets 12 (the end of “then we went”), g gets 24 (everything, newline included).

In C, %n writes through a pointer, which made it the classic format-string attack primitive—the one directive that turns “print” into “write to memory.” In bash the pointer-writing half is gone: the target is a shell variable, named by an ordinary argument, created if it doesn’t exist. The useful half survives—measuring layout while producing it.

That’s the actual use under a narrow contract: byte-offset math where each emitted byte occupies one display column. Print a label, capture the count, and compute padding or alignment instead of re-deriving it with ${#...} arithmetic over every piece of the format. Under the same contract, the number can feed cursor-movement escapes.

Gotchas, in order of likelihood. This is Bash’s builtin interface; another shell or an external printf has its own format rules. Do not assume that the same spelling sets a variable there. The count is bytes, not display columns: multibyte UTF-8, tabs and other control characters, and terminal escape sequences all break the equivalence. Only printable, single-column ASCII makes the byte count a column count.