Print newlines portably in shell scripts
Use printf with a fixed format, separate data from formatting, and diagnose the shell, bytes, and downstream consumer before changing production output.
When a shell script prints \n instead of moving to the next line, the reliable fix is usually printf, not another variation of echo. The deeper production issue is deciding whether backslashes belong to the format or to untrusted data, then verifying the exact bytes that the next program receives.
The stable baseline
printf 'Hello,\nWorld!\n'
Here the format is a constant controlled by the script. printf interprets the newline escapes in that format and writes a final line terminator. For variable data, keep the data out of the format:
message='deployment complete'
printf '%s\n' "$message"
This separation is both safer and more predictable. If $message contains %s, backslashes, spaces, glob characters, or begins with a dash, it is still printed as data. Never write printf "$message"; percent sequences in the value would become formatting directives.
Why echo changes across environments
echo has historical variants. In Bash, -e enables backslash expansion and -E disables it, but the xpg_echo option can change defaults. The external utility, another shell's builtin, an alias, and a compatibility mode can behave differently. Bash's echo also does not treat -- as a conventional end-of-options marker.
That explains the classic “works in my terminal” failure: an interactive Bash session, a script invoked as sh, a minimal container, and a CI runner may resolve the same word to different implementations or options. GNU Coreutils therefore recommends printf as the more portable and flexible choice, especially for unknown values.
Diagnose before editing the script
printf 'shell=%s\n' "$0"
type echo
type printf
printf '%s\n' "$BASH_VERSION"
Run diagnostic commands only in an authorized test context; do not add environment disclosure to public logs. Confirm the script's shebang and how the scheduler actually invokes it. #!/usr/bin/env bash and #!/bin/sh promise different languages, while sh script overrides the script's Bash shebang.
Then inspect bytes rather than trusting terminal rendering:
output_file=$(mktemp)
trap 'rm -f "$output_file"' EXIT
printf 'Hello,\nWorld!\n' >"$output_file"
od -An -tx1 -c "$output_file"
A line feed is byte 0a. The two literal characters backslash and n are 5c 6e. This test distinguishes generation problems from viewer problems. A log collector may escape embedded newlines for a one-record-per-line format; JSON represents a newline as \n in serialized text even though a decoder reconstructs the character.
Literal formats, variable data, and %b
If a trusted script literal defines layout, put escapes in the fixed format. If a value is ordinary data, use %s. Bash printf also provides %b, which interprets backslash escapes in its argument:
trusted_template='first\nsecond'
printf '%b\n' "$trusted_template"
Use %b only when interpreting escapes is an explicit part of the data contract. It can turn \t, \xNN, and other sequences into control characters; Bash documents \c as stopping further output for %b. Applying it blindly to user input can corrupt records, forge multiline logs, or change a downstream protocol.
For multiple values, repeat one fixed format:
printf '%s\n' "$first" "$second" "$third"
Do not use command substitution to retain trailing newlines: the shell removes trailing newline characters from $(...). Write directly to the destination, or use a representation whose framing is explicit.
Line endings and protocol boundaries
Unix text convention uses LF. Some network protocols and Windows-oriented consumers require CRLF, written in a controlled format as \r\n. Do not globally replace line endings until the receiving specification is known. A script can be generating correct LF while a file checked out with CRLF introduces stray carriage returns into commands or comparisons.
Shell variables cannot contain the NUL byte, so shell string handling is unsuitable for arbitrary binary payloads. For filenames, prefer NUL-delimited producer/consumer pairs such as find ... -print0 and a corresponding reader rather than newline framing.
Verification that matches production
- Run the script through its real interpreter and invocation path, not only an interactive shell.
- Test empty strings, values beginning with
-nor-e, percent signs, backslashes, spaces, non-ASCII text, and a missing final newline. - Capture the output bytes and feed the fixture to the actual parser, service manager, log collector, or API client.
- Check exit status and write failures. A successful format decision does not help if a full filesystem or closed pipe loses output.
- Use ShellCheck or an equivalent review aid, then retain runtime tests because portability depends on the selected shell.
Deployment, rollback, and failure recovery
Output is an interface. Changing literal \n into real records can alter line counts, signatures, parsers, monitoring rules, and log ingestion costs. Stage the new output beside the old, compare bytes and consumer results, then switch writers. For pipelines with side effects, replay a captured non-sensitive fixture rather than production events.
If a newline change has merged records, split them only when there is an unambiguous delimiter and preserve the original artifact for audit. If it created unintended extra records, pause the consumer before replay, deduplicate using durable event IDs, and restore the previous producer only if it will not repeat side effects. Avoid “repair” commands copied from incident data until their scope is independently verified.
Attribution and verification
This guide was prompted by Sergey's Stack Overflow question and sth's accepted answer, used under CC BY-SA. The preference for printf remains current. It was independently verified against the current GNU Bash builtin documentation, including echo, printf, and %b, and the GNU Coreutils echo guidance.
Primary source: Review the official reference ↗