Quirrel4.41.0

Strings

A Quirrel string is an immutable sequence of bytes, not of characters. len counts bytes. There are three literal forms: ordinary "...", verbatim @"...", and interpolated $"...".

Ordinary strings

"..." accepts the usual backslash escapes: \n \t \a \b \r \v \f, a literal \\, \" and \', and \0 for a zero byte. \xHH writes one byte from one or two hex digits. \uHHHH and \UHHHHHHHH write a Unicode code point encoded as UTF-8, from up to four and up to eight hex digits. Any other character after a backslash, including { or }, is a compile error. \{ and \} are valid only in an interpolated string (see below).

A newline may not appear directly inside "...". Write \n, or use a verbatim string.

examples/pages/language/strings-escapes.nut
// \n \t and friends work as in C; \xHH writes one raw byte
let briefing = "turret\tready\nstatus: \x4F\x4B"
println(briefing)

// an escape the compiler does not recognise is a compile error - a
// backslash is never just "the next character", so escape it too
println("supply\\depot")
Output:
turret	ready
status: OK
supply\depot

Verbatim strings

@"..." takes every character between the quotes literally. No backslash escape is processed, and a real newline is allowed, so it is the convenient form for a multi-line block of text. Because \ is not special, the only way to put a " inside is to write "". The newline in the string is the newline in the file, so a file saved with CRLF puts a \r in the string as well.

examples/pages/language/strings-verbatim.nut
// @"..." takes every character literally; double a quote to put one in
let modelPath = @"content\tanks\""t34""\turret.dag"
println(modelPath)

// and, unlike an ordinary string, it may span real newlines. The break is the
// one in the file, so a CRLF file puts a \r in the string: strip each line
let briefing = @"first wave: north ridge
second wave: south gate"
foreach (line in briefing.split("\n"))
  println(line.strip())
Output:
content\tanks\"t34"\turret.dag
first wave: north ridge
second wave: south gate

Interpolated strings

$"..." mixes text with {expr} holes. Each hole is evaluated, converted to a string, and inserted into the result. The compiler turns the whole literal into a call to subst with one {n} placeholder per hole: $"{a}: {b}" compiles to "{0}: {1}".subst(a, b).

A literal brace in the text must be written \{ or \}, because a bare { opens a hole. A hole can hold any expression, including another $"...". The inner literal is parsed as its own expression, so one interpolated string can be nested inside a hole of another.

examples/pages/language/strings-interpolation.nut
let squadName = "alpha"
let hitPoints = 87

// {expr} substitutes; the whole literal compiles to a call to subst
println($"{squadName}: {hitPoints} hp")

// a brace meant to print literally has to be escaped
println($"damage taken: \{{hitPoints}\}")

// a hole may hold another interpolated string
println($"squad: {$"[{squadName}]"}")
Output:
alpha: 87 hp
damage taken: {87}
squad: [alpha]

Building strings

concat, join and subst are type methods. They can be called on any string (usually "" or a separator string). They are the recommended way to build a string from parts.

+ also concatenates when either operand is a string. Avoid it. + is evaluated left to right, so in 1 + 2 + "a" + 3 the numbers before the string are added and everything after it is concatenated: the result is "3a3". The static analyzer flags this as w264. See Operators and expressions for how + decides between adding and stringifying.

examples/pages/language/strings-concat.nut
let weaponName = "kar98k"
let ammoLeft = 5

// prefer these to +: none of them silently stringifies a stray operand
println("".concat(weaponName, ": ", ammoLeft.tostring()))
println(", ".join(["alpha", "bravo", "charlie"]))
println("{0} has {1} rounds left".subst(weaponName, ammoLeft))

// DEPRECATED:
// + also concatenates once either side is a string - but it is still
// left-associative +, so where the string sits in the chain matters
println("1 + \"2\" =", 1 + "2")
println("2 + 3 + \"1\" =", 2 + 3 + "1")
println("\"1\" + 2 + 3 =", "1" + 2 + 3)
Output:
kar98k: 5
alpha, bravo, charlie
kar98k has 5 rounds left
1 + "2" = 12
2 + 3 + "1" = 51
"1" + 2 + 3 = 123

Bytes, not characters

Because a string is a sequence of bytes, a multi-byte UTF-8 character counts as more than one unit for len, slice or an index. There is no separate character count. Write a non-ASCII byte sequence with \x or \u escapes when a script must be portable across source encodings.

examples/pages/language/strings-bytes.nut
let callsign = "OK"
println("callsign.len() =", callsign.len())

// \x writes raw bytes: a 2-byte UTF-8 character counts as 2, not 1
let playerNick = "\xD0\x9F"
println("playerNick.len() =", playerNick.len())
Output:
callsign.len() = 2
playerNick.len() = 2