skip to content

In Ruby, how do <<~, <<- and plain << heredocs differ, and what does quoting the identifier change?

level: middleimportance: must knowfreq 52%

answer

  1. where the terminator may sit
  2. <<~ strips the least indentation
  3. <<- indents only the terminator
  4. 'EOS' disables interpolation
  5. method call goes on the opener

basics

~20 s

Plain <<EOS needs its terminator at column 0 and keeps text as written; <<-EOS lets the terminator be indented; <<~EOS also removes the smallest common indentation from the body. Quoting the identifier as 'EOS' turns off interpolation and escapes.

solid answer

~40 s

A **heredoc** is a multi-line string literal that starts on the line after its opener and ends at a line holding only the identifier; the result includes the final newline. With plain `<<EOS`, the terminator must start at column 0 and the body keeps every space. `<<-EOS` only lets the **terminator** be indented; body indentation still ends up in the string. `<<~EOS`, the **squiggly** heredoc, also strips the indentation of the least-indented non-blank line from every line, so the body can be indented with the code. By default a heredoc behaves like a double-quoted string; `<<~'EOS'` makes it single-quoted (no `#{}` and no escapes), and backticks run it as a shell command. Methods chain on the opener: `<<~SQL.chomp`.

code

ruby · 15 lines
ruby
def body
  a = <<-DASH
    Latte
  DASH
  b = <<~SQUIGGLY
    Latte
      oat milk
  SQUIGGLY
  c = <<~'RAW'
    #{not_interpolated}\n
  RAW
  [a, b, c]
end
body
# => ["    Latte\n", "Latte\n  oat milk\n", "\#{not_interpolated}\\n\n"]

go deeper

for a junior

Recall the three openers and that a heredoc ends with a newline. Know that <<~ is the one to use inside indented code.

for a middle

Explain how <<~ measures and removes indentation, including blank lines, and how quoting the identifier switches between double-quoted, single-quoted and command bodies.

for a senior

Anticipate the production traps: SQL or templates with unexpected leading spaces from <<-, missing .chomp in comparisons, and misaligned multi-line interpolated values.

for a principal

Set conventions for large embedded text: when a heredoc is fine, when the text belongs in a template file, and how linters keep indentation consistent.

## What a heredoc is A **here document** (heredoc) is a string literal for multi-line text. It opens with `<<` and an identifier, the body starts on the **next line**, and it ends at the first line that contains only the identifier. The resulting string includes the newline of the last body line. ```ruby banner = <<EOS BEAN THERE COFFEE EOS banner # => "BEAN THERE COFFEE\n" ``` Uppercase identifiers such as `EOS`, `SQL` or `RECEIPT` are conventional, and naming them after the content helps editors highlight the body. ## Three openers | Opener | Terminator may be indented | Body indentation kept | |---|---|---| | `<<EOS` | no, must start at column 0 | yes, exactly as written | | `<<-EOS` | yes | yes, exactly as written | | `<<~EOS` | yes | no, common indentation removed | - **`<<EOS`** is the original form. Inside an indented method, the terminator at column 0 breaks the visual structure of the code. - **`<<-EOS`** fixes only the terminator. If you indent the body to match the code, those spaces become part of the string. - **`<<~EOS`** (the squiggly heredoc) measures the indentation of the least-indented line and removes that much from every line. Blank lines and lines made only of spaces or tabs are ignored when measuring; escaped spaces and tabs count as content. ## The squiggly heredoc in a receipt ```ruby def receipt_footer(total) <<~FOOTER ------------------------------ TOTAL #{format("%24.2f", total)} Thank you, come again! FOOTER end ``` Four spaces are the smallest indentation, so they are removed from each line; the thank-you line keeps its extra two spaces. The terminator sits at the method's own indentation. Tabs are measured as advancing to the next multiple of eight columns; mixing tabs and spaces in a squiggly body is legal but hard to reason about, so most style guides forbid it. ## Quoting the identifier The quotes around the identifier choose the literal type of the body: 1. **Unquoted** or **double-quoted** (`<<~EOS`, `<<~"EOS"`): like a double-quoted string, with `#{}` interpolation and escapes such as `\n` and `\t`. 2. **Single-quoted** (`<<~'EOS'`): like a single-quoted string. `#{...}` and backslashes stay as typed, which suits embedded code samples, shell scripts or templates rendered later. 3. **Backticks** (`` <<~`CMD` ``): the body is run as a shell command, like `Kernel#` `` ` ``, and the heredoc evaluates to its output. ## Calling methods and passing heredocs Anything after the opener on the same line is ordinary code, so methods apply to the whole heredoc: - `<<~SQL.chomp` drops the final newline. - `<<~TEXT.lines.map(&:strip)` post-processes each line. - `puts(<<~ONE, <<~TWO)` opens two heredocs on one line; their bodies follow in order. Legal, but hard to read. ## When a heredoc is the right tool Ruby offers several ways to write multi-line text, and each has a sweet spot: - A **double-quoted string** can simply span lines, but its continuation lines must start at column 0 or their indentation becomes content. - **Adjacent literals** joined with a trailing backslash suit two or three long lines without newlines between them. - **`Array#join`** on a list of lines suits text assembled conditionally, such as a receipt that prints a discount line only when a discount applies. - A **squiggly heredoc** suits fixed text with a few interpolated values: SQL, email bodies, help output and receipt templates. - A **separate template file** suits long or translated text that non-developers edit. ## Common mistakes - Using `<<-` and expecting the body to be de-indented; only `<<~` does that. - Forgetting that the result ends with `"\n"`, then comparing it with a string that does not. - Interpolating a multi-line value into `<<~` and expecting it to be re-indented; the de-indentation is computed from the literal source lines, and inserted text is used as it is. - Writing the terminator with trailing text; the terminator line must hold only the identifier (plus leading whitespace for `<<-` and `<<~`).

  • How do you remove the trailing newline a heredoc always ends with?
    Call a method on the opener, for example `<<~MSG.chomp`. The code after the identifier on the opening line applies to the complete heredoc string, so `.chomp` removes the final `"\n"`.
  • In a <<~ heredoc, does a blank line with no spaces reduce the indentation that gets stripped?
    No. Blank lines, and lines made only of literal spaces or tabs, are ignored when Ruby measures the least indentation. Escaped spaces or tabs such as `\t` do count as content.
  • Why might a multi-line value interpolated into a <<~ heredoc look misaligned?
    The common indentation is computed from the literal lines in the source. A value inserted with `#{}` is used as it is, so its second and later lines carry only their own indentation, not the heredoc's.

A squiggly heredoc is like photocopying an indented block of text after sliding the page left until the least-indented line touches the margin: relative indentation survives, the common margin does not.

saying these in an interview costs you the question

  • <<- strips the leading indentation from every body line
  • A heredoc result never ends with a newline
  • <<~'EOS' still interpolates #{} but skips escapes
  • A blank line with no spaces makes <<~ strip nothing
  • A method such as .chomp must go after the closing identifier