skip to content

Here-Documents and Here-Strings

Here-docs inline a block of text as a command's stdin, and quoting the delimiter decides whether the shell expands variables inside it — the difference between a working remote script and one that expanded on the wrong machine. Here-strings are the one-line version.

part ofBashoverview, primer and where to startread it →
on this pageshow

questions

4

In a bash script, `ssh web01 <<EOF` ... `EOF` sends a block of commands to a remote host as that command's standard input. What changes if you write the opener as `ssh web01 <<'EOF'` instead, and which form do you want when the block references variables that exist only on the remote machine?

level: middleimportance: must knowfreq 72%

answer

  1. who expands it, local or remote
  2. the switch lives on the delimiter
  3. quotes inside the body do nothing
  4. quoted delimiter means literal body

basics

~20 s

Quoting the here-document delimiter (<<'EOF') sends the body through untouched; leaving it unquoted (<<EOF) makes the local shell expand $variables, $(...) and backticks in the body first. Use the quoted form for anything that must be evaluated on the remote host.

solid answer

~50 s

A here-document's delimiter word carries the switch. With `<<EOF` the body is processed by the **local** shell almost as if it were inside double quotes: `$var`, `${var}`, `$(cmd)`, backticks and `$((...))` are all expanded before a single byte reaches `ssh`, and only `\$`, `` \` ``, `\\` and backslash-newline escape that. If any part of the delimiter is quoted — `<<'EOF'`, `<<"EOF"` or `<<\EOF` — expansion is switched off entirely and the remote shell sees the literal text. So a block that says `echo $HOSTNAME` under `<<EOF` prints the *local* hostname on the remote box, which is exactly the bug people hit. My default is the quoted form, because a remote or generated script should be data, not a template; when I genuinely need a local value inside a literal body I pass it as an argument with `ssh host bash -s -- "$tag"` rather than interpolating it.

code

bash · 7 lines
bash
NAME=local
cat <<EOF
expanded: $NAME on $(uname -s)
EOF
cat <<'EOF'
literal: $NAME on $(uname -s)
EOF

go deeper

for a junior

Be able to write a here-doc that feeds text to a command and to say that quoting the delimiter, as in <<'EOF', stops the shell from substituting variables in the body.

for a middle

Explain precisely which expansions run on an unquoted body — parameter, command and arithmetic substitution — and that backslash is the only escape available there. Name the awk or ssh case where local expansion produces the wrong text.

for a senior

Show the judgment: default to a quoted delimiter, pass values in as positional parameters via bash -s rather than string-interpolating them, and recognise the same split in sudo, docker exec and SQL clients reading stdin.

for a principal

Own the position that a remote or generated script is data, not a template, and that escape-soup bodies are unreviewable. Argue when a heredoc-over-ssh deploy should be replaced by shipping a versioned script or a real configuration-management tool.

