Strings and Pattern Matching Basics
Strings are everywhere in programming, and Lua gives you powerful tools to work with them. Whether you’re parsing data, building URLs, or processing user input, understanding Lua’s string and pattern capabilities will save you countless hours.
Prerequisites
You’ll need a Lua interpreter (5.1 or later) and a basic text editor. No external libraries are required; all string and pattern functions covered here are part of Lua’s standard string library.
Creating strings
In Lua, strings can be created using single quotes, double quotes, or long brackets:
local single = 'Hello'
local double = "World"
local long = [[This is a
multi-line string]]
Note: Lua strings are immutable. Every operation that appears to modify a string actually creates a new one.
Lua’s string handling goes beyond simple creation. The standard string library offers functions for case conversion, substring extraction, length queries, and more. Most of these functions can be called either as string.method(str) or via the colon syntax str:method() on string values; both forms are widely used in Lua codebases.
The string library
Lua’s string library provides functions for manipulating and analyzing strings. Most functions live in the string table:
local text = "Hello, Lua!"
-- Get length
print(#text) -- 11
print(string.len(text)) -- 11
-- Convert to uppercase/lowercase
print(string.upper("hi")) -- HI
print(string.lower("HI")) -- hi
-- Get substring
print(string.sub(text, 1, 5)) -- Hello
-- Find substring
local start, finish = string.find(text, "Lua")
print(start, finish) -- 8 10
Once you have the basics of string inspection and case conversion, the next logical step is building new strings from existing ones. Lua handles this with a dedicated operator rather than a method call, which keeps string construction clean and readable in expression-heavy code.
String concatenation
Combine strings using the .. operator:
local first = "Hello"
local second = "World"
local greeting = first .. " " .. second
print(greeting) -- Hello World
While the .. operator works fine for joining two or three strings, it becomes expensive inside loops because each concatenation creates a new string and copies the accumulated content. For building strings from many pieces, Lua provides table.concat, which joins all elements of a table in a single pass without repeated allocation.
Tip: For building strings in loops, use
table.concatinstead of..to avoid O(n²) performance:
local parts = {"a", "b", "c"}
local result = table.concat(parts, "-")
print(result) -- a-b-c
Pattern matching basics
Lua’s pattern matching is a lightweight alternative to full regex. It’s built into the string library and uses special characters called magic characters:
| Character | Meaning |
|---|---|
. | Any character |
%a | Any letter |
%d | Any digit |
%s | Any whitespace |
%w | Alphanumeric characters |
%p | Punctuation |
%b() | Balanced parentheses |
^ | Start of string (in pattern) |
$ | End of string (in pattern) |
Unlike regular expressions, Lua patterns use % as the escape character instead of \. This means %d matches a digit rather than \d, and %. matches a literal period. The tradeoff is simpler syntax at the cost of some regex features like alternation and lookaround.
Character classes
Character classes let you match types of characters:
local text = "abc123DEF"
-- Match all letters
print(string.match(text, "%a+")) -- abc
-- Match all digits
print(string.match(text, "%d+")) -- 123
-- Match uppercase letters
print(string.match(text, "%u+")) -- DEF
Matching a character class tells you what kind of character is at a given position, but practical pattern matching needs control over how many characters to consume. Lua provides four repetition operators that determine how aggressively the engine reads input, and understanding the difference between greedy and non-greedy matching is essential for writing correct patterns.
Repetition operators
Control how many times a pattern matches:
| Operator | Meaning |
|---|---|
* | 0 or more (greedy) |
+ | 1 or more (greedy) |
- | 0 or more (non-greedy) |
? | 0 or 1 |
local text = "123abc456"
print(string.match(text, "%d+")) -- 123 (greedy -- longest match)
print(string.match(text, "%d-")) -- (empty -- non-greedy)
print(string.match(text, "%a*")) -- abc (0 or more)
print(string.match(text, "%a?")) -- a (0 or 1)
Repetition operators tell the engine how much to consume, but you often need to extract specific portions of what was matched rather than the entire match. Lua’s capture mechanism lets you pull out individual pieces by wrapping parts of the pattern in parentheses, with each capture group returning a separate value from string.match.
Captures
Extract specific parts of a match using parentheses:
local date = "2024-03-17"
-- Capture each component
local year, month, day = string.match(date, "(%d+)-(%d+)-(%d+)")
print(year, month, day) -- 2024 03 17
-- Extract domain from email
local email = "user@example.com"
local user, domain = string.match(email, "(.+)@(.+)")
print(user, domain) -- user example.com
With captures in your toolkit, you can now search for and transform structured text in a single pass. The next section covers the two main functions for applying patterns to real strings: string.find for locating matches by position and string.gsub for replacing every occurrence globally.
Common pattern operations
Finding and replacing
local text = "The quick brown fox"
-- Find a pattern
local pos = string.find(text, "quick")
print(pos) -- 5
-- Replace all occurrences (gsub)
local result = string.gsub(text, "o", "X")
print(result) -- The quick brXwn fXx
-- Replace with captures
local result = string.gsub("Hello, John!", "(%w+)", "%1!")
print(result) -- Hello!, John!!
The global substitution function string.gsub replaces every occurrence of a pattern in a string, but Lua does not include a built-in function for splitting a string into parts by a delimiter. You can build one yourself using string.gmatch, which returns an iterator over all non-overlapping matches of a pattern.
Splitting strings
Lua doesn’t have a built-in split function, but you can create one:
function split(str, delimiter)
local result = {}
local pattern = string.format("([^%s]+)", delimiter)
for match in string.gmatch(str, pattern) do
table.insert(result, match)
end
return result
end
local parts = split("apple,banana,cherry", ",")
print(table.concat(parts, ", ")) -- apple, banana, cherry
Once you can split strings and extract substrings, a natural next step is checking whether input data conforms to expected formats. Lua patterns make it straightforward to validate email addresses, phone numbers, and other structured text without pulling in external libraries.
Validating input
Pattern matching is excellent for validation:
function is_valid_email(email)
-- Simple pattern: something@something.something
return string.match(email, "[^@]+@[^@]+%.[^@]+") ~= nil
end
function is_phone_number(phone)
-- Accepts: 123-456-7890 or (123) 456-7890
return string.match(phone, "%(?%d% d%d%d%)?%s*%d%d%d-%d%d%d%d") ~= nil
end
print(is_valid_email("test@example.com")) -- true
print(is_valid_email("invalid")) -- false
print(is_phone_number("123-456-7890")) -- true
Validation patterns often need to match literal characters that also have special meaning in Lua’s pattern syntax. The period, dollar sign, caret, and percent sign are all magic characters, so when your data contains them literally, you must escape them properly before including them in a pattern.
Escaping magic characters
When you need to match a literal magic character, escape it with %:
local text = "Price: $99.99"
-- Match literal dollar sign
local price = string.match(text, "$%d+%.%d+")
print(price) -- $99.99
-- Match literal period
local version = string.match("lua5.4", "5%.%d")
print(version) -- 5.4
All the individual techniques covered so far come together when you tackle real-world parsing tasks. The following example shows how to combine character classes, repetition operators, and captures into a single pattern that extracts structured fields from semistructured log output.
Practical example: parsing log lines
Let’s build a log parser that extracts useful information:
function parse_log_line(line)
-- Pattern: [TIMESTAMP] LEVEL: Message
local timestamp, level, message = string.match(
line,
"%[(%d+:%d+:%d+)%]%s(%w+):%s(.+)"
)
return {
timestamp = timestamp,
level = level,
message = message
}
end
local log = "[14:32:15] ERROR: Connection refused"
local parsed = parse_log_line(log)
print(parsed.level) -- ERROR
print(parsed.message) -- Connection refused
Summary
Lua’s string capabilities cover most common text-processing needs:
#operator andstring.lenfor lengthstring.subfor extracting substringsstring.findfor searchingstring.gsubfor replacements- Patterns for flexible matching and extraction
- Captures to extract specific parts of matches
Pattern matching in Lua is simpler than regex but powerful enough for most tasks. Remember to escape magic characters (%, ., ^, $, etc.) when matching them literally.
Why Lua patterns are not regex
Lua deliberately avoids the full POSIX or PCRE regular-expression grammar. The reasons matter when you decide which tool to reach for. Patterns have no alternation operator, no backreferences inside a single match, and no quantifier on character classes longer than a single character. In exchange you get a tiny, predictable engine that lives entirely in the standard library, with no allocation surprises and no catastrophic backtracking. For most log lines, slug fragments, or field extractions in a config file, the trade is comfortably in your favor.
When patterns are not enough, the standard option is the lrexlib-pcre library, which exposes full PCRE through a familiar interface. Reach for it when you need lookaround, named captures, or alternation. For everything else, the built-in string.match, string.gmatch, and string.gsub keep dependencies small and behavior easy to reason about.
For a single-line refresher: %a is letter, %d is digit, %w is alphanumeric, %s is whitespace, and uppercase versions are the complement. Combine with +, *, ?, or - to control repetition, and remember that - is the lazy quantifier rather than a range marker.
Next steps
Pattern matching improves with practice. Try parsing log files or configuration data to build confidence with captures. The next tutorial covers modules and the require system, where you’ll learn to organize your pattern-matching utilities into reusable Lua modules.
See also
- string.find reference — Find substrings with string.find()
- string.gsub reference — replace patterns globally with string.gsub()
- Lua tables tutorial — the companion data-structure tutorial in this series