Lua String Patterns

Pattern Classes

Character classes (not regex — Lua patterns)
-- Lua patterns are NOT regular expressions
-- No alternation (|), no grouping for repetition, no backreferences

-- Character classes:
-- .   any character
-- %a  letters            %A  non-letters
-- %d  digits             %D  non-digits
-- %l  lowercase          %L  non-lowercase
-- %u  uppercase          %U  non-uppercase
-- %w  alphanumeric       %W  non-alphanumeric
-- %s  whitespace         %S  non-whitespace
-- %p  punctuation        %P  non-punctuation
-- %c  control chars      %C  non-control

-- Quantifiers:
-- *   zero or more (greedy)
-- +   one or more (greedy)
-- -   zero or more (lazy)
-- ?   zero or one

string.find and string.match

Finding and extracting
local log = "2026-04-10 14:30:22 ERROR auth failed from 10.50.1.99"

-- find returns start, end positions
local s, e = string.find(log, "ERROR")
print(s, e)                              -- 21, 25

-- find with captures
local ip_start, ip_end, ip = string.find(log, "(%d+%.%d+%.%d+%.%d+)")
print(ip)                                -- 10.50.1.99

-- match returns captures only
local date = string.match(log, "(%d+%-%d+%-%d+)")
print(date)                              -- 2026-04-10

-- Multiple captures
local year, month, day = string.match(log, "(%d+)-(%d+)-(%d+)")
print(year, month, day)                  -- 2026  04  10

-- Plain text search (no pattern interpretation)
string.find(log, "10.50", 1, true)       -- plain flag = 4th arg

string.gmatch

Iterator for all matches
-- Extract all words
for word in string.gmatch(log, "%S+") do
  print(word)
end

-- Extract all IPs from multi-line text
local text = [[
  server 10.50.1.1
  dns    10.50.1.50
  vault  10.50.1.40
]]
local ips = {}
for ip in text:gmatch("(%d+%.%d+%.%d+%.%d+)") do
  ips[#ips + 1] = ip
end

-- Parse key=value pairs
local config_line = "timeout=30 retries=3 verbose=true"
local config = {}
for k, v in config_line:gmatch("(%w+)=(%w+)") do
  config[k] = v
end

string.gsub

Search and replace
-- Simple replacement
local clean = log:gsub("ERROR", "WARN")

-- Replace with limit
local first_only = log:gsub("%d", "X", 4)  -- replace first 4 digits

-- Replace with capture reference
local redacted = log:gsub("%d+%.%d+%.%d+%.%d+", "<REDACTED>")

-- Replace with function
local result = log:gsub("%d+", function(n)
  return tostring(tonumber(n) * 2)
end)

-- Replace with table lookup
local level_map = { ERROR = "ERR", WARNING = "WARN", INFO = "INF" }
local short = log:gsub("%u+", level_map)

-- gsub returns string AND replacement count
local output, count = log:gsub("%d", "X")
print(count)                             -- number of replacements

Practical Patterns

Infrastructure parsing
-- Parse syslog-style line
local function parse_syslog(line)
  local date, time, level, msg = line:match(
    "^(%d+%-%d+%-%d+)%s+(%d+:%d+:%d+)%s+(%u+)%s+(.+)$"
  )
  return { date = date, time = time, level = level, message = msg }
end

-- Validate IP address
local function is_valid_ip(s)
  local a, b, c, d = s:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
  if not a then return false end
  a, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d)
  return a <= 255 and b <= 255 and c <= 255 and d <= 255
end

-- Split string (Lua has no built-in split)
local function split(s, sep)
  local parts = {}
  for part in s:gmatch("([^" .. sep .. "]+)") do
    parts[#parts + 1] = part
  end
  return parts
end

local fqdn_parts = split("vault-01.inside.domusdigitalis.dev", ".")