Desk Clarity

A field guide to everyday digital problems.

Developer tools / Practical guide

Printf Misreads Percent Signs in a Variable: Keep the Format Literal

Pass data as a separate argument to printf.

By ON Lab2 min readSources checked November 10, 2026 (KST)

Pass data as a separate argument to printf. A quoted variable used as the format is still a format: quoting prevents shell splitting but does not disable printf conversion directives.

Reproduce in an empty temporary directory

This isolated example creates only fixture data in its working directory or runs local JavaScript. Run it separately from your project so the named files cannot overwrite real work.

payload='progress: 50% complete; literal \n'
printf '%s\n' "$payload" > output.txt
IFS= read -r observed < output.txt
test "$observed" = "$payload"
printf 'preserved: <%s>\n' "$observed"
# Avoid: printf "$payload"
# Use %% only inside a deliberately authored format:
printf 'fixed progress: %d%%\n' 50

Observed local output

preserved: <progress: 50% complete; literal \n>
fixed progress: 50%

What to compare

The first output preserves both the percent character and the backslash-n text in the data. The second deliberately formats an integer and a literal percent sign. These are different inputs: an authored format and a data argument. Adding shell quotes around the data is necessary for argument boundaries, but it does not change which argument printf treats as its format.

Choose the next action

Use a fixed %s format for arbitrary text and add a newline only when the output contract calls for one. For multiple records, provide each value as its own argument or process records using the intended delimiter. If you need numeric formatting, validate the numeric input and keep the format under application control. Include percent signs, spaces and backslashes in a small fixture so a later refactor cannot accidentally reinterpret them.

Limits and verification

The %b directive intentionally interprets backslash escapes and is a different contract from %s. Do not switch to it simply because the input contains slash characters. This fixture uses the Bash builtin and writes only local temporary files. It does not define escaping for JSON, CSV, HTML or terminal control sequences; use a format-specific serializer for those outputs.

Related: Bash Read Changes Backslashes or Spaces: Choose Raw Line Input Explicitly.

Isolated example executed on macOS on November 10, 2026 (KST), using Bash 3.2, Python 3 for the byte fixture, and Node v26.5.0. Results describe the example, not your application or every runtime version.

Sources & further reading