Strings and Lua patterns
Build strings efficiently, format output with string.format, and use find, match, gmatch and gsub with Lua's own pattern syntax.
The string library
local s = "Hello, Lua"
print(#s) --> 10, the length operator works on strings
print(s:upper()) --> HELLO, LUA (method-call sugar on a string)
print(s:sub(1, 5)) --> Hello (1-based, inclusive)
print(s:sub(-3)) --> Lua (negative counts from the end)
print(s:len(), string.len(s)) --> 10 10
-- searching with plain text: find the fourth argument true to disable patterns
print(s:find("Lua"))
-- joining and splitting
print(table.concat({ "a", "b", "c" }, "-")) --> a-b-c
local function split(text, sep)
local out = {}
for field in text:gmatch("([^" .. sep .. "]+)") do
out[#out + 1] = field
end
return out
end
print(table.concat(split("a,b,,c", ","), "|")) --> a|b|c (empty fields skipped)
-- formatting: the C printf family, with the same pitfalls
print(string.format("%d items at %.2f each", 3, 1.5))
print(string.format("%5s|%-5s|", "ab", "ab")) --> ab|ab |
print(string.format("%q", 'he said "hi"')) --> quoted and escaped, valid Lua source
print(string.format("%x %X %o", 255, 255, 8)) --> ff FF 10
-- building a large string
local parts = {}
for i = 1, 1000 do parts[#parts + 1] = tostring(i) end
local joined = table.concat(parts, ",") -- one allocation, not 1000
print(#joined > 0) --> true- Strings are immutable.
s = s .. xin a loop creates a new string each time, which is quadratic; collect into a table and usetable.concat. string.format("%d", value)errors whenvalueis a float with a fractional part in Lua 5.3 and later. Use%.0formath.floorfirst.#sgives the length in bytes, not characters. A multi-byte UTF-8 string reports its byte count.- The
%qformat writes a string that can be read back as Lua source, which is the safe way to emit a string literal.
Lua patterns
| Class | Matches | Example |
|---|---|---|
%a | Letters | %a+ matches a word |
%d | Digits | %d%d%d matches three digits |
%w | Letters and digits | [%w_]+ matches an identifier |
%s | Whitespace | %s* matches optional spacing |
%p | Punctuation | %p matches a comma or a dot |
%l / %u | Lower / upper case | %u%l+ matches a capitalised word |
%x | Hexadecimal digit | Parse a colour code |
%. | A literal dot | Escape a magic character with % |
local log = [==[
2026-09-18 10:00:01 INFO started service
2026-09-18 10:00:02 WARN retrying: connection refused
2026-09-18 10:00:03 ERROR failed after 3 retries
]==]
-- find with captures returns the start, end and every capture
local s, e, date, level = log:find("(%d%d%d%d%-%d%d%-%d%d) (%u+)")
print(s, e, date, level) --> 2 21 2026-09-18 INFO
-- match: captures only
print(log:match("(%u+)%s+([^\n]+)")) --> INFO started service
-- gmatch: iterate every match, an iterator returning each capture list
for line in log:gmatch("[^\n]+") do
local date, time, lvl, msg = line:match("^(%S+) (%S+) (%u+)%s+(.*)$")
if lvl == "ERROR" then print("!!", msg) end
end
-- gsub: replace every occurrence, with a function, a table or a template
print(("a-b-c"):gsub("%-", "_")) --> a_b_c 2
print(("name=%s age=%s"):gsub("%%s", { "ada", "36" })) -- table substitution
print(("hello world"):gsub("(%w+)", function(w) return w:upper() end))
-- the count is the second return value; capture it only when you need it
local replaced, count = ("aaa"):gsub("a", "b")
print(replaced, count) --> bbb 3
-- anchored match: '^' at the front of the pattern
print(("abc"):match("^a")) --> a
print(("abc"):find("^b")) --> nil
-- a greedy versus lazy quantifier: '-' is the non-greedy form of '*'
print(("<a><b>"):match("<(.-)>")) --> a (lazy)
print(("<a><b>"):match("<(.*)>")) --> a><b (greedy)- Lua patterns are not regular expressions. There are no alternation groups, no backreferences and no lookaround;
-is non-greedy repetition and%b()matches balanced delimiters. %bxyis the balanced-match class:("f(a(b)c)"):match("%b()")returns the whole parenthesised group. It handles nesting, which no regex can do without recursion.- Any magic character in a plain search must be escaped with
%. The magic set is^$()%.[]*+-?. string.findwith the fourth argumenttruetreats the pattern as plain text, which is much faster when you are not matching a pattern at all.gsubreturns the new string and the number of substitutions. A common bug is assigning only the first and losing the count.
⚠️
Never build a pattern by concatenating untrusted input.
("text"):find(user_input) lets the input act as a pattern, and a crafted one can hang your program or make the match wrong. Escape the magic characters with input:gsub("%W", "%%%0") before using it as plain text, or pass the plain flag.FAQ
How do I split a string properly?
gmatch with [^sep]+ skips empty fields, which is usually what you want. If empty fields matter, walk the string with find and record the gaps including the leading and trailing ones.Why does my pattern not match a newline?
The
. class in Lua matches any character including newlines, unlike many regex flavours. If a match stops, the problem is usually the class or an unanchored ^ in the middle of a pattern, which only has meaning at the start.Related
Lua syntax and types The standard library in daily use
Last refreshed 2026-09-18.