stringLuaLuauLuwu

A string is an immutable sequence of bytes that usually represents text. The string library slices, searches, splits, formats and packs strings. Method calls on strings call functions in the string library: s:upper() functions the same as string.upper(s).

Please note that unlike many high-level programming languages, Luwu strings are not UTF-8 encoded. Lua and Luau code from before the buffer primitive was added often treats strings as the primary vehicle for representing arbitrary bytes.

The utf8 library handles Unicode codepoints, but it's not ergonomic to use and it doesn't yet support the full featureset you'd expect for Unicode. We're looking forward to adding full Unicode support in Luwu, including graphemes, grapheme clusters, Unicode-aware string library functions, and more.

In modern Luau, you may see users and libraries avoid string method calls like the plague. This is because in the Luau VM, string method calls are 2x as expensive as string library calls. We've fixed this: with FFlag LuwuStringExtensions enabled, string method calls are just as fast as string library calls.

find, match, gmatch and gsub search with Patterns, strings that describe the text to look for. split and the Luwu additions like contains and replace compare plain text and never read patterns.

Input
const line = "nyla, ada, lin"
const names = line:split(", ")
print(names[2]:upper())
print(line:contains("ada"))
print(line:match("%a+$"))
Output
ADA
true
lin
Summary

Bytes and slicing

string.byteLua
Returns the numeric codes of a string's bytes
string.charLua
Builds a string from byte codes
string.lenLua
Returns the number of bytes in a string
string.subLua
Returns the part of a string between two indices
string.repLua
Repeats a string
string.reverseLua
Reverses a string's bytes
string.lowerLua
Changes ASCII letters to lowercase
string.upperLua
Changes ASCII letters to uppercase

Searching with patterns

string.findLua
Finds a pattern or plain text and returns where it is
string.matchLua
Returns the captures of a pattern's first match
string.gmatchLua
Loops over every match of a pattern
string.gsubLua
Replaces every match of a pattern

Searching plain text

string.containsLuwuflagged
Checks whether a string contains any of the given strings
string.startswithLuwuflagged
Checks whether a string starts with any of the given strings
string.endswithLuwuflagged
Checks whether a string ends with any of the given strings
string.replaceLuwuflagged
Replaces plain text

Splitting and trimming

string.splitLuauLuwu
Splits a string at a separator
string.trimLuwuflagged
Removes whitespace or the given strings from both ends
string.trimstartLuwuflagged
Removes whitespace or the given strings from the start
string.trimendLuwuflagged
Removes whitespace or the given strings from the end

Formatting

string.formatLuaLuau
Formats values into a string with printf-style specifiers

Binary data

string.packLuau
Encodes values as binary data
string.packsizeLuau
Returns how many bytes a pack format takes
string.unpackLuau
Decodes binary data

Bytes and slicing

string.byteLua

function string.byte(s: string, i: number?, j: number?): ...number

Returns the numeric code of each byte of s from index i to index j. i defaults to 1 and j defaults to i. With one index or none, byte returns a single code.

Negative indices count back from the end. Indices past either end are clamped to the string. When the range is empty, byte returns no values.

Input
print(string.byte("A"))
const first, last = string.byte("Hi", 1, -1)
print(first)
print(last)
Output
65
72
105

string.charLua

function string.char(...: number): string

Returns a string with one byte for each argument. Each value must be an integer from 0 to 255.

Input
print(string.char(72, 105, 33))
Output
Hi!

string.lenLua

function string.len(s: string): number

Returns the number of bytes in s. It's the same as #s. A character outside ASCII takes more than one byte. utf8.len counts characters.

Input
print(string.len("héllo"))
print(utf8.len("héllo"))
Output
6
5

string.subLua

function string.sub(s: string, i: number, j: number?): string

Returns the part of s from index i to index j, both included. j defaults to -1, the last byte.

Negative indices count back from the end and indices past either end are clamped to the string. When i comes after j, sub returns an empty string.

