Skip to content
Shell & TerminalDeep Dive Published Updated 3 min readViews unavailable

Why printf Is the Portable Shell Output Primitive and echo Is Not

A byte-accurate comparison of POSIX printf and echo covering option ambiguity, backslashes, format-string safety, arbitrary values, locales, and NUL limits.

echo is convenient for fixed human messages, but its behavior becomes implementation-defined when an operand is -n or contains backslashes, and shells add incompatible options such as -e. POSIX printf has an explicit format and predictable escape processing. For scripts that must emit exact bytes, printf is the portable default.

Values are not options or format strings

This is unsafe for arbitrary data:

echo "$value"

If value is -n, some implementations suppress the newline. If it contains \c or \n, implementations disagree about interpretation. echo -- "$value" is not a portable escape hatch because POSIX echo does not require -- option handling.

Use a constant format:

printf '%s\n' "$value"

Never use untrusted data as the first argument:

# Wrong: percent sequences in $value become directives.
printf "$value"

# Correct:
printf '%s' "$value"

The format controls conversion and is reused as needed for extra operands. %s treats its corresponding argument as string data. Quote expansions so the shell does not split words or expand pathnames before printf receives them.

Escapes belong in the format

Put deliberate newlines, tabs, and literal percent signs in a single-quoted format:

printf 'name=%s\tstatus=%d%% complete\n' "$name" "$percent"

Single quotes keep the shell from consuming backslashes; printf interprets the format. %b asks printf to interpret backslash escapes in an argument and is useful only when that mini-language is intended. Applying %b to untrusted content can turn visible characters into controls or terminate output with \c under specified behavior.

Numeric conversions parse operands according to utility rules and locale. Validate numbers before formatting and set a deliberate locale when output is machine-readable. Width such as %08d is a minimum field width, not a range check; a larger number produces more characters.

Shell variables cannot contain NUL

POSIX shell command substitutions and variables are not a binary-safe container for arbitrary byte streams. A NUL byte cannot be reliably stored in a shell variable. printf '\0' may emit one directly depending on the specified format escape, but data=$(...) cannot preserve it as ordinary shell text. Use files, pipes, or a binary-capable language for binary protocols.

Command substitution also removes trailing newline characters. Capturing printf '%s\n' "$value" and later printing the variable does not preserve the exact record ending. If byte identity matters, redirect to a file or pipe it directly.

printf itself may be a shell builtin or external utility. The POSIX interface is the portability baseline; extensions such as Bash printf -v, %q, or time formats should be gated by a declared shell. %q output is shell-specific and should not be mistaken for a universal serialization format.

Choose output format by consumer

For humans, clear text with stderr for diagnostics is sufficient. For machines, define a delimiter/escaping grammar or use JSON/CSV tooling that understands the full format. printf '%s\n' cannot make embedded newlines unambiguous in a line protocol. Filenames may contain newlines, so use NUL-delimited interfaces only through tools that explicitly support them.

Test empty values, -n, backslashes, percent signs, spaces, globs, newlines, non-ASCII text, locale changes, and leading dashes across every target shell. printf does not solve every serialization problem, but it cleanly separates a constant output grammar from variable data—exactly the boundary portable shell code needs.

Related:

Sources:

Comments