## What a here-document is A here-document is a redirection, not a command. Writing ```bash command <<DELIM line one line two DELIM ``` tells the shell to collect every line up to a line consisting of exactly `DELIM` and supply those lines as `command`'s standard input. The command never knows the difference between this and `command < some-file`. Because it is a redirection it can be combined with others (`command <<DELIM >out.log 2>&1`), and it can appear anywhere a redirection can — on a function's body, on a loop, on a compound command. The closing line must be the delimiter alone: no leading spaces (unless you used `<<-`, which strips leading **tabs** only), no trailing spaces, no comment. If bash never finds the delimiter it reads to end of file and warns `warning: here-document at line N delimited by end-of-file (wanted 'EOF')` — and the rest of your script has silently become here-doc data. ## The delimiter word decides expansion This is the whole question. Bash looks at the delimiter word you wrote: - **Unquoted** (`<<EOF`): the body is expanded by the shell that reads it. Parameter expansion (`$var`, `${var:-x}`), command substitution (`$(cmd)` and backticks) and arithmetic expansion (`$((1+1))`) all happen. Backslash escapes `$`, `` ` ``, `\` and a newline (line continuation). Filename globbing and word splitting are *not* performed — the body is delivered as text. - **Quoted in any way** (`<<'EOF'`, `<<"EOF"`, `<<\EOF`): the delimiter is the word after quote removal, and the body is passed through completely literally. Not even backslash is special, so a `\$HOME` you wrote inside a quoted body arrives as the two characters `\$` followed by `HOME`. The trap that catches experienced people: **quotes inside the body are not special.** The body is not re-parsed as shell words, so wrapping something in single quotes does nothing to protect it. That is why this classic breaks: ```bash ssh web01 <<EOF ps aux | awk '{print $1}' EOF ``` The local shell expands `$1` — which in a script is the script's own first positional parameter, usually empty — and the remote host receives `awk '{print }'`. Quote the delimiter and it works. ## Why ssh makes it visible `ssh host` with no command runs the remote user's shell, which reads and executes whatever arrives on stdin. So there are two shells in the picture, and the delimiter decides which one evaluates the text: ```bash ssh web01 <<EOF echo "$(hostname -s)" # your laptop's name, computed locally EOF ssh web01 <<'EOF' echo "$(hostname -s)" # web01, computed remotely EOF ``` The same split appears with `sudo bash`, with `docker exec -i ctr bash`, with `psql` or `mysql` reading SQL from stdin (an unquoted body will happily expand a `$$`-quoted PostgreSQL function body into your shell's PID), and with `cat > file <<EOF` generating a config or a script. ## Selective expansion When you want *mostly* literal with a couple of local values, you have three honest options: 1. Unquoted delimiter, and backslash-escape everything that must stay literal: `\$1`, `` \`date\` ``. Correct, but one missed escape is a silent bug and nobody reviews it reliably. 2. Quoted delimiter, and pass values as arguments: `ssh host bash -s -- "$tag" "$env" <<'EOF'`. The remote `bash -s` reads the literal script from stdin and takes the words after `--` as `$1`, `$2`. Values cross as arguments, so spaces and quotes survive. 3. Quoted delimiter, and generate the assignments separately (for a file you are writing) — emit a `TAG=...` line, then `cat <<'EOF'` the literal remainder. Option 1 is the one interviewers expect you to *know*; options 2 and 3 are the ones that show you have maintained such a script. ## Here-strings `command <<< "$word"` is the one-line cousin: the expanded word plus a trailing newline becomes stdin. Note that its content goes through normal shell quoting rules at the point you write it, so `<<< '$HOME'` is literal and `<<< "$HOME"` is expanded — the opposite-looking convention from here-docs, where the quotes live on the delimiter rather than on the data. ## Rules of thumb Default to `<<'EOF'` and opt into expansion deliberately. Never build the body by concatenating values you do not control — that is command injection into a remote shell, and the safe patterns for it are their own topic. And say out loud which shell expands what; that sentence is what the interviewer is listening for.

  • How do you get a local value into a body you deliberately kept literal?
    Pass it as an argument instead of interpolating it: `ssh host bash -s -- "$tag" <<'EOF'`. The remote bash reads the literal script from standard input and the words after `--` become its positional parameters, so the body refers to `$1`. Values containing spaces or quotes survive intact, and nothing in the body is evaluated locally.
  • Why does an awk one-liner inside an unquoted here-doc stop working even though the program is in single quotes?
    Because quotes inside a here-doc body are not special — the body is never re-parsed as shell words. Expansion runs over the raw text first, so `'{print $1}'` becomes `'{print }'` when `$1` is unset. Quote the delimiter, or escape it as `\$1`.
  • Does `<<"EOF"` behave like the quoted or the unquoted form?
    Like the quoted form. If any part of the delimiter word is quoted, bash removes the quotes to get the delimiter and disables expansion of the body. `<<'EOF'`, `<<"EOF"` and `<<\EOF` are all equivalent in that respect; only a wholly unquoted word enables expansion.

A here-doc is a letter. An unquoted delimiter lets your local post office fill in every blank before it seals the envelope; a quoted delimiter seals it as written, so the recipient reads the blanks with their own values.

saying these in an interview costs you the question

  • Assumes the remote shell expands variables in an unquoted here-doc
  • Thinks single quotes inside the body prevent expansion
  • Calls <<'EOF' versus <<EOF a matter of style
  • Writes \$var inside a quoted delimiter and expects it to expand
  • Interpolates untrusted values straight into a remote command block

context

open as a page

In bash, the here-string operator appears in commands such as `grep -q error <<< "$line"`. What exactly does `<<<` feed to the command's standard input, and how does that data differ from the string held in the variable?

level: juniorimportance: should knowfreq 50%

basics

~20 s

A here-string expands the word after <<< and supplies it as the command's standard input with a single newline appended. Bash performs parameter and command substitution on the word but no word splitting or globbing, so an unquoted variable still arrives as one intact string.

open as a page

A container entrypoint script generates a wrapper script with `cat > /usr/local/bin/run <<EOF` ... `EOF`. The generated wrapper must keep `"$@"` literal so it forwards its own arguments at runtime, but must bake in the value of `$IMAGE_TAG` from the entrypoint's environment. How do you get both behaviours out of one here-document, and which approach survives review?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Either use an unquoted delimiter and backslash-escape every literal, writing "$@", or use a quoted delimiter for a fully literal body and emit the baked-in values as separate generated assignment lines. The second survives review, because one missed backslash in the first is a silent bug.

open as a page

A bash script has a here-document nested inside an indented function, opened with `<<-EOF` and closed with an indented `EOF`. Bash still warns `here-document at line N delimited by end-of-file (wanted 'EOF')`. What does the `<<-` operator actually strip, and what must the closing delimiter line contain?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

The dash in <<- strips leading TAB characters only — never spaces — from both the body lines and the delimiter line. A closing line indented with spaces therefore never matches, so bash reads to end of file and swallows the rest of the script.

open as a page