Skip to content

Strings

  • Declare strings with single quotes, double quotes, and long brackets
  • Concatenate with .. and measure length with #
  • Use the string library: find, match, gsub, format, sub, rep, upper, lower
  • Understand why Lua strings are immutable byte arrays
  • Write test-driven specifications for string-processing functions

Create strings_test.lua:

local cyl = require('checkyour')
local describe, it, expect = cyl.describe, cyl.it, cyl.expect
cyl.parseargs()

Lua gives you three ways to write a string literal:

describe("string literals", function()
it("single and double quotes are interchangeable", function()
expect.equal('hello', "hello")
expect.equal(type('hello'), "string")
end)
it("long brackets can span multiple lines", function()
local poem = [[
line one
line two]]
expect.contains(poem, "line one")
expect.contains(poem, "line two")
end)
it("escape sequences work inside quoted strings", function()
expect.equal("tab:\there", "tab:\there")
expect.equal("newline:\n!", "newline:\n!")
expect.equal("quote: \"hi\"", 'quote: "hi"')
end)
end)

The .. operator concatenates strings. # returns the byte length.

describe("concatenation and length", function()
it(".. joins two strings", function()
expect.equal("Hello" .. ", " .. "world!", "Hello, world!")
end)
it(".. coerces numbers to strings", function()
expect.equal("value: " .. 42, "value: 42")
end)
it("# returns byte length, not character count", function()
expect.equal(#"Lua", 3)
expect.equal(#"", 0)
expect.equal(#"hello!", 6)
end)
it("repeated concatenation produces the right result", function()
local parts = {}
for i = 1, 5 do
parts[i] = tostring(i)
end
expect.equal(table.concat(parts, "-"), "1-2-3-4-5")
end)
end)

Lua strings are objects with methods. You can call them as s:method() or string.method(s) — they’re equivalent.

describe("case conversion", function()
it("upper() uppercases ASCII", function()
expect.equal(("hello"):upper(), "HELLO")
expect.equal(string.upper("Lua"), "LUA")
end)
it("lower() lowercases ASCII", function()
expect.equal(("WORLD"):lower(), "world")
end)
it("case conversion does not mutate the original", function()
local s = "Lua"
local _ = s:upper()
expect.equal(s, "Lua") -- strings are immutable
end)
end)

string.sub(s, i, j) returns characters from index i to j (1-based, inclusive). Negative indices count from the end.

describe("sub", function()
it("extracts a substring by 1-based indices", function()
local s = "Hello, Lua!"
expect.equal(s:sub(1, 5), "Hello")
expect.equal(s:sub(8, 10), "Lua")
end)
it("negative indices count from the end", function()
expect.equal(("abcdef"):sub(-3), "def") -- last 3 chars
expect.equal(("abcdef"):sub(-3, -2), "de")
end)
it("out-of-range indices are clamped", function()
expect.equal(("abc"):sub(1, 100), "abc")
expect.equal(("abc"):sub(5), "") -- past the end → empty
end)
end)
describe("rep", function()
it("repeats a string n times", function()
expect.equal(("ab"):rep(3), "ababab")
expect.equal(("ha"):rep(0), "")
end)
it("supports a separator argument (Lua 5.2+)", function()
expect.equal(("na"):rep(4, "-"), "na-na-na-na")
end)
end)

Lua’s pattern language is a lightweight alternative to full PCRE regex. The key functions are string.find, string.match, string.gmatch, and string.gsub.

Returns the start and end positions of the first match, or nil.

describe("string.find", function()
it("returns start and end positions of a match", function()
local s, e = ("hello world"):find("world")
expect.equal(s, 7)
expect.equal(e, 11)
end)
it("returns nil when there is no match", function()
local result = ("hello"):find("xyz")
expect.equal(type(result), "nil")
end)
it("plain=true treats the pattern as a literal string", function()
-- Without plain=true, '.' is a wildcard
local s = ("1.2.3"):find(".", 1, true)
expect.equal(s, 2)
end)
end)

Returns the captured text (or the whole match if no captures are defined).

describe("string.match", function()
it("returns the matched substring", function()
expect.equal(("2024-05-17"):match("%d+"), "2024")
end)
it("returns nil on no match", function()
expect.equal(type(("abc"):match("%d+")), "nil")
end)
it("captures with () return multiple values", function()
local year, month, day = ("2024-05-17"):match("(%d+)-(%d+)-(%d+)")
expect.equal(year, "2024")
expect.equal(month, "05")
expect.equal(day, "17")
end)
end)

Iterates over all matches. Useful for tokenizing.

describe("string.gmatch", function()
it("iterates over all matches", function()
local words = {}
for w in ("one two three"):gmatch("%a+") do
table.insert(words, w)
end
expect.equal(words, {"one", "two", "three"})
end)
it("can extract key=value pairs", function()
local pairs_found = {}
for k, v in ("a=1, b=2, c=3"):gmatch("(%a+)=(%d+)") do
pairs_found[k] = tonumber(v)
end
expect.equal(pairs_found.a, 1)
expect.equal(pairs_found.b, 2)
expect.equal(pairs_found.c, 3)
end)
end)

Global substitution. Returns a new string and a count of replacements.

describe("string.gsub", function()
it("replaces all occurrences by default", function()
local result, count = ("banana"):gsub("a", "o")
expect.equal(result, "bonono")
expect.equal(count, 3)
end)
it("limits replacements with the third argument", function()
local result = ("aaa"):gsub("a", "b", 2)
expect.equal(result, "bba")
end)
it("can use a function as the replacement", function()
local result = ("hello world"):gsub("%a+", string.upper)
expect.equal(result, "HELLO WORLD")
end)
it("can use a table as the replacement map", function()
local t = { name = "Lua", version = "5.5" }
local template = "Welcome to $name $version"
local result = template:gsub("%$(%a+)", t)
expect.equal(result, "Welcome to Lua 5.5")
end)
end)

Lua’s string.format works like printf in C.

describe("string.format", function()
it("formats integers and floats", function()
expect.equal(string.format("%d", 42), "42")
expect.equal(string.format("%.2f", 3.14159), "3.14")
end)
it("pads with spaces or zeros", function()
expect.equal(string.format("%5d", 42), " 42")
expect.equal(string.format("%05d", 42), "00042")
expect.equal(string.format("%-5d|", 42), "42 |")
end)
it("formats strings with %s", function()
expect.equal(string.format("Hello, %s!", "Lua"), "Hello, Lua!")
end)
it("formats hex with %x", function()
expect.equal(string.format("%x", 255), "ff")
expect.equal(string.format("%X", 255), "FF")
end)
end)

Lua strings are interned and immutable. Every “modification” creates a new string.

describe("immutability", function()
it("string operations return new strings", function()
local original = "hello"
local upper = original:upper()
expect.equal(original, "hello") -- unchanged
expect.equal(upper, "HELLO")
end)
it("the same string literal is the same object", function()
-- Lua interns strings; identity test with rawequal
local a = "intern me"
local b = "intern me"
expect.truthy(rawequal(a, b))
end)
end)

Let’s write a small utility function using the full TDD cycle. The goal: a trim function that strips leading and trailing whitespace.

Red — write the test first:

describe("trim", function()
it("removes leading and trailing spaces", function()
expect.equal(trim(" hello "), "hello")
end)
it("handles strings with no whitespace", function()
expect.equal(trim("hello"), "hello")
end)
it("handles an all-whitespace string", function()
expect.equal(trim(" "), "")
end)
end)

Running now gives attempt to call a nil value (global 'trim'). Good — that’s red.

Green — implement the minimum code:

function trim(s)
return s:match("^%s*(.-)%s*$")
end

Refactor — make it a proper module function:

local M = {}
function M.trim(s)
return s:match("^%s*(.-)%s*$")
end
return M

Update the test to local trim = require('stringutils').trim. Tests still green. Cycle complete.


  • Lua has three string literal forms; long brackets handle multiline content cleanly.
  • .. concatenates; # measures byte length (not Unicode character count).
  • Strings are immutable — all operations return new strings.
  • The string library covers find, match, gmatch, gsub, sub, rep, format, upper, lower.
  • Lua patterns are not PCRE; they’re a lighter alternative worth learning on their own terms.
  • table.concat is the right tool for building strings from many pieces.

Continue to Numbers and Math to explore Lua’s numeric tower and math library through tests.