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?
answer
- two jobs, one switch
- escape now or inject around
- backslash-dollar in an unquoted body
- literal delimiter plus generated assignment lines
basics
~20 sEither 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.
solid answer
~50 sA here-doc gives you one switch for the whole body, so mixed content forces a choice. With `<<EOF` everything expands and you must escape what should not: the wrapper's own `"$@"` becomes `"\$@"`, and every `$`, backtick and backslash in the body needs the same treatment. That works, but it scales badly — the file you read no longer looks like the file you generate, and one missing backslash silently interpolates a value at build time instead of run time. My preference is to keep the body literal with `<<'EOF'` and inject the few dynamic values around it: emit `APP_TAG=$(printf '%q' "$IMAGE_TAG")` as its own line before the literal block, or run the block through `envsubst` with an explicit variable list so only the names you nominate are replaced. Remember the shebang and `chmod +x`, and never assemble the body from values you do not control.
code
bash · 10 linesIMAGE_TAG=v3.1
{
printf '#!/usr/bin/env bash\n'
printf 'APP_TAG=%q\n' "$IMAGE_TAG"
cat <<'EOF'
exec /opt/app/bin/app --tag "$APP_TAG" "$@"
EOF
} > /tmp/run
chmod +x /tmp/run
cat /tmp/rungo deeper
Know that a here-doc expands either everything or nothing depending on whether the delimiter is quoted, and that \$ keeps a dollar sign literal in the unquoted form.
Explain that quotes inside the body are not special, so \$ under <<EOF is the only way to keep a runtime variable literal, and that a quoted delimiter passes even backslashes through untouched.
Argue for keeping the generated body literal and injecting values around it, using printf %q for safe quoting, and cover the operational details: shebang, chmod +x, atomic rename, and identical output on every run.
Own the question of whether a script should be generating executables at all. Weigh a literal here-doc against shipping a versioned file plus a config, and set the team rule for how build-time values reach runtime — arguments and environment, not string interpolation.
## Why this is a real problem and not a puzzle Generating a script or a config file from a here-doc is one of the most common things entrypoints, installers and provisioning scripts do. It is also where the here-doc's single expansion switch stops fitting the job, because the generated file is *itself* shell — full of `$1`, `"$@"`, `${VAR}` and backticks that must survive into the output — while a handful of build-time values must be substituted now. ## The switch, restated The delimiter word decides everything. `<<EOF` expands the whole body at generation time; `<<'EOF'` expands none of it. There is no per-line or per-token control, and — the trap that catches people — **quotes inside the body do nothing**, because the body is never re-parsed as shell words. Writing `'$@'` inside an unquoted body does not protect it. ## Approach 1: expand everything, escape the literals ```bash cat > /usr/local/bin/run <<EOF #!/usr/bin/env bash exec /opt/app/bin/app --tag "$IMAGE_TAG" "\$@" EOF ``` Here `$IMAGE_TAG` is substituted now and `\$@` is emitted as `$@` for the wrapper to expand later. Inside an unquoted body the only escapes bash honours are `\$`, `` \` ``, `\\` and backslash-newline, so those are the four things to hunt for. This is correct and worth knowing, but its failure mode is nasty. The escape is invisible in review, and forgetting one does not produce an error — it produces a wrapper that silently forwards nothing, or a config that hard-codes the build machine's hostname. Worse, the source you read and the artefact you ship diverge textually, so grepping the repo for a line in the generated file finds a backslashed variant. It also does not compose: the moment the wrapper itself needs a nested here-doc you are escaping escapes. ## Approach 2: literal body, injected values Keep the body exactly as it will appear on disk and supply the dynamic parts around it: ```bash { printf '#!/usr/bin/env bash\n' printf 'APP_TAG=%q\n' "$IMAGE_TAG" cat <<'EOF' exec /opt/app/bin/app --tag "$APP_TAG" "$@" EOF } > /usr/local/bin/run chmod +x /usr/local/bin/run ``` Three things earn their place here. `printf '%q'` (a bash builtin behaviour) emits the value quoted so that it re-reads as the same single word — which matters if the tag ever contains a space, a quote or a `$`. The braces group the three writers so the file is opened once rather than truncated and re-appended. And the literal block is now byte-identical to what ships, so it reviews, greps and lints like the file it is. ## Approach 3: a template and an explicit substitution list When the file is mostly text with named placeholders, `envsubst` (from GNU gettext) reads stdin and replaces `$VAR` and `${VAR}` references from the environment. Crucially it accepts a *shell-format* argument that restricts which names it touches: ```bash export IMAGE_TAG envsubst '${IMAGE_TAG}' <<'EOF' > /etc/app.conf tag = ${IMAGE_TAG} log_format = $remote_addr $request_time EOF ``` Without that argument `envsubst` would substitute every `$NAME` it recognises, including the `$remote_addr` that belongs to the downstream config language. Passing the list is what makes the technique safe rather than merely convenient. Note that `envsubst` is not present in every minimal image — check before relying on it in a container. ## Choosing - One or two escapes in a short body: approach 1 is fine, and everyone can read it. - A generated *script*, or anything with more than a couple of literals: approach 2. The literal body is the artefact. - A text config with named placeholders, and gettext available: approach 3. ## Things that bite regardless of approach **Executability.** `cat > file` creates a plain file with the current umask; a generated script needs `chmod +x`, and a `#!` line at absolute position zero. **Atomicity.** Writing directly to the final path leaves a truncated file if the script dies mid-write, and a concurrent reader can observe it. Write to a temporary name in the same directory and rename it into place. **Untrusted values.** Interpolating a value you did not control into a body that will later be executed is command injection with extra steps; `printf '%q'` helps with shell quoting, but the safe handling of untrusted input is its own subject and deserves the fuller treatment there. **Idempotence.** An entrypoint that regenerates the file on every start should produce byte-identical output for identical inputs, so that restart loops and config-drift detectors do not fire. ## The sentence that answers the question One here-doc, one expansion mode: either escape the literals under an unquoted delimiter, or — better for anything you will maintain — keep the body literal under `<<'EOF'` and generate the dynamic lines separately, so the block in the source reads exactly like the file on disk.
- Why not simply backslash-escape every dollar sign in the body and move on?Because it is unreviewable at scale. The escapes are invisible in a diff, a single omission silently substitutes a build-time value instead of failing, and the source text no longer matches the generated artefact, so grepping for a line in the shipped file misses it. It also stops composing as soon as the generated script needs its own here-doc.
- What does `printf '%q'` add when you emit a generated assignment line?It prints the value in a form that bash re-reads as the same single word, quoting or backslashing spaces, quotes, newlines and `$`. Writing `printf 'APP_TAG=%q\n' "$IMAGE_TAG"` therefore produces a line that is safe to source even when the value is awkward, whereas a bare `APP_TAG=$IMAGE_TAG` line breaks on the first space.
- Why must `envsubst` be given an explicit variable list?Because with no argument it replaces every `$NAME` and `${NAME}` reference it finds from the environment, which will eat placeholders that belong to the target config language rather than to you. Passing a shell-format argument such as `'${IMAGE_TAG}'` limits substitution to the names you nominate and leaves the rest untouched.
- What else does a generated wrapper script need beyond correct content?A `#!` line at the very start, `chmod +x` since `cat >` respects the umask and creates a non-executable file, and ideally an atomic write — generate to a temporary name in the same directory and rename it into place, so a crash mid-write cannot leave a truncated executable for the next process to run.
saying these in an interview costs you the question
- Escapes dollars ad hoc and hopes none were missed
- Thinks single quotes inside the body block expansion
- Expects backslash escapes to work under a quoted delimiter
- Forgets chmod +x on the generated script
- Concatenates untrusted values straight into the body