Strings
Lua strings are immutable byte sequences — typically interpreted as ASCII or UTF-8, but the language treats them as raw bytes. The principal forms: single-quoted, double-quoted, and long-bracket ([[ ... ]]). The string library admits substantial functions for manipulation: string.format (printf-style), string.sub, string.rep, string.upper, string.lower, string.find/string.match/string.gmatch/string.gsub (Lua patterns — simpler than regex). Concatenation uses ..; length uses #. The combination — immutable byte strings, the simpler-than-regex pattern language, the substantial string.* library, the multiple literal forms — is the substance of Lua’s text surface.
String literals
Three principal forms:
local single = 'single quotes'
local double = "double quotes"
local long = [[
multi-line string
admits raw newlines and "quotes" without escapes
]]
The single and double forms are equivalent — both admit escape sequences:
local s = "newline\n tab\t backslash\\"
local s = '\65' -- "A" (ASCII 65)
local s = '\x41' -- "A" (hex 41)
local s = '\u{1F600}' -- 😀 (5.3+)
The long-bracket form does not interpret escapes:
local s = [[\n is two characters: backslash and n]]
local s = [[
The first newline after [[ is stripped if present.
]]
For long brackets containing ]], the equals-padded form admits substantial nesting:
local s = [==[
This admits ]] inside the string.
]==]
The number of = signs in the opening must match the closing.
Escape sequences
Single and double quote forms admit:
"\a" -- bell (0x07)
"\b" -- backspace (0x08)
"\f" -- form feed (0x0C)
"\n" -- newline (0x0A)
"\r" -- carriage return (0x0D)
"\t" -- tab (0x09)
"\v" -- vertical tab (0x0B)
"\\" -- backslash
"\"" -- double quote
"\'" -- single quote
"\65" -- ASCII 65 ("A")
"\x41" -- hex 41 ("A")
"\u{1F600}" -- Unicode 0x1F600 (5.3+)
"\z" -- skip whitespace (concatenation)
The \z admits substantial multi-line literal flexibility:
local s = "first line \z
and continued without break"
-- "first line and continued without break"
String concatenation and length
local s = "hello" .. " " .. "world" -- "hello world"
print(#s) -- 11
-- Implicit number coercion:
local s = "value: " .. 42 -- "value: 42"
local s = "pi: " .. math.pi -- "pi: 3.1415926535898"
For substantial concatenation, table.concat is conventionally efficient:
local parts = {}
for i, item in ipairs(items) do
parts[i] = tostring(item)
end
local result = table.concat(parts, ", ")
The string library
The principal functions:
string.len(s) -- length (same as #s)
string.upper(s) -- uppercase
string.lower(s) -- lowercase
string.reverse(s)
string.rep(s, n) -- repeat n times
string.rep(s, n, sep) -- with separator (5.3+)
string.sub(s, i, j) -- substring [i, j]
string.byte(s, i) -- byte value at i
string.char(n1, n2, ...) -- chars from byte values
string.format(fmt, ...) -- printf-style
string.find(s, pattern) -- pattern search
string.match(s, pattern) -- single match
string.gmatch(s, pattern) -- iterator over matches
string.gsub(s, pattern, replacement) -- global substitution
string.sub
string.sub("hello world", 1, 5) -- "hello"
string.sub("hello world", 7) -- "world"
string.sub("hello", -3) -- "llo" (negative from end)
string.sub("hello", -3, -1) -- "llo"
The 1-based indexing applies; negative indices count from the end.
string.format
The printf-style formatter:
string.format("%d + %d = %d", 2, 3, 5) -- "2 + 3 = 5"
string.format("%.2f", math.pi) -- "3.14"
string.format("%5d", 42) -- " 42"
string.format("%-5d|", 42) -- "42 |"
string.format("%05d", 42) -- "00042"
string.format("%s is %d", "Alice", 30) -- "Alice is 30"
string.format("%x", 255) -- "ff"
string.format("%q", 'hello "world"') -- '"hello \"world\""' (Lua-quoted)
The format specifiers follow C printf conventions; %q admits Lua-readable quoting (substantial for serialisation).
Method-style invocation
The s:method(...) form is sugar for string.method(s, ...):
local s = "hello"
print(s:upper()) -- "HELLO"
print(s:len()) -- 5
print(s:sub(1, 3)) -- "hel"
print(s:format("hi")) -- formatted (s as fmt)
print(s:rep(3)) -- "hellohellohello"
The form is conventional in modern Lua — admits substantial fluent reading.
Patterns
Lua’s pattern language is simpler than regex — uses % for special chars instead of \:
%a -- letters
%A -- non-letters
%d -- digits
%D -- non-digits
%s -- whitespace
%S -- non-whitespace
%w -- alphanumeric
%W -- non-alphanumeric
%p -- punctuation
%P -- non-punctuation
%l -- lowercase
%u -- uppercase
%c -- control characters
%x -- hex digits
. -- any character
-- Modifiers:
* -- 0 or more (greedy)
+ -- 1 or more (greedy)
- -- 0 or more (lazy/shortest)
? -- 0 or 1
-- Anchors:
^ -- start of string (or start of class with negation)
$ -- end of string
-- Character classes:
[abc] -- a, b, or c
[^abc] -- not a, b, or c
[a-z] -- range
[%w_] -- alphanumeric or underscore
-- Captures:
( ) -- capture group
%1, %2, ... -- backreferences
-- Special:
%% -- literal %
%. -- literal .
string.find
string.find("hello world", "world") -- 7, 11 (start, end)
string.find("abc123", "%d+") -- 4, 6
string.find("hello", "ll", 1, true) -- 3, 4 (plain text, no patterns)
string.find("hello", "no match") -- nil
string.match
Returns the matched (or captured) text:
string.match("Date: 2026-01-15", "%d+-%d+-%d+") -- "2026-01-15"
-- Captures:
local year, month, day = string.match("2026-01-15", "(%d+)-(%d+)-(%d+)")
-- year="2026", month="01", day="15"
local name, age = string.match("Alice 30", "(%w+) (%d+)")
-- name="Alice", age="30"
string.gmatch
Iterator over all matches:
for word in string.gmatch("the quick brown fox", "%w+") do
print(word) -- the, quick, brown, fox
end
-- With captures:
for name, age in string.gmatch("Alice 30, Bob 25", "(%w+) (%d+)") do
print(name, age)
end
string.gsub
Global substitution:
string.gsub("hello world", "o", "O") -- "hellO wOrld", 2 (count)
string.gsub("hello world", "o", "O", 1) -- "hellO world", 1 (max replacements)
-- With captures and backreferences:
string.gsub("hello world", "(%w+)", "<%1>") -- "<hello> <world>"
-- With function:
string.gsub("hello", "%w", function(c)
return c:upper()
end) -- "HELLO", 5
-- With table:
local replacements = {hello = "hi", world = "earth"}
string.gsub("hello world", "%w+", replacements) -- "hi earth"
The third argument may be a string, function, or table.
Common patterns
Building a string
-- Inefficient:
local s = ""
for i = 1, 100 do
s = s .. tostring(i)
end
-- Efficient:
local parts = {}
for i = 1, 100 do
parts[i] = tostring(i)
end
local s = table.concat(parts)
Multi-line literals
local sql = [[
SELECT id, name, email
FROM users
WHERE active = 1
ORDER BY created_at DESC
]]
local html = string.format([[
<html>
<body>
<h1>%s</h1>
<p>%s</p>
</body>
</html>
]], title, content)
Splitting
Lua does not include string.split; the conventional implementation:
local function split(s, sep)
sep = sep or "%s"
local parts = {}
for part in string.gmatch(s, "([^" .. sep .. "]+)") do
parts[#parts + 1] = part
end
return parts
end
local words = split("hello world foo") -- {"hello", "world", "foo"}
local fields = split("a,b,c,d", ",") -- {"a", "b", "c", "d"}
Joining
local s = table.concat({"a", "b", "c"}, ", ") -- "a, b, c"
Trimming
local function trim(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
trim(" hello ") -- "hello"
The parentheses around s:gsub(...) admit returning only the first value (gsub returns string and count).
Replacing
local s = "hello world"
s:gsub("world", "Lua") -- "hello Lua", 1
s:gsub("o", "0", 2) -- "hell0 w0rld", 2 (max 2)
Case-insensitive find
local function ifind(s, pattern)
return s:lower():find(pattern:lower())
end
ifind("Hello World", "HELLO") -- 1, 5
Number formatting
string.format("%d", 42) -- "42"
string.format("%.2f", math.pi) -- "3.14"
string.format("%e", 12345.6789) -- "1.234568e+04"
string.format("%,d", 1000000) -- not supported in standard Lua
string.format("%5d", 3) -- " 3"
For thousands separators:
local function format_thousands(n)
local s = tostring(n)
return s:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
end
format_thousands(1234567) -- "1,234,567"
Validation
local function is_email(s)
return s:match("^[^@]+@[^@]+%.[^@]+$") ~= nil
end
local function is_numeric(s)
return s:match("^%-?%d+$") ~= nil
end
local function is_alphanumeric(s)
return s:match("^[%w]+$") ~= nil
end
Substring extraction
local s = "Date: 2026-01-15"
-- Extract date:
local date = s:sub(7) -- "2026-01-15"
local date = s:match("%d+-%d+-%d+") -- "2026-01-15"
-- Extract components:
local year, month, day = s:match("(%d+)-(%d+)-(%d+)")
Reverse and length
local s = "hello"
print(s:reverse()) -- "olleh"
print(#s) -- 5
print(s:len()) -- 5 (equivalent)
Padding
local function pad_left(s, n, char)
char = char or " "
return string.rep(char, n - #s) .. s
end
local function pad_right(s, n, char)
char = char or " "
return s .. string.rep(char, n - #s)
end
pad_left("42", 5, "0") -- "00042"
pad_right("hello", 10, "*") -- "hello*****"
-- Or with format:
string.format("%05d", 42) -- "00042"
string.format("%-10s|", "hello") -- "hello |"
Reading lines
for line in io.lines("file.txt") do
print(line)
end
-- Or from a string:
local text = [[line one
line two
line three]]
for line in text:gmatch("[^\n]+") do
print(line)
end
Word count
local function word_count(s)
local count = 0
for _ in s:gmatch("%S+") do
count = count + 1
end
return count
end
word_count("the quick brown fox") -- 4
Character iteration
local s = "hello"
-- By byte:
for i = 1, #s do
print(s:sub(i, i)) -- "h", "e", "l", "l", "o"
end
-- Or:
for i = 1, #s do
print(string.char(s:byte(i)))
end
For UTF-8 (5.3+):
for p, c in utf8.codes(s) do
print(p, c, utf8.char(c)) -- byte position, code point, char
end
A note on bytes vs characters
Lua strings are byte sequences. For UTF-8, the utf8 library (5.3+) admits substantial Unicode-aware operations:
local s = "héllo"
print(#s) -- 6 (bytes; é is 2 bytes)
print(utf8.len(s)) -- 5 (characters)
for p, c in utf8.codes(s) do
print(p, utf8.char(c)) -- per character
end
The conventional discipline is byte-level for ASCII operations and utf8 for substantial international text.
A note on the conventional discipline
The contemporary Lua strings advice:
- Use double quotes for routine strings.
- Use long brackets
[[ ]]for multi-line and embedded code/text. - Use
..for short concatenation;table.concatfor substantial. - Use
string.formatfor printf-style formatting. - Use
s:method(...)for fluent calls. - Use Lua patterns for substantial string parsing (not regex).
- Use
gmatchfor iterating matches. - Use
gsubfor substitution. - Use the
utf8library for Unicode-aware operations. - Avoid
stringglobal indexing —s:method(...)is conventional.
The combination — immutable byte strings, the simpler-than-regex pattern language, the substantial string.* library, the multiple literal forms (single, double, long-bracket), the implicit numeric coercion in .., the method-call sugar — is the substance of Lua’s text surface. The discipline produces concise, expressive text handling.