Optional — nothing in Unit 2 needs it, which is exactly what makes it a clean side trip.
Lua has its own miniature language for describing shapes of text. It is not regular expressions. It is smaller, it is about two hundred lines of C rather than a library, and it covers most of what people actually use regex for.
The differences that matter, up front, so you stop trying to write regex:
% is the escape, not \\. Digits are %d, and a literal dot is %..|, and no (abc)? on a group. Quantifiers apply to one character class only.- is the lazy repeat, where regex writes *?. This one catches everybody.| Class | Matches |
|---|---|
%d | a digit |
%a | a letter |
%w | a letter or digit |
%s | whitespace |
%p | punctuation |
. | any character at all |
%D %A %W %S | the opposite of the lowercase one |
And four repeat markers: * none or more, + one or more, ? none or one, and - none or more but as few as possible.
Parentheses make a capture — the part you want back rather than just matched.
local line = "score=1200 lives=3 name=Ada"
print(line:match("score=(%d+)"))print(line:match("(%a+)=(%d+)"))1200score 1200Two captures return two values, which is Activity 1.7's multiple returns again. With no captures at all, match returns the whole matched text; with no match, it returns nil, which makes it usable directly in an if.
Note the second pattern found only the first name=number pair. match stops at the first hit.
gmatch returns an iterator — the same contract from Activity 1.8 — so it goes straight into a for loop.
local line = "score=1200 lives=3 name=Ada"
for key, value in line:gmatch("(%w+)=(%w+)") do print(key, value)endscore 1200lives 3name AdaThree lines to parse a configuration format. This is the pattern you would reach for reading a save file, a level definition, or anything else stored as plain text.
local line = "score=1200 lives=3 name=Ada"
print((line:gsub("%d+", "#")))
local padded = " Skynest "print("[" .. padded:match("^%s*(.-)%s*$") .. "]")score=# lives=# name=Ada[Skynest]The extra parentheses around the gsub are deliberate. It returns two values — the new string and how many replacements it made — and wrapping the call in parentheses throws all but the first away. Without them you would also print the count.
The second line is the standard Lua trim, and it is worth taking apart because every piece earns its place:
^ and $ anchor it to the whole string.%s* eats the spaces at each end.(.-) captures everything between, as little as possible. Use (.*) instead and the greedy match swallows the trailing spaces, leaving them in your result.Where patterns run out. Anything with nesting — HTML, JSON, matched brackets — is not something patterns can do, and neither can regex. If you find yourself building a pattern with five captures and a lot of %p, stop and write a loop.
Start a fresh main.lua. You are parsing a line of save-file text.
score=1200 lives=3 name=Ada.-- main.lua - string patterns
local line = "score=1200 lives=3 name=Ada"
print(line:match("score=(%d+)"))print(line:match("(%a+)=(%d+)"))1200score 1200key=value pair with gmatch.%w rather than %d for the value, so that Ada is caught as well as the numbers.%d first and watch a line go missing. That is the whole skill: the pattern describes what you expect, and anything you did not expect silently does not appear.-- main.lua - string patterns
local line = "score=1200 lives=3 name=Ada"
print(line:match("score=(%d+)"))print(line:match("(%a+)=(%d+)"))
for key, value in line:gmatch("(%w+)=(%w+)") do print(key, value)end1200score 1200score 1200lives 3name Ada#, remembering the extra parentheses.2026-08-06 into three captures in one call.-- main.lua - string patterns
local line = "score=1200 lives=3 name=Ada"
print(line:match("score=(%d+)"))print(line:match("(%a+)=(%d+)"))
for key, value in line:gmatch("(%w+)=(%w+)") do print(key, value)end
print((line:gsub("%d+", "#")))
local padded = " Skynest "print("[" .. padded:match("^%s*(.-)%s*$") .. "]")
print(("2026-08-06"):match("(%d+)-(%d+)-(%d+)"))gmatch loop into one that builds a table, so you end up with config.score and config.name. Run the values through tonumber where they are numeric. You have just written a save-file loader.split(text, sep) returning a list, using gmatch. Lua has no built-in split, and this is why nobody misses it.gsub's third argument instead of a string. It gets called with each match and its return value is used as the replacement — so you can double every number in a string in one line.1200score 1200score 1200lives 3name Adascore=# lives=# name=Ada[Skynest]2026 08 06[Skynest] with no spaces inside the brackets. If yours has trailing spaces, you used (.*) where the pattern needed (.-).