Wiki

Here-strings break when the doc indents them

The boundary between a doc and the code it embeds has rules the doc never mentions. Markdown indents code blocks inside lists. YAML indents multi-line strings under keys. JSON schemas wrap shell snippets in escaped strings. The code that gets pasted out the other side carries the host's indent on every line. Most code-as-string constructs survive that fine. A few of them, the ones the language's parser anchors to column 0, break silently the moment the host adds any indent. PowerShell here-strings are the loudest case I've hit. The rule generalizes.

When to reach for it

Any time you author a code snippet inside a host doc and the snippet itself contains a multi-line string literal: a PS here-string @"..."@, a bash heredoc <<EOF, a Ruby heredoc <<EOS, a Python triple-quoted block intended for exec(), a SQL $tag$...$tag$ dollar-quoted literal. If the host doc is going to wrap your snippet in a numbered list, a YAML block scalar, or a JSON-escaped string, the snippet ships with leading whitespace on every line. Know whether your literal's closing marker requires column 0 before you write it.

The failure shape

I hit this on commands/setup.md while shipping jarrodwatts/claude-hud#538. The setup command reads a markdown file and runs the PowerShell snippet inside Step 4. The snippet wrote statusline.ps1 to disk via:

$wrapperBody = @"
   try { $w = [Console]::WindowWidth } catch { $w = 120 }
   ...
   "@

Inside a numbered list, every line of a fenced code block is indented three spaces in the markdown source. The closing "@ ended up at column 3, not column 0. PowerShell here-strings require the closing "@ to start at column 0 — RFC-equivalent clear in the language spec, but the failure mode is silent: the parser drops the trailing "@ from the string body entirely, treats everything from @" to end-of-file as one string, and the rest of the script gets eaten. No syntax error. The script "runs" but produces nothing.

The fix wasn't to dedent the markdown. It was to pick a different string construct for the same job.

The two forms

Most languages with multi-line code-as-string offer two camps of constructs, and they handle host indent differently.

Column-sensitive constructs

The literal's start and end markers are anchored to column 0 of the source line. Indent that, the marker disappears.

Column-insensitive constructs

The literal's body is wrapped in a construct the parser treats as a syntactic unit, not a column-anchored marker. You can paste the whole thing at any indent.

The shape across languages is: for each pair, the parser of the column-insensitive form treats the body as opaque content delimited by syntactic brackets, not by column-anchored sentinels. Brace balance, parenthesis balance, leading-indent matching — all of these survive arbitrary host indent. Sentinel strings on bare lines don't.

The technique

When the host doc forces an indent on your code, use the column-insensitive form and convert to string at the seam.

In PowerShell, replace @"..."@ with ({ ... }.ToString().Trim()). Embedded $var references stay as text because the block body never evaluates during .ToString(). If you need to inject a value into the string, use a placeholder token and -replace afterward — embedding a {}-bracket value inside the script block confuses the parser:

$body = ({
    Write-Host "value is __VALUE__"
}.ToString().Trim()) -replace '__VALUE__', $someValue

In bash, replace <<EOF with printf or with the squiggly heredoc <<~EOF if the host is bash 4+. Best of all, write a real .sh file to disk and source it; the question goes away.

In Python, write the snippet triple-quoted at any indent, then textwrap.dedent() before exec(). Or use a .py file loaded with runpy.run_path().

The deeper move, when the host is something like markdown docs or a JSON config field that ships shell snippets, is to stop embedding code-as-string entirely. Write the snippet to a real script file at install time and have the host config point at the file. The setup.md change I shipped does this for the PS wrapper, and the file-on-disk form is fully indent-immune by construction (the file's lines have whatever column the file has, with no host doc in the way).

When this doesn't help

Diagnosis

The failure is silent in the bad cases. There is no error message that says "your closing marker was at the wrong column." The script just behaves as if the marker is missing. Three quick checks when an embedded snippet does nothing visible:

  1. Eyeball the closing marker's column. Paste the snippet into a plain text editor. If the host indented it, the marker is on column N, not column 0.
  2. Run a parser-only check. Most languages have a parse-without-execute mode: PowerShell's [System.Management.Automation.Language.Parser]::ParseInput, python -c "compile(open('f').read(), 'f', 'exec')", bash -n script.sh. A clean parse against the indent-bearing source proves the construct is column-insensitive.
  3. Print the literal's length. A here-string whose closing marker the parser dropped is much longer than intended (often eats to end-of-file). len(s) versus expected size catches the silent-failure case.

Real applications

Related

Revisit

When the next column-anchored construct bites me, fold the example back in here. Specifically watch for: