Skip to content

Raw strings, escapes and the literal prefixes

Level: 101 → 201 · working knowledge

One line: A handful of prefixes decide what the compiler does with the characters between your quotes — none, r, b, c, and the br / cr combinations — and the difference between "C:\temp\new" and r"C:\temp\new" is a tab and a newline you never typed.

let typed = r"C:\temp\new";    // 11 bytes: exactly the characters you see
let cooked = "C:\temp\new";    //  9 bytes: \t became a tab, \n a newline

Every literal, in one table

you type you get
"…" &'static str UTF-8 text, supporting the three escapes below
"\n\r\t\0\\" the common escapes — and \" for a quote
"\x36" one ASCII byte, \x00\x7F only
"\u{7fff}" any Unicode scalar value, up to six hex digits
r"…" &'static str raw: no escape above is interpreted
r#"…"# raw, and the text may contain "
c"…" &'static CStr NUL-terminated, for calling C — since 1.77
cr"…", cr#"…"# the raw combination of the two above
b"…" &'static [u8; N] ASCII source, and no UTF-8 promise at all
br"…", br#"…"# the raw combination again
b'x' u8 one byte, an ASCII byte literal
'🦀' char one Unicode scalar value, always four bytes wide

The same twelve, run:

Verified output of raw_strings_and_escapes.rs — regenerated by tools/run_examples.py, never hand-typed.

THE TWELVE LITERALS
  you type               you get            it holds
  "hello"                &'static str       "hello"
  "a\tb\nc\0d\\e"        &'static str       "a\tb\nc\0d\\e"
  "\x36"                 &'static str       "6"
  "\u{7fff}"             &'static str       "翿" = U+7FFF
  r"C:\temp\new"         &'static str       "C:\\temp\\new"
  r#"he said "hi""#      &'static str       "he said \"hi\""
  c"linker"              &'static CStr      "linker" [108, 105, 110, 107, 101, 114, 0]
  cr#"C:\a"b"#           &'static CStr      "C:\\a\"b"
  b"Hi"                  &'static [u8; 2]   [72, 105]
  br"\n"                 &'static [u8; 2]   [92, 110]  <- backslash, n
  b'A'                   u8                 65
  '🦀'                    char               '🦀' = U+1F980, 4 bytes of UTF-8, 4 bytes as a char

SAME ELEVEN KEYSTROKES, TWO DIFFERENT STRINGS
   "C:\temp\new"    9 bytes   "C:\temp\new"
  r"C:\temp\new"   11 bytes   "C:\\temp\\new"

COUNTING THE # UNTIL THE TEXT FITS
  r#"a " quote"#          "a \" quote"
  r##"a "# sequence"##    "a \"# sequence"

\x STOPS AT 7F IN TEXT, REACHES FF IN BYTES
  "\x7F"     "\u{7f}"   the highest a str will take
  b"\xF5"    [245]      a byte string owes UTF-8 nothing

A LITERAL SPANS LINES UNLESS YOU END ONE WITH \
  no backslash   "one\n    two"
  backslash      "one two"   <- newline AND the indent are eaten

DISPLAY RENDERS THE ESCAPE, DEBUG PRINTS IT BACK
  {}    a	b
  {:?}  "a\tb"

r is the one you reach for

Every backslash in a Windows path, a regex, or a chunk of embedded JSON is an escape the compiler will try to honour. "C:\temp\new" compiles cleanly and is wrong — \t and \n are real characters now, and the string is two bytes shorter than it looks. r"…" turns the escapes off wholesale.

The # count is a delimiter you widen until the text fits:

let quote = r#"he said "hi""#;        // the text contains "
let awkward = r##"a "# sequence"##;   // the text contains "#

Nothing dedents a multi-line literal, so a raw string is indented the way you wrote it. Ending a line with \ eats the newline and the leading whitespace of the next line — which is how a long message stays one line of output while staying readable in the source.

\x is ASCII in text, anything in bytes

\x stops at \x7F in a str, because above that a single byte is not valid UTF-8 on its own. In a byte string it goes to \xFF, since b"…" promises nothing about encoding — that is what it is for.

fn main() {
    let high = "\xF5";        // does not compile
    let polish = b"ł";        // does not compile either
    let nul = c"a\0b";        // nor this
}
Abridged — real rustc output for escapes.rs, aborting line dropped
error: out of range hex escape
 --> escapes.rs:2:18
  |
2 |     let _high = "\xF5";
  |                  ^^^^ must be a character in the range [\x00-\x7f]
  |
  = help: if you want to write a byte literal, use `b'\xF5'`
  = help: if you want to write a Unicode character, use `'\u{F5}'`

error: non-ASCII character in byte string literal
 --> escapes.rs:3:21
  |
3 |     let _polish = b"ł";
  |                     ^ must be ASCII
  |
help: if you meant to use the UTF-8 encoding of 'ł', use \xHH escapes
  |
3 -     let _polish = b"ł";
3 +     let _polish = b"\xC5\x82";
  |

error: null characters in C string literals are not supported
 --> escapes.rs:4:19
  |
4 |     let _nul = c"a\0b";
  |                   ^^

The third one is the C string type showing through the syntax: a CStr ends at its NUL, so it cannot contain one. "a\0b" is a perfectly ordinary three-character str — the refusal belongs to c"…", not to Rust. Which promise each type makes is six kinds of string.

What you see when you print one

An escape is resolved at compile time, so by run time there is no \t left to print — there is a tab. Display writes that tab; Debug writes the escape back, quotes included, which is why {:?} is the one to reach for when the question is what is actually in this string.

println!("{}", "a\tb");     // a	b
println!("{:?}", "a\tb");   // "a\tb"

If you are coming from another language

Python. Almost the whole table transfers, prefix for prefix — r"…", b"…", rb"…", \x41, \u1234, \N{…} — and two differences bite.

Python Rust
r"C:\temp\new" raw string r"C:\temp\new" — same idea, same spelling
r"ends with\" is a syntax error Python's raw strings still cannot end in a backslash r"ends with\" is fine; the # count, not the backslash, is what closes it
"""…""" for multi-line Rust needs no triple quote — every literal already spans lines "one\ntwo" written across two lines
b"\xF5" is bytes, "\xF5" is str 'õ' \xNN in a Python str means code point NN "\xF5" does not compile — \x is a byte, and a str is UTF-8
textwrap.dedent trimming indentation from a block literal no dedent in std; end lines with \ or trim yourself
a NUL-terminated literal for FFI c"…", which Python has no syntax for

ABAP. A string template |…| interprets \|, \{ and \\, and a text literal '…' interprets nothing at all — so 'C:\temp\new' in ABAP already behaves like Rust's r"C:\temp\new", and the surprise runs the other way: what needs no thought in ABAP needs the r prefix here. ABAP has no byte-string literal; a raw byte sequence is an X literal of hex digits (X'C582'), which is the same information as b"\xC5\x82" written as data rather than as text.

See also

Po polsku

Z tej tabeli dwa wiersze dotyczą polskiego tekstu bezpośrednio. \x kończy się na \x7F, czyli dokładnie na ASCII, a każda polska litera diakrytyczna leży wyżej — "\xF3" się nie skompiluje, ó zapisuje się jako "\u{F3}", a ł jako "\u{142}". W łańcuchu bajtowym b"…" jest jednocześnie luźniej i ciaśniej: \xNN sięga tam do \xFF, ale wpisanie ł wprost jest błędem (non-ASCII character in byte string literal), bo b"…" to &[u8; N] bez żadnej obietnicy UTF-8 — i właśnie po to istnieje. Kompilator podpowiada wtedy b"\xC5\x82", czyli te same dwa bajty, którymi ł jest zakodowane w UTF-8.

Surowy łańcuch r"…" przydaje się natomiast tam, gdzie ukośnik ma zostać ukośnikiem: "C:\temp\nowe" to w rzeczywistości C:, tabulator, emp, nowa linia i owe, podczas gdy r"C:\temp\nowe" jest tym, co widać.

Szukaj po polsku: surowe łańcuchy znaków · sekwencje ucieczki · polskie znaki diakrytyczne w kodzie · rust raw string literal · rust byte string literal b""