Input
const greeting = "hello world"
print(greeting:sub(1, 5))
print(greeting:sub(-5))
Output
hello
world

string.repLua

function string.rep(s: string, n: number): string

Returns s repeated n times. A zero or negative n gives an empty string.

Input
print(string.rep("ab", 3))
Output
ababab

string.reverseLua

function string.reverse(s: string): string

Returns s with its bytes in reverse order. A character that takes more than one byte comes out as invalid UTF-8.

Input
print(("stressed"):reverse())
Output
desserts

string.lowerLua

function string.lower(s: string): string

Returns a copy of s with every ASCII uppercase letter changed to lowercase. Other bytes stay as they are, letters outside ASCII included.

Input
print(("ÉCOLE"):lower())
Output
École

string.upperLua

function string.upper(s: string): string

Returns a copy of s with every ASCII lowercase letter changed to uppercase. Other bytes stay as they are, letters outside ASCII included.

Input
print(("école"):upper())
Output
éCOLE

Searching with patterns

string.findLua

function string.find(
    s: string,
    pattern: string,
    init: number?,
    plain: boolean?
): (number?, number?, ...string)

Looks for the first match of pattern in s that starts at index init or later. It returns the indices where the match starts and ends, followed by the pattern's captures. When nothing matches, find returns nil.

init defaults to 1 and a negative init counts back from the end. When plain is true, find compares pattern as plain text. Searching for user input or text with . or % in it needs plain, or string.contains when the indices don't matter.

Input
const start, finish = string.find("key = value", " = ")
print(start)
print(finish)
print((string.find("a.b", ".")))
print((string.find("a.b", ".", 1, true)))
Output
4
6
1
2

string.matchLua

function string.match(s: string, pattern: string, init: number?): ...string?

Looks for the first match of pattern in s that starts at index init or later. It returns the pattern's captures, or the whole match when the pattern has no captures. When nothing matches, match returns nil. init defaults to 1 and a negative init counts back from the end.

Input
print(string.match("level 12", "%d+"))
const user, host = string.match("ada@example.com", "^(.-)@(.+)$")
print(user)
print(host)
print(string.match("no digits", "%d+"))
Output
12
ada
example.com
nil

string.gmatchLua

function string.gmatch(s: string, pattern: string): () -> ...string

Returns an iterator that finds each match of pattern in s in turn. Each call returns the pattern's captures, or the whole match when the pattern has no captures. A for loop calls it until it runs out of matches.

A ^ at the start of the pattern doesn't anchor it here. It matches a literal ^ instead.

Input
for word in string.gmatch("one two three", "%a+") do
    print(word)
end
for key, value in string.gmatch("hp=30, gold=12", "(%w+)=(%w+)") do
    print(`{key} is {value}`)
end
Output
one
two
three
hp is 30
gold is 12

string.gsubLua

function string.gsub(
    s: string,
    pattern: string,
    replacement: string | { [string]: string } | (...string) -> string?,
    max: number?
): (string, number)

Replaces every match of pattern in s and returns the new string followed by the number of matches it replaced. max limits how many matches are replaced. What each match becomes depends on replacement:

When the table or function gives nil or false, the match stays as it was. Passing gsub's result straight into another call passes the count too. Wrap the call in parentheses to keep only the string, or use string.replace to replace plain text.

Input
print((string.gsub("hello world", "o", "0")))
print((string.gsub("Ada Lovelace", "(%a+) (%a+)", "%2, %1")))
const prices: { [string]: string } = { apple = "1.20", kiwi = "0.80" }
print((string.gsub("apple and kiwi", "%a+", prices)))
Output
hell0 w0rld
Lovelace, Ada
1.20 and 0.80

Searching plain text

string.containsLuwuflagged

function string.contains(s: string, needle: string, ...: string): boolean

Returns whether s contains needle or any of the other strings given. The strings are compared byte by byte and are never patterns. An empty needle is in every string.

Input
print(("a.b"):contains("."))
print(("abc"):contains("."))
print(("error: disk full"):contains("warning", "error"))
Output
true
false
true

