Polyglot
Languages Lua strings
Lua § strings

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.concat for substantial.
  • Use string.format for printf-style formatting.
  • Use s:method(...) for fluent calls.
  • Use Lua patterns for substantial string parsing (not regex).
  • Use gmatch for iterating matches.
  • Use gsub for substitution.
  • Use the utf8 library for Unicode-aware operations.
  • Avoid string global indexings: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.