Skip to content

Instantly share code, notes, and snippets.

@kwando
Created July 30, 2026 11:17
Show Gist options
  • Select an option

  • Save kwando/8ab73d1ee771f4bf2182e1e0926e5045 to your computer and use it in GitHub Desktop.

Select an option

Save kwando/8ab73d1ee771f4bf2182e1e0926e5045 to your computer and use it in GitHub Desktop.
Survey of multiline strings

Multiline Strings Across Programming Languages

A survey of how major programming languages handle multiline string literals, focusing on delimiters, whitespace trimming behavior, and design patterns.

Summary Table

Language Syntax Whitespace Trimming Trimming Strategy
Python """...""" No (preserve all) Use textwrap.dedent() manually
Python (PEP 822) d"""...""" Yes (automatic) d prefix for dedented strings; uses the textwrap.dedent() algorithm (Draft PEP)
JavaScript `...` No Preserve all; use tagged template or manual dedent
TypeScript `...` No Same as JavaScript
Go `...` No Raw literal; no interpolation, no trimming (but \r is discarded)
Zig \\... (per line) No Each \\ starts or continues a line; source indentation before the marker is outside the string
Rust "..." or r"..." No Preserve line breaks (CRLF normalized to LF); \ continuations remove following whitespace; use indoc! macro (external crate)
Java 15+ """...""" Yes (automatic) Strips incidental whitespace using the least indentation of content and closing-delimiter lines; also strips trailing whitespace
C# 11+ """...""" Yes (automatic) Closing """ position determines left margin
C# < 11 @"..." No Preserve all; no built-in trimming
Kotlin """...""" No (built-in) .trimIndent() / .trimMargin() stdlib functions
Swift 4+ """...""" Yes (automatic) Closing """ indentation defines left margin; no implicit trailing newline
Ruby <<~ID (squiggly) Yes (automatic) Strips minimum common whitespace from all lines
Ruby <<ID, <<-ID No Preserve all leading whitespace
Scala """...""" No (built-in) .stripMargin method with | margin markers
Elixir """...""" Yes (automatic) Indentation of last """ used to strip leading whitespace from all lines
Perl 5.26+ <<~"ID" Yes (automatic) Removes the exact indentation prefix before the closing delimiter
Perl < 5.26 <<"ID", <<'ID' No Preserve all; closing delimiter must be flush left
PHP 7.3+ <<<ID ... ID; Yes (automatic) Closing marker indentation determines stripping; body must not be less indented than marker, and must use the same whitespace kind
PHP < 7.3 <<<ID ... ID; No Closing marker must be flush left
Bash <<-EOF Partial (tabs only) <<- strips leading tabs, not spaces
Dart '''...''' or """...""" No Preserve all; use .trim() manually
GHC 9.12.1+ """...""" Yes (automatic) MultilineStrings extension; strips the longest whitespace prefix common to all lines except the first
Haskell without MultilineStrings String gaps or QuasiQuoters No Preserve all; use string gaps or packages such as neat-interpolation or raw-strings-qq

Language Details

Python

Python uses triple-quoted strings ("""...""" or '''...'''). No automatic whitespace trimming is performed -- the string contains exactly what you type, including leading indentation.

text = """
    line one
    line two
"""
# Result: "\n    line one\n    line two\n"

To strip common indentation, use textwrap.dedent() or inspect.cleandoc():

import textwrap
text = textwrap.dedent("""
    line one
    line two
""")

PEP 822 (Draft) proposes a d"""...""" syntax for dedented multiline strings that would strip indentation at the language level, using the same least-indented-line algorithm as textwrap.dedent(). This places it in the Java/Ruby camp rather than the Swift/C# camp, which derive the margin from the closing delimiter's position.

JavaScript / TypeScript