string.startswithLuwuflagged

function string.startswith(s: string, prefix: string, ...: string): boolean

Returns whether s starts with prefix or with any of the other strings given. The strings are compared byte by byte and are never patterns. An empty prefix matches every string.

Input
print(("# a comment"):startswith("#", "--"))
print(("-- a comment"):startswith("#", "--"))
print(("value"):startswith("#", "--"))
Output
true
true
false

string.endswithLuwuflagged

function string.endswith(s: string, suffix: string, ...: string): boolean

Returns whether s ends with suffix or with any of the other strings given. The strings are compared byte by byte and are never patterns. An empty suffix matches every string.

Input
print(("main.luwu"):endswith(".luau", ".luwu"))
print(("main.lua"):endswith(".luau", ".luwu"))
Output
true
false

string.replaceLuwuflagged

function string.replace(s: string, old: string, new: string, max: number?): string

Replaces occurrences of old in s with new from left to right without overlapping and returns the new string. Both old and new are plain text: . and % need no escaping.

max limits the number of replacements and a max of 0 returns s unchanged. A negative max or an empty old is an error. Unlike string.gsub, replace returns only the string.

Input
print(("a.b.c"):replace(".", "/"))
print(("100%"):replace("%", " percent"))
print(("aaaa"):replace("a", "b", 2))
print(("aaa"):replace("aa", "b"))
Output
a/b/c
100 percent
bbaa
ba

Splitting and trimming

string.splitLuauLuwu

function string.split(s: string, separator: string?, maxpieces: number?): { string }

Splits s at each occurrence of separator and returns the pieces as an array. separator defaults to "," and is compared as plain text, not as a pattern. Two separators in a row leave an empty piece between them. An empty separator splits s into one-byte strings.

With maxpieces, split returns at most that many pieces and the last piece holds the rest of the string. maxpieces must be at least 1.

