FIELD NOTES / UNIX & LINUXNew tools on weekdays. Beyond the man page.
$printf

Printf: keep the format separate from the data

Build dependable reports and diagnostic streams by choosing exactly how each value is interpreted.

THE DEEP DIVE12 MIN
01

Start with a fixed format

A monitoring script can collect the right values and still produce a misleading report. A filename contains a newline. A number arrives with a leading zero. A string that looks like ordinary text becomes formatting instructions. `printf` gives you control over these boundaries, provided you decide which argument is the format and which arguments are data.

`printf` writes formatted output. Its first argument describes the output; later arguments supply values. This example prints one inventory record. `%s` consumes a string, `%d` consumes an integer, and `\n` in the format emits a newline. Everything else in the format is literal text. Single quotes prevent the shell from changing the format before `printf` sees it.

The output names the node and displays its GPU count. `printf` does not add a newline automatically: the format includes one deliberately. The examples use Bash 5.2.21's built-in `printf`; GNU Coreutils 9.4 provides a separate executable. These are widely available implementations, with extensions that other shells need not share. All input here is synthetic.

TERMINALRun in Bash
printf 'node=%s gpus=%d\n' node07 4
TERMINALExpected standard output
node=node07 gpus=4
02

Give data its own argument

Now an inventory label contains a percent sign and the two literal characters backslash and n. A shell assignment stores the label; the single quotes make its contents literal. `$label` expands the variable, and double quotes preserve its entire value as one argument, including its space.

In `%s`, neither the percent sign nor the backslash sequence inside the supplied value is interpreted as formatting. Only the fixed format is interpreted. This is the useful default for values taken from files, users, or services: choose the format yourself and pass the value separately.

The fixed-format pattern protects the formatting boundary, not every output boundary. An actual terminal-control byte already present in a value can still be written by `%s`. We will inspect that distinction without sending control sequences to a terminal.

TERMINALRun in Bash
label='node07 100% ready\n'
printf '%s\n' "$label"
TERMINALExpected standard output
node07 100% ready\n
03

Why one format prints several records

One `%s` conversion and several arguments are enough to make a line list. After it consumes an argument, `printf` reuses the format while arguments remain. This is useful for emitting known values without a shell loop. The resulting list could feed the sort article's ordering examples; the important point here is the one-record-per-format contract.

Reusing a two-conversion format consumes arguments in pairs. That is convenient until a pair is incomplete. Missing string arguments become empty strings, and missing numeric arguments become zero. `printf` is a formatter, not a validator of your inventory schema. If a zero GPU count is meaningful, distinguish missing data before formatting; do not rely on a missing argument to produce an error.

TERMINALRun in Bash
printf '%s\n' node07 node12
printf '%s:%d\n' node07 4 node12
TERMINALExpected standard output
node07
node12
node07:4
node12:0
04

Align a report without changing the value

For a human-readable report, `%-8s` prints a string in a field at least eight bytes wide in these ASCII examples. The minus flag aligns it to the left; spaces fill the remaining width. `%3d` puts an integer in a field at least three characters wide, right-aligned by default. Literal vertical bars make padding visible.

A field width is a minimum, not a maximum. The second node's long name crosses the intended column boundary. That behavior preserves the complete name, which can be better than silently truncating an identifier. It also means aligned output is unsuitable as an implicit machine-readable schema: downstream consumers should use explicit delimiters and a defined escaping rule.

The short and long records deliberately share one format so the difference is visible. Keeping the format constant makes unexpected input length easier to diagnose.

TERMINALRun in Bash
printf '|%-8s|%3d|\n' node07 4 node-with-long-name 16
TERMINALExpected standard output
|node07  |  4|
|node-with-long-name| 16|
05

Limit a preview, not an identifier

`%.6s` limits a string conversion to six bytes in the tested Bash implementation. Combining width and precision as `%8.6s` allows at most six bytes of data inside a field at least eight positions wide. Here it yields two leading spaces and the six-byte node name.

This can make a compact preview, but two different names can acquire the same prefix. Keep the complete identifier in the actual record. Precision is also not a general Unicode-aware truncation mechanism: a byte limit can split a multibyte UTF-8 character. Terminal column width is another problem again; combining marks and wide characters need more than a byte count.

Dynamic precision uses an asterisk: `%.*s` consumes the precision from the next argument before consuming the string. The integer `4` therefore controls how much of `node07` is displayed, rather than being displayed itself. This is useful when a report's preview length is a setting, but validate a setting before granting it control over output size.

TERMINALRun in Bash
printf '|%8.6s|\n' node07-extra
printf '|%.*s|\n' 4 node07
TERMINALExpected standard output
|  node07|
|node|
06

A leading zero can change a number

`%d` produces decimal output, but that does not mean its input is interpreted only as decimal. Bash and GNU `printf` accept numeric constants: a leading zero denotes octal, and a leading `0x` denotes hexadecimal. Octal `010` is decimal eight; hexadecimal `0x10` is decimal sixteen.

This matters when a collector emits padded counts. `08` is not a valid octal constant in these implementations. A diagnostic and a failing status are evidence of invalid input, not a count you should accept just because something was written. Likewise, formatting does not turn arbitrary text into a reliable integer.

If a value is an identifier, preserve it with `%s`. If it is a count, define and validate the accepted representation before formatting. Do not casually put untrusted strings into shell arithmetic to repair the problem: shell arithmetic has its own grammar. Awk's record-processing article is a better starting point when a structured stream needs validation as well as presentation.

TERMINALRun in Bash
printf '%d\n' 010 0x10
printf '%s\n' 010
TERMINALExpected standard output
8
16
010
07

