Back

Bonus: String Patterns

Optional — nothing in Unit 2 needs it, which is exactly what makes it a clean side trip.

divider

The Idea

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 %..
  • There is no alternation. No |, and no (abc)? on a group. Quantifiers apply to one character class only.
  • - is the lazy repeat, where regex writes *?. This one catches everybody.
ClassMatches
%da digit
%aa letter
%wa letter or digit
%swhitespace
%ppunctuation
.any character at all
%D %A %W %Sthe 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.

Pulling one thing out

Parentheses make a capture — the part you want back rather than just matched.

main.lua
local line = "score=1200 lives=3 name=Ada"
print(line:match("score=(%d+)"))
print(line:match("(%a+)=(%d+)"))
Output
1200
score 1200

Two 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.

Pulling out all of them

gmatch returns an iterator — the same contract from Activity 1.8 — so it goes straight into a for loop.

main.lua
local line = "score=1200 lives=3 name=Ada"
for key, value in line:gmatch("(%w+)=(%w+)") do
print(key, value)
end
Output
score 1200
lives 3
name Ada

Three 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.

Replacing, and the trim idiom

main.lua
local line = "score=1200 lives=3 name=Ada"
print((line:gsub("%d+", "#")))
local padded = " Skynest "
print("[" .. padded:match("^%s*(.-)%s*$") .. "]")
Output
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.

divider

Build

Start a fresh main.lua. You are parsing a line of save-file text.


Task 1: Capture

  • Store the line score=1200 lives=3 name=Ada.
  • Pull out just the score as digits.
  • Then write a pattern with two captures that gets a name and a number together.
main.lua
-- main.lua - string patterns
local line = "score=1200 lives=3 name=Ada"
print(line:match("score=(%d+)"))
print(line:match("(%a+)=(%d+)"))
Output
1200
score 1200

Task 2: All of them

  • Loop over every key=value pair with gmatch.
  • Use %w rather than %d for the value, so that Ada is caught as well as the numbers.
  • Try it with %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
-- 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
Output
1200
score 1200
score 1200
lives 3
name Ada

Task 3: Replace, trim, and split a date

  • Replace every run of digits with #, remembering the extra parentheses.
  • Trim a padded string with the anchored pattern, and print it inside brackets so you can see the edges.
  • Split 2026-08-06 into three captures in one call.
main.lua
-- 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+)"))

Challenge (Optional)

  • Turn the 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.
  • Write split(text, sep) returning a list, using gmatch. Lua has no built-in split, and this is why nobody misses it.
  • Pass a function as 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.
divider

Check Your Work

Output
1200
score 1200
score 1200
lives 3
name Ada
score=# 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 (.-).