How to Preserve Trailing Newlines in Bash Command Output
Why Does Bash Strip Trailing Newlines?
If you have ever captured command output in Bash using command substitution like VAR="$(command)", you might have noticed that any trailing newline characters (\n) mysteriously disappear. This isn't a bug—it is an intentional design choice mandated by the POSIX standard. Bash automatically removes all trailing newlines resulting from command substitutions.
While this behavior is convenient when dealing with utility commands that append an extra newline (like echo or pwd), it becomes problematic when you need exact byte-for-byte fidelity—such as when handling binary streams, formatting files, or capturing HTTP response bodies via curl.
The Sentinel Character Method (Recommended & POSIX-Compliant)
The standard, most reliable way to preserve trailing newlines across all POSIX-compatible shells is the sentinel character idiom.
By appending a non-newline character (often x or _) to the output inside the subshell, you prevent Bash from stripping the preceding newlines. Afterward, you simply strip off that single sentinel character using parameter expansion.
# 1. Append a sentinel character ('x') inside the command substitution
VAR="$(printf 'abc\n\n'; printf 'x')"
# 2. Strip the trailing 'x' using parameter expansion
VAR="${VAR%x}"
# Verify with xxd
printf "%s" "$VAR" | xxd
Output:
00000000: 6162 630a 0a abc..
Why does this work?
Command substitution only strips trailing newlines at the very end of the captured string. Because the output now ends with x instead of \n, Bash leaves all newlines intact. The ${VAR%x} expansion then removes only the final x in constant time.
Alternative: Using read and Process Substitution (Bash Only)
If you are writing scripts strictly for Bash (version 4.0+) and want to avoid the two-step assignment, you can read the command output directly into a variable using a null delimiter (-d ''):
# Capture command output preserving all newlines
IFS= read -r -d '' VAR < <(printf 'abc\n\n' && printf '\0')
# Inspect output
printf "%s" "$VAR" | xxd
How this works:
-d ''tellsreadto use a null byte (\0) as the line delimiter instead of a newline.IFS=ensures that leading and trailing whitespace are not trimmed.-rprevents backslash escapes from being interpreted.< <(...)creates a process substitution and streams the data directly toread.
Crucial Tip: Always Quote Your Variables
Even if you capture trailing newlines correctly, failing to double-quote your variable when referencing it will cause Bash's word-splitting mechanism to swallow whitespace and newlines:
# Incorrect (Word splitting will collapse/strip newlines):
printf %s $VAR | xxd
# Correct (Exact representation preserved):
printf "%s" "$VAR" | xxd
Summary
- Standard command substitution
$(...)will always trim trailing newlines. - Use
VAR="$(cmd; printf 'x')"; VAR="${VAR%x}"for maximum compatibility across Bash, Zsh, and POSIXsh. - Always wrap references in double quotes (
"$VAR") to preserve spaces and newlines during expansion.