Input
const parts = string.split("a,b,,c")
print(#parts)
print(table.concat(parts, "|"))
const key, value = table.unpack(string.split("role = admin = yes", " = ", 2))
print(key)
print(value)
Output
4
a|b||c
role
admin = yes

string.trimLuwuflagged

function string.trim(s: string, ...: string): string

Removes whitespace, or the strings given, from both ends of s.

With no strings, trim removes every character with Unicode's White_Space property and the byte order mark (U+FEFF). That covers the ASCII tab, line feed, vertical tab, form feed, carriage return and space. It also covers characters the %s pattern class misses, like the no-break space (U+00A0) and the ideographic space (U+3000).

With strings, trim removes only those and leaves whitespace alone. Each string is a whole token rather than a set of characters. trim keeps removing tokens from an end until none is left there, and at each step the first given string that matches is removed. Empty strings are ignored.

Input
print(`[{("  hello  "):trim()}]`)
print(`[{("\u{FEFF}name,age\u{3000}\r\n"):trim()}]`)
print(("--title--"):trim("-"))
print(`[{(" --title-- "):trim("-")}]`)
Output
[hello]
[name,age]
title
[ --title-- ]

string.trimstartLuwuflagged

function string.trimstart(s: string, ...: string): string

Works like string.trim at the start of s only.

Input
print(`[{("  indented  "):trimstart()}]`)
print(("////path"):trimstart("//"))
Output
[indented  ]
path

string.trimendLuwuflagged

function string.trimend(s: string, ...: string): string

Works like string.trim at the end of s only. A token like "\r\n" is removed only as a pair. Pass "\r" and "\n" separately to remove either one.

Input
const function show(s: string): string
    return s
        :replace("\r", "\\r")
        :replace("\n", "\\n")
end
print(show(("/usr/local/"):trimend("/")))
print(show(("line\r\n\r\n"):trimend("\r\n")))
print(show(("line\n"):trimend("\r\n")))
print(show(("line\r\n\n"):trimend("\r", "\n")))
Output
/usr/local
line
line\n
line

Formatting

string.formatLuaLuau

function string.format(format: string, ...: unknown): string

Returns format with each specifier replaced by the next argument. Specifiers start with % and follow C's printf:

SpecifierWritesExampleResult
%dan integer in decimalstring.format("%d", 42)"42"
%ian integer in decimal, the same as %dstring.format("%i", 42)"42"
%da number with a fraction truncated toward zerostring.format("%d", -7.9)"-7"
%uan unsigned integer in decimalstring.format("%u", 42)"42"
%oan integer in octalstring.format("%o", 8)"10"
%xan integer in lowercase hexadecimalstring.format("%x", 255)"ff"
%Xan integer in uppercase hexadecimalstring.format("%X", 255)"FF"
%cthe byte with the given codestring.format("%c", 65)"A"
%ea number in scientific notationstring.format("%e", 1234.5)"1.234500e+03"
%Ea number in scientific notation with a capital Estring.format("%E", 1234.5)"1.234500E+03"
%fa number in decimal notationstring.format("%f", 3.14159)"3.141590"
%ga number the shorter way of %e and %fstring.format("%g", 0.0001)"0.0001"
%Ga number the shorter way of %E and %fstring.format("%G", 1e-10)"1E-10"
%qa string in double quotes, escaped so Luwu reads it back as the same stringstring.format("%q", "say \"hi\"")"\"say \\\"hi\\\"\""
%sa string or number as it isstring.format("%s", "gold")"gold"
%*any value, converted the way tostring converts itstring.format("%*", true)"true"
%%a percent signstring.format("%%")"%"

Between the % and the letter, a specifier can have flags (-, +, space, # and 0), a width and a precision, like %-8s or %.2f. A width or precision has at most two digits and * can't stand in for one. %* takes no flags, width or precision.

Input
print(string.format("%-6s|%5.1f|%03d", "gold", 12.345, 7))
print(string.format("%q", 'say "hi"'))
print(string.format("%* and %*", true, nil))
Output
gold  | 12.3|007
"say \"hi\""
true and nil

Binary data

string.packLuau

function string.pack(format: string, ...: unknown): string

Encodes the arguments as binary data according to format and returns the bytes as a string. Each option in format encodes one value or sets how the next ones are encoded:

OptionEncodesSize
<switches to little-endian for the options after itnone
>switches to big-endiannone
=switches to the machine's native byte ordernone
!naligns values to at most n bytes, or 8 when n is left outnone
ba signed integer1 bytes
Ban unsigned integer1 bytes
ha signed integer2 bytes
Han unsigned integer2 bytes
ja signed integer4 bytes
Jan unsigned integer4 bytes
la signed integer8 bytes
Lan unsigned integer8 bytes
Tan unsigned integer4 bytes
ina signed integer of n bytes, from 1 to 16n bytes, or 4 when n is left out
Inan unsigned integer of n bytes, from 1 to 16n bytes, or 4 when n is left out
fa float4 bytes
da float8 bytes
na float8 bytes
sna string after its length, which takes n bytes, or 4 when n is left outn bytes plus the string
za string followed by a zero byte. The string can't contain zeros.the string plus 1 byte
cna string of exactly n bytes, padded with zerosn bytes
xone byte of padding1 bytes
Xoppadding up to the alignment of option opup to op's size

Spaces between options are ignored. These sizes are the same on every platform. Lua 5.x sizes h, j, l, T and i after the C compiler's types instead.

Input
const data = string.pack("<I2 z", 513, "hi")
print(#data)
const number, name, next_index = string.unpack("<I2 z", data)
print(number)
print(name)
print(next_index)
Output
5
513
hi
6

string.packsizeLuau

function string.packsize(format: string): number

Returns the number of bytes string.pack produces for format. The variable-length options s and z are an error here.

Input
print(string.packsize("i4 h d"))
Output
14

string.unpackLuau

function string.unpack(format: string, data: string, init: number?): ...unknown

Decodes data according to format and returns the decoded values followed by the index of the first byte it didn't read. Decoding starts at byte init, which defaults to 1. string.pack lists the format options.