Choose expansion deliberately

The `%b` conversion interprets backslash escapes in the supplied argument. In this example the argument contains an encoded newline, so one supplied value becomes two displayed lines. `%s` preserves the same argument's literal backslash and n. This is a purposeful choice between text and an escape-encoded representation.

Expansion can do more than insert newlines. The escape `\c` stops further output when processed by `%b`. The second command shows a literal `begin` followed by an encoded stop instruction: neither the remainder of that argument nor the following format text is emitted.

That makes `%b` useful for controlled fixture generation but a poor default for external log fields. A value should not quietly gain the ability to stop the rest of a report. The format and the escape-encoded argument are two different interpretation surfaces; keep both under your control when expansion is intentional.

TERMINALRun in Bash
printf '%s\n' 'node07\nnode12'
printf '%b\n' 'node07\nnode12'
printf '%b END\n' 'begin\cignored'
TERMINALExpected standard output
node07\nnode12
node07
node12
begin

The final begin has no terminating newline; any visual line break after it is display layout.

08

Inspect shell arguments without executing them

Bash's `%q` converts a value to a representation suitable for reuse as shell input. Spaces, semicolons, and dollar signs are escaped in this ASCII example. Those bytes otherwise have special meanings in shell syntax. The output lets you inspect what a troublesome argument actually contains; it does not run the argument.

The backslashes in the result describe shell quoting. They are not added to the original variable. GNU's separate `printf` also supports `%q`, but may choose a different representation. Treat this example as Bash behavior, not a universal serialization format.

Do not use shell quoting as SQL quoting, JSON escaping, or HTML encoding. Those consumers have different grammars. Even for shells, retaining values as arguments is usually better than generating a command string and parsing it again. For a diagnostic transcript, `%q` is handy precisely because it shows boundaries that ordinary output hides.

TERMINALRun in Bash
label='node 07; $HOME'
printf '%q\n' "$label"
TERMINALExpected standard output
node\ 07\;\ \$HOME
09

Produce zero-byte separators

Line boundaries are not always record boundaries: filenames can contain newlines. `\0` in this format emits a NUL byte, whose value is zero. On Unix, a filename cannot contain NUL, making it a useful separator for filename streams. The find article explains why path handling needs this distinction.

A pipe, written `|`, sends one command's output to the next command's input. `od` displays bytes; `-An` suppresses offset addresses, and `-tx1` displays each byte as two hexadecimal digits. The output below shows `00` after each name, while `0a` lies inside the second name. It is part of the data, not the separator.

Use a receiving tool that understands NUL-separated records. Bash variables cannot hold NUL bytes; Bash command substitution discards them, so capturing this stream in a variable destroys its framing. Keep the stream in a pipe or file. NUL is a transport contract, not a cosmetic variation on a newline.

TERMINALRun in Bash
printf '%s\0' node07 'node
12' | od -An -tx1
TERMINALExpected standard output
 6e 6f 64 65 30 37 00 6e 6f 64 65 0a 31 32 00
10

Inspect a hostile-looking label as bytes

This isolated example uses `%b` to construct an actual escape byte from `\033`, an octal byte notation. Bash's `-v label` option assigns the formatted result to the variable named `label` instead of writing it to standard output. The fixture contains an escape sequence that many terminals interpret as a screen-clearing instruction.

The next command passes the value through the fixed `%s` format into `od`, so the control sequence reaches the byte inspector rather than the terminal. Hexadecimal `1b` is the escape byte; `5b 32 4a` represents `[2J`. No format injection occurs: `%s` treats the argument as data. Yet the output contains a terminal instruction because that instruction was already part of the data.

For evidence collection, preserve the original bytes and inspect a derived view. Stripping control characters may make a display quieter while also removing evidence. The safe-format rule solves one specific problem; it does not certify a string as harmless for every consumer. This is misuse of interpretation boundaries, not a claim of a vulnerability in these tested `printf` implementations.

TERMINALRun in Bash
printf -v label '%b' 'node07\033[2J'
printf '%s' "$label" | od -An -tx1
TERMINALExpected standard output
 6e 6f 64 65 30 37 1b 5b 32 4a
11

Select the implementation and keep the contract

A bare `printf` in Bash normally selects its built-in command. `env printf` asks `env` to launch the executable found through the command search path instead. On the tested host that is GNU Coreutils 9.4. The following fixed-format example works in both; Bash-specific `-v` is not a GNU executable option.

Bash also recognizes format features absent from the GNU executable, including `%n`, which assigns the number of bytes written so far to a shell variable named by its argument. GNU's supported conversion list does not include it. This is another reason an externally supplied format is not equivalent to a harmless string. It does not justify treating the shell utility as the C library function's memory-writing exploit mechanism.

For portable scripts, keep to the documented subset your target shells and utilities support. For deliberate Bash diagnostics, use its extensions and name the dependency. The recurring choice is small but consequential: identify the data, choose the representation, and know which program interprets each byte. A report is trustworthy when those contracts survive unusual input.

TERMINALRun in Bash
env printf 'node=%s gpus=%d\n' node07 4
TERMINALExpected standard output
node=node07 gpus=4
Back to latest issue
HELP SHAPE THE NEXT ISSUE

What belongs in your toolbox?

Choose a command you want to know better. We start with familiar tools and work toward the less obvious ones.

READER’S CHOICE

What should we cover next?

Vote for a future deep dive, or suggest a command we missed.

One command name, up to 32 characters. Suggestions are reviewed by a person. A new article starts a fresh poll.

Loading poll…