Step 1 of 5 · Reading · ~3 min
Learn
Strings
String Patterns and Matching
You have already used one of these without being told what it was. Back in
String Basics, string.gmatch handed you one word per call, and the thing that
told it what a word is looked like this:
print(("to be or not"):match("%S+"))
--> to
match returns the first thing the pattern finds; gmatch is the same search, repeated so that each call hands back the next match.
"%S+" is a Lua pattern. It is not a regular expression — Lua ships its own
smaller notation, and the differences bite hardest when you assume otherwise.
Character classes
A class matches one character. Upper-case negates it.
| Class | Matches |
|---|---|
%d | a digit |
%a | a letter |
%s | whitespace |
%S | anything that is not whitespace |
%w | a letter or digit |
%p | punctuation |
+ after a class means "one or more". So %d+ is a run of digits and %S+ is a
run of non-space characters — a word:
print(("order 42 shipped"):match("%d+"))
print(("ab12"):match("%a+"), ("ab12"):match("%w+"))
print(("a1b22c"):gsub("%d", "#"))
--> 42
--> ab ab12
--> a#b##c 3
%a+ stops at the 1 because a digit is not a letter; %w+ keeps going because
it is. And notice gsub returned two values — the new string and how many
substitutions it made.
Captures
Parentheses capture, and match returns one value per capture:
local y, m = ("2026-07-27"):match("(%d+)-(%d+)")
print(y, m)
--> 2026 07
Counting matches with gsub
gsub returns the new string and then the number of substitutions. Assign both, and use _ as the name for a value you do not want:
local _, runs = ("a1b22c333"):gsub("%d+", "")
print(runs)
--> 3
Replacing each match with an empty string deletes it, so the count is how many matches there were. A single local t = s:gsub(...) keeps only the first value and drops the count.
The magic characters
^ $ ( ) % . [ ] * + - ? mean something in a pattern. To match one literally,
escape it with % — not with a backslash:
print(("a.b"):find("."))
print(("a.b"):find(".", 1, true))
print(("a.b"):match("%."))
--> 1 1
--> 2 2
--> .
The first line is the trap. . matches any character, so it found position 1.
The second passes true as find's fourth argument — plain text search, no
pattern. The third escapes the dot properly.
What Lua patterns do NOT have
This is the part that catches people arriving from other languages. There is no alternation:
print(("abc"):match("a|b"))
--> nil
No error, no warning — a|b is read as the literal three characters a, |,
b, which simply are not in "abc". Silence, not a crash. ? does work, for an
optional single character:
print(("color"):match("colou?r"), ("colour"):match("colou?r"))
--> color colour
Anchors
^ at the start means "must match here"; $ at the end means "must reach the
end".
print(("hello"):match("^h"), ("hello"):match("o$"))
--> h o
Common mistakes
- Assuming regex syntax works — no
|, no\d, no{2,3}. Classes are%dstyle and there is no alternation. - Escaping with a backslash — Lua escapes pattern magic with
%, so a literal dot is%., not\.. - Forgetting
findreturns positions — it gives you start and end indices;matchgives you the text. - Ignoring
gsub's second return value — it is the substitution count, and it is often exactly the number you wanted to compute. - Using
.when you meant a dot — it matches everything, so the bug looks like "my pattern is too greedy".
Your exercise
Line Fields reads one line and prints three fields joined by |: the first run of digits, how many words the line has, and the line with its punctuation removed. The starter reads the line; you write the three lookups and the print. Watch the first field on a1b22c: the first run of digits is 1, not 122.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…