Skip to content
String Patterns and Matching
step 1/5

Reading — step 1 of 5

Learn

~3 min readStrings

String Patterns and Matching

You have already used one of these without being told what it was. Back in String Basics, splitting a line into words looked like this:

for w in ("to be  or not"):gmatch("%S+") do io.write(w, " ") end

--> to be or not

"%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.

ClassMatches
%da digit
%aa letter
%swhitespace
%Sanything that is not whitespace
%wa letter or digit
%ppunctuation

+ 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

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 %d style and there is no alternation.
  • Escaping with a backslash — Lua escapes pattern magic with %, so a literal dot is %., not \..
  • Forgetting find returns positions — it gives you start and end indices; match gives 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 a count, then that many lines, and prints one summary line each in the form <first-number>|<word-count>|<punctuation-stripped>.

The first number is the first run of digits — none when the line has none. The word count is how many %S+ runs the line contains. The stripped text is the line with every %p character removed.

order 42, shipped! therefore gives 42|3|order 42 shipped. 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…

String Patterns and Matching — Lua Fundamentals