Skip to content

String patterns

Lesson 8.3

Patterns are a small language for searching text. With them you can check input, pull numbers out of a command, split a line into words, and replace text.

Where patterns are used

Four string functions take a pattern:

Function Does
s:find(p) the start and end position of the first match
s:match(p) the text of the first match (or its captures)
s:gmatch(p) an iterator over every match, for a for loop
s:gsub(p, repl) a copy with every match replaced, plus the number of replacements

In a pattern, most characters match themselves. ("diamond sword"):find("sword") finds the word sword. A few characters are special, and they make patterns powerful.

Not regular expressions

Lua patterns look like the regular expressions of other languages, but they are simpler and different. There is no | for “or”, no {2,3} counts, and the escape character is %, not \.

Character classes

A class matches one character of a kind:

Class Matches
. any character
%a a letter
%d a digit
%l / %u a lowercase / uppercase letter
%s a space, tab or line break
%w a letter or digit
%p punctuation
%x a hexadecimal digit

An uppercase version means the opposite: %D is any non-digit, %S any non-space.

print(("Level 42"):match("%d"))   --> 4
print(("a1b2"):gsub("%d", "#"))   --> a#b#	2

Sets

Square brackets build your own class. [abc] matches a, b or c. [a-z] is a range. [^...] means “anything except”:

print(("x=10;y=20"):gsub("[;=]", " "))  --> x 10 y 20	3
print(("minecraft:oak_log"):match("[^:]+$"))  --> oak_log

You can put classes inside a set: [%w_] matches a letter, digit or underscore, the characters of a Minecraft ID path.

Repetition

These follow a class and say how many times it may repeat:

Symbol Means
+ 1 or more, as many as possible
* 0 or more, as many as possible
- 0 or more, as few as possible
? 0 or 1 (optional)
print(("Level 42"):match("%d+"))       --> 42
print(("  hi  "):match("%s*(.-)%s*$")) --> hi
print(("-5"):match("^-?%d+$"))         --> -5

The difference between * and - matters with .: "<b>x</b>" matched with <.*> takes everything from the first < to the last >. <.-> stops at the first >.

print(("<b>x</b>"):match("<.*>"))  --> <b>x</b>
print(("<b>x</b>"):match("<.->"))  --> <b>

Anchors

^ at the start of a pattern means “must match at the beginning”. $ at the end means “must match at the end”. With both, the whole string must match. This is how you validate input:

local function is_valid_id(s)
  return s:match("^[%l%d_]+:[%l%d_/%.%-]+$") ~= nil
end

print(is_valid_id("minecraft:stone"))   --> true
print(is_valid_id("Minecraft:Stone"))   --> false
print(is_valid_id("stone"))             --> false

Escaping with %

To match a special character literally, put % before it. The special characters are ^ $ ( ) % . [ ] * + - ?:

print(("3.5"):find("%."))       --> 2	2
print(("50%"):match("%d+%%"))   --> 50%

Or turn patterns off completely in find with its fourth argument: s:find(".", 1, true).

Captures

Brackets in a pattern capture the part they match. match then returns the captures instead of the whole match:

local cmd = "/give Alex diamond 5"
local who, item, count = cmd:match("^/give (%S+) (%S+) (%d+)$")
print(who, item, tonumber(count))  --> Alex	diamond	5

Captures are always strings, so convert numbers with tonumber. If the pattern doesn’t match, match returns nil, which makes it a check and a parser at once:

local x, y, z = ("10 64 -3"):match("^(-?%d+) (-?%d+) (-?%d+)$")
if x then
  print(tonumber(x), tonumber(y), tonumber(z))  --> 10	64	-3
end

An empty capture () returns a position instead of text: ("hello"):match("()ll") is 3.

Looping over matches: gmatch

gmatch returns an iterator. Each round of the loop gives the next match:

for word in ("dig, build, fight"):gmatch("%a+") do
  print(word)
end
dig
build
fight

With captures, each round gives all the captures. Here it parses key=value pairs:

local settings = {}
for key, value in ("pvp=true difficulty=hard"):gmatch("(%w+)=(%w+)") do
  settings[key] = value
end
print(settings.pvp, settings.difficulty)  --> true	hard

A common split by a separator: s:gmatch("[^,]+") gives every piece between commas.

Replacing: gsub

gsub(pattern, repl) replaces every match. repl can be:

  • a string, where %1, %2 refer to captures and %0 to the whole match,
  • a table, where the match is looked up as a key,
  • a function, which is called with the captures and returns the new text.
print(("oak_log"):gsub("_", " "))                  --> oak log	1
print(("Alex Kai"):gsub("(%a+)", "<%1>"))           --> <Alex> <Kai>	2

local colours = { red = "§c", gold = "§6" }
print(("{red}Hi {gold}there"):gsub("{(%a+)}", colours))  --> §cHi §6there	2

print(("diamond sword"):gsub("(%a)(%a*)", function(first, rest)
  return first:upper() .. rest
end))  --> Diamond Sword	2

If the function or table gives nil or false, the match stays as it was. A fourth argument limits the number of replacements: s:gsub("a", "b", 1) replaces only the first.

gsub returns two values. When you only want the string, wrap the call in brackets, (s:gsub(...)), or assign it to one variable.

Two special items

  • %b() matches a balanced pair: an opening (, everything up to the matching ). Any two characters work: %b{}, %b[].
  • %f[set] is a frontier: it matches the empty spot where the previous character is not in the set and the next one is. %f[%w]%w+ finds whole words.
print(("f(a(b)c) d"):match("%b()"))                 --> (a(b)c)
print(("cat concat cat"):gsub("%f[%w]cat%f[%W]", "dog"))  --> dog concat dog	2

Patterns and UTF-8

Patterns work on bytes. . and %a match one byte, so letters such as é or ß (two bytes each) don’t count as %a. To loop over real characters, use utf8.charpattern:

for ch in ("Café"):gmatch(utf8.charpattern) do
  io.write(ch, " ")
end
print()
C a f é 

Try it

Parse a chat command like "/tp 100 64 -30" into three numbers, returning nil when it doesn’t fit. Then write snake_to_title("deepslate_diamond_ore") that returns "Deepslate Diamond Ore" using gsub twice.

Show a solution
local function parse_tp(text)
  local x, y, z = text:match("^/tp (-?%d+) (-?%d+) (-?%d+)$")
  if not x then
    return nil
  end
  return tonumber(x), tonumber(y), tonumber(z)
end

print(parse_tp("/tp 100 64 -30"))  --> 100	64	-30
print(parse_tp("/tp here"))        --> nil

local function snake_to_title(s)
  s = s:gsub("_", " ")
  return (s:gsub("(%a)(%a*)", function(a, b) return a:upper() .. b end))
end

print(snake_to_title("deepslate_diamond_ore"))  --> Deepslate Diamond Ore

s = s:gsub(...) keeps only the first result. The brackets in the return do the same, so the function returns just the string.

Recap: patterns use classes like %d and %a, repeats like + and -, anchors ^ and $, and captures in brackets, with match, gmatch and gsub doing the work.