JavaScript uses backticks (`) for template literals. No automatic whitespace trimming is performed.

const text = `
    line one
    line two
`;
// Result: "\n    line one\n    line two\n"

No built-in dedent mechanism exists. Libraries like common-tags provide tagged template utilities, but there is no standard approach. The standard String.raw tag is unrelated to indentation -- it only suppresses escape sequence processing.

Go

Go uses backticks for raw string literals. No interpolation, no escape processing, and no whitespace trimming.

s := `line one
line two`
// Result: "line one\nline two"

Backticks cannot contain backtick characters. No interpolation is supported within raw literals.

One exception to the "verbatim" rule: the specification requires carriage return characters (\r) inside raw string literals to be discarded from the value, so raw literals in a CRLF-encoded source file still yield LF-only line breaks.

Zig

Zig uses multiline string literals with a \\ marker at the beginning of each content line. The marker starts or continues the literal; the physical line ending is omitted, but a newline is inserted when the following line also begins with \\. Multiline literals do not process escape sequences.

const text =
    \\line one
    \\line two
;
// Result: "line one\nline two"

Indentation before each \\ is source formatting, not string content. Any whitespace after the marker is preserved, so this syntax avoids incidental indentation without applying a common-whitespace trimming algorithm.

Rust

Rust double-quoted and raw string literals can span physical lines. Line breaks normally represent themselves, though a CRLF pair in the source is normalized to a single LF before the literal is processed. A backslash continuation escape strips the backslash, newline, and all immediately following whitespace (including further newlines). This is not intentional trimming, but a fundamental aspect of Rust's string literal syntax.

let s = "line one\n\
         line two";
// Result: "line one\nline two" (the indentation is omitted)

This behavior can be surprising for indentation stripping. For explicit indentation stripping, use the indoc! macro from the indoc crate:

use indoc::indoc;
let s = indoc! {"
    line one
    line two
"};

Java (Text Blocks, Java 15+)

Java introduced text blocks with """ delimiters. The compiler automatically strips "incidental" whitespace using its content and closing-delimiter lines.

String s = """
        line one
        line two
        """;
// Result: "line one\nline two\n"

The algorithm finds the minimum indentation across all non-blank content lines and the closing-delimiter line, then strips that amount from each line. Trailing whitespace is also stripped. The closing """ indentation therefore limits, but does not solely determine, the amount removed.

The trailing newline is not mandatory. It appears above only because the closing delimiter sits on its own line. Placing the closing """ at the end of the last content line omits it:

String s = """
        line one
        line two""";
// Result: "line one\nline two"

C# (Raw String Literals, C# 11+)

C# 11 introduced raw string literals using three or more double-quote characters. The closing delimiter's position determines the left margin.

var s = """
    line one
    line two
    """;
// Result: "line one\nline two"

Prior to C# 11, verbatim strings (@"...") preserved all whitespace without trimming.

Kotlin

Kotlin uses triple-quoted strings with no built-in trimming. Two stdlib methods provide trimming:

// trimIndent: removes common leading whitespace
val s = """
    line one
    line two
""".trimIndent()

// trimMargin: removes up to a margin character (default |)
val s = """
    |line one
    |line two
""".trimMargin()

trimIndent finds the minimum indent across non-blank lines and strips it; it also drops the first and last lines if they are blank, which is why the examples above do not begin or end with a newline. Because both are ordinary runtime functions operating on the interpolated result, an interpolated multi-line value can defeat the indent calculation. trimMargin strips everything before its margin character on lines that contain that margin prefix.

Swift (Multiline String Literals, Swift 4+)

Swift uses """ delimiters with automatic indentation stripping based on the closing delimiter's position.

let s = """
        line one
        line two
        """
// Result: "line one\nline two"

The whitespace before the closing """ defines how much leading whitespace to strip from all content lines. Both delimiters must be on their own lines, and a single newline is always stripped after the opening delimiter and before the closing one -- so unlike Java or Elixir, the value has no trailing newline unless you add a blank line. A backslash at end of line suppresses the newline character.

Ruby

Ruby provides multiple heredoc variants:

# Standard heredoc (no trimming)
s = <<ID
    line one
    line two
ID

# Squiggly heredoc (auto-trim, Ruby 2.3+)
s = <<~ID
    line one
    line two
ID

# With explicit trim
s = <<~ID.strip
    line one
    line two
ID

The <<~ (squiggly) form strips the minimum common leading whitespace from all lines. Blank lines and lines with only whitespace are ignored when calculating the minimum indent.

Scala

Scala uses triple-quoted strings with no automatic trimming. The .stripMargin method is the standard approach:

val s = """line one
          |line two
          |""".stripMargin
// Result: "line one\nline two\n"

The | character (or custom delimiter) marks the margin on each line. Everything before the margin character is stripped.

Elixir

Elixir uses triple-quoted strings with automatic indentation stripping based on the closing """ position.

s = """
    line one
    line two
    """
# Result: "line one\nline two\n"

The indentation of the closing """ determines how much leading whitespace to strip from each line, as in Swift, C#, and PHP. (Java differs: it uses the least-indented line, of which the closing delimiter's line is only one candidate.) Elixir heredocs always end with a newline.

Perl

Perl uses heredoc syntax with the ~ modifier for indentation stripping (introduced in Perl 5.26):

# With indentation stripping (5.26+)
my $s = <<~"ID";
    line one
    line two
    ID

# Without indentation stripping (closing delimiter must be flush left)
my $s = <<"ID";
    line one
    line two
ID

The <<~ form removes the exact whitespace prefix before the closing delimiter. Every non-empty body line must begin with that prefix, or Perl fails at compile time. Without ~, body lines may still be indented -- that whitespace simply becomes part of the string -- but the closing delimiter itself must start at column 0.

PHP

PHP uses heredoc/nowdoc syntax. PHP 7.3 introduced flexible indentation:

// PHP 7.3+ (with indentation stripping)
$s = <<<ID
    line one
    line two
    ID;

// PHP < 7.3 (no indentation stripping, must be flush left)
$s = <<<ID
line one
line two
ID;

Since PHP 7.3, the closing marker's indentation determines how much leading whitespace to strip. The body lines must not be less indented than the closing marker, or PHP raises a ParseError. Tabs and spaces are both accepted but must not be intermixed between the closing marker and the body's indentation prefix; doing so is also a ParseError.

Bash

Bash uses heredoc syntax with the - modifier for tab stripping:

# No stripping (content indentation is preserved)
cat <<EOF
line one
line two
EOF

# Tab stripping only (not spaces)
cat <<-EOF
	line one
	line two
EOF

The <<- form strips leading tabs only (not spaces). This is more limited than other languages.

Dart

Dart uses triple-quoted strings ('''...''' or """...""") with no automatic whitespace trimming.

var s = """
    line one
    line two
    """;
// Result: "    line one\n    line two\n    "

No built-in dedent. Use .trim() for leading/trailing whitespace removal.

Haskell

GHC gained multiline strings in version 9.12.1 via the MultilineStrings extension, with automatic indentation stripping:

{-# LANGUAGE MultilineStrings #-}
s = """
    line one
    line two
    """
-- Result: "line one\nline two\n"

The stripped indentation is the longest whitespace prefix shared by all lines except the first, ignoring whitespace-only lines -- a least-indented-line rule, not a closing-delimiter rule. The newline immediately after the opening delimiter is dropped, but the final newline is kept; use a string gap (\) at the end of the last content line to omit it. Use the \& escape to protect indentation that should survive stripping.

Without MultilineStrings, ordinary string literals can use string gaps, and packages can provide QuasiQuoters such as raw-strings-qq or neat-interpolation.

Trimming Strategies

Languages use four main strategies for handling whitespace in multiline strings:

1. No Trimming (Preserve All)

Languages: Python, JavaScript, TypeScript, Go, Dart, C# < 11, Scala, Ruby (<</<<-), Perl (<<), PHP < 7.3, Kotlin (built-in)

The string contains exactly what you type. Developer must handle indentation manually via library functions or string methods.

Pros: Simple semantics; what you see is what you get. Cons: Code indentation pollutes string content; common to need external dedent utilities.

2. Closing Delimiter Position

Languages: Swift, C# 11+, Elixir, PHP 7.3+, Perl (<<~)

The closing delimiter alone defines the left margin. In C#, PHP, and Perl a content line indented less than the delimiter is an error; Swift warns.

// C# example
var s = """
    line one
    line two
    """;
//  ^-- closing """ at column 4
// Result: "line one\nline two"

Pros: Elegant; code indentation and string content are independent. Cons: Confusing at first; closing delimiter position determines behavior; easy to miscount indentation.

3. Least-Indented Line Detection

Languages: Java 15+, Ruby (<<~), Kotlin (trimIndent), GHC MultilineStrings, Python PEP 822 (Draft)

The algorithm scans all non-blank lines, finds the one with minimum leading whitespace, and strips that amount from all lines. Java is a hybrid: the closing-delimiter line participates in the calculation as one more candidate, so moving that delimiter left adds indentation back to every line.

# Ruby example
s = <<~ID
      line one
    line two
ID
# Minimum indent is 4 spaces (from "line two")
# Result: "  line one\nline two\n"

Pros: More flexible; doesn't depend on closing delimiter position. Cons: Blank lines can complicate the calculation; slightly more complex to reason about.

4. Margin Marker-Based

Languages: Scala (stripMargin), Kotlin (trimMargin), GHC MultilineStrings (\&), Zig (\\)

Developer places a marker character (typically |) on each line to indicate where the margin is. Everything before the marker is stripped. Zig uses \\ as a required line prefix; indentation before the prefix is outside the literal, and consecutive prefixed lines insert a newline.

val s = """|line one
           |line two
           |""".stripMargin

Pros: Explicit control over each line; unambiguous. Cons: Requires marker characters on every line; more verbose.

Design Trends

  1. Newer languages automate trimming. Java (15), Swift (4), C# (11), Elixir, and GHC's MultilineStrings extension (9.12.1) make indentation stripping the default behavior. Python's PEP 822 (Draft) is following this trend.

  2. Two algorithms dominate, roughly evenly. Swift, C#, Elixir, PHP, and Perl derive the margin from the closing delimiter; Java, Ruby, Kotlin, GHC, and the PEP 822 draft use the least-indented line. Both decouple code indentation from string content without requiring explicit markers.

  3. Heredoc syntax (<<MARKER) remains popular in scripting languages (Ruby, PHP, Bash, Perl). The indentation modifier is not standardized across them: Ruby and Perl spell it <<~, Bash uses <<- for tabs only, and PHP infers it from the closing marker with no sigil at all.

  4. Most languages still provide escape hatches for cases where automatic trimming is unwanted -- Java allows the closing """ at column 0, Ruby has <</<<- variants, and Perl has ordinary <<EOF, <<'EOF', <<"EOF", and <<\EOF variants.

  5. Trailing whitespace handling varies. Java strips trailing whitespace on each line; most others preserve it. Whether the value ends in a newline also differs: Elixir always adds one, Java and C# depend on where the closing delimiter sits, and Swift never adds one. These are subtle differences that can cause bugs when porting between languages.

References

  • Python PEP 822: Dedented Multiline String (d-string)
  • Java JEP 378: Text Blocks
  • C# 11 Raw String Literals
  • Kotlin stdlib: trimIndent / trimMargin
  • Swift SE-0168: Multi-Line String Literals
  • Ruby Squiggly Heredocs (Ruby 2.3)
  • PHP RFC: Flexible Heredoc and Nowdoc Syntaxes
  • GHC MultilineStrings extension (GHC 9.12.1)
  • Go String Literals specification
  • Zig Language Reference: Multiline String Literals
  • Rust Reference: Literal expressions (string continuation escapes)
  • Elixir Syntax Reference: Heredocs
  • Perl 5.26 indented heredocs (<<~)
  • GNU Bash Reference Manual: Here Documents
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment