A Bash here-string supplies the expanded value of one word to a command’s standard input, with a newline appended. It is convenient when a small textual value already exists in a variable and the receiving tool reads stdin. The syntax removes the need for a separate printing command, but it does not promise byte-for-byte identity with the variable or a particular implementation using memory, a pipe, or a file.
Expansion happens before the input is read
The word after the triple-less-than operator can contain parameter expansion, arithmetic expansion, or command substitution. It is therefore not necessarily a literal constant. Single quotes preserve literal source text, while double quotes permit the usual variable and command expansions. Text obtained by expanding a variable is not recursively executed as shell code merely because it contains a dollar sign and parentheses.
In current Bash, a here-string’s word does not undergo word splitting or filename expansion. It is delivered as a single string. Quoting a variable in this position still makes the intended boundary easy to read, but do not confuse this redirection context with passing an unquoted variable as a normal command argument. The latter can split the value and expand wildcard characters into filenames.
The appended newline is unconditional. A value without a final newline gains one; a value already ending in a newline gains another. An empty variable consequently produces a one-byte input containing a newline, rather than immediate end-of-file. This makes here-strings natural for many line-oriented tools and unsuitable when the receiving protocol requires an exact length or a specific final byte.
When it breaks
Read applies its own interpretation after the shell constructs the input. By default, its field splitting can remove surrounding IFS whitespace, and backslashes can act as escapes. For one line whose spaces and backslashes must be retained, use IFS= read -r name. This protects the line’s content, but a single read still stops at the first newline. It does not capture an arbitrary multiline value into that variable.
A redirected builtin or loop can run in the current shell, so its assignments remain visible. That is useful compared with a loop placed in a pipeline, where Bash normally runs the stage in a subshell. The input redirection itself does not create a subshell around the command. An explicit subshell or command substitution elsewhere in the expression still has its usual scope and effects.
The absence of an explicit filename in the script does not establish how Bash stores the redirection internally. Avoid code that depends on stdin being seekable, on a fixed buffering capacity, or on there being no temporary backing storage. For large existing files, direct file redirection avoids first collecting the whole input in a shell variable. For exact textual bytes, a carefully formatted printf or a file can avoid the added newline.
Here-strings are also a shell-language feature, not portable syntax for every interpreter called sh. A script using them should select an interpreter that supports them, and tests should invoke that interpreter explicitly. Conversely, changing an echo pipeline to a here-string can alter both newline behavior and variable scope. Compare the consumer’s observed bytes and state changes rather than treating the forms as interchangeable typography.
Check it quickly
This complete fixture needs Bash and Python 3. Python displays stdin as hexadecimal so appended newline bytes remain visible; capturing that hexadecimal output does not lose the input bytes being inspected. The remaining checks use read and a redirected loop to test whitespace preservation, literal text, and multiline consumption. No fixture files are required.
bash --noprofile --norc <<'BASH'
set -e
hex_input() {
python3 -c 'import sys; print(sys.stdin.buffer.read().hex())'
}
[[ $(hex_input <<< X) == 580a ]]
[[ $(hex_input <<< '') == 0a ]]
[[ $(hex_input <<< $'X\n') == 580a0a ]]
[[ $(printf '%s' X | hex_input) == 58 ]]
value=' a\b *.txt '
IFS= read -r received <<< "$value"
[[ $received == "$value" ]]
literal='$(printf injected)'
IFS= read -r received <<< "$literal"
[[ $received == '$(printf injected)' ]]
value=$'one\ntwo'
IFS= read -r first <<< "$value"
[[ $first == one ]]
lines=0
while IFS= read -r line; do
lines=$((lines + 1))
done <<< "$value"
[[ $lines -eq 2 ]]
printf 'PASS: newline bytes, preserved text, single read, loop scope\n'
BASH
Expect only the PASS line. The hexadecimal assertions distinguish one newline from two and compare a here-string with printf that adds none. The literal command-looking text must remain unchanged, and the loop count must survive in the outer shell. These results test the documented input behavior, not the internal storage mechanism. Before using the pattern with another reader, check how that reader handles empty lines, multiple records, and trailing whitespace of its own.
Sources
- GNU Bash: Redirections, here-string expansion and appended newline, checked 2026-09-06.
- GNU Bash: Bash Builtins, read delimiters and raw-input mode, checked 2026-09-06.
- GNU Bash: Command Substitution, captured output and trailing-newline removal, checked 2026-09-06.