string.find
string.find(s, pattern [, init [, plain]]) string.find searches a string for the first match of a Lua pattern (or a literal substring when plain is true) and returns the start and end indices of the match. If the pattern contains captures, those captures are returned after the indices. When nothing matches, string.find returns nil.
Signature
local start_idx, end_idx, ...captures = string.find(s, pattern [, init [, plain]])
s, the subject string to search.pattern, a Lua pattern (or plain text whenplainis true).init, optional 1-based index where the search starts (defaults to1). Negative values count from the end.plain, optional boolean. Whentrue, the pattern is treated as a literal string and pattern-matching is disabled.
Basic usage
local s = "hello world"
print(string.find(s, "world")) --> 7 11
print(string.find(s, "lua")) --> nil
The two integers are inclusive 1-based indices: the substring s:sub(7, 11) is the match. When the search fails, string.find returns nil — not zero, not an empty string. This means you can test the first return value directly in an if condition without worrying about falsy-but-valid results like position 1 being confused with a failure.
Plain (non-pattern) search
When you want a literal lookup, pass plain = true. This avoids surprises with the magic characters ( ) . % + - * ? [ ] ^ $.
local path = "config.lua"
print(string.find(path, ".", 1, true)) --> 7 7 (the dot)
print(string.find(path, ".")) --> 1 1 (matches any char)
Starting offset
Setting plain = true is essential whenever you are looking for a literal piece of text that happens to contain pattern characters. File paths, URLs, and user-provided input are the most common offenders — a single dot or hyphen can silently change the match semantics if plain stays at its default. With the flag turned on, string.find does a straightforward byte-by-byte substring search, which is both predictable and faster than the pattern-matching engine.
The init argument lets you continue searching after a previous hit:
local s = "ab-cd-ef"
local i, j = string.find(s, "-")
while i do
print(i, j)
i, j = string.find(s, "-", j + 1)
end
-- 3 3
-- 6 6
Negative init counts from the end. When you pass a negative starting position, Lua adds the string length to it before beginning the search — so -1 maps to the last character, -5 to the fifth from the end, and so forth. This is the same convention used by string.sub and string.byte, making the behaviour easy to remember once you are familiar with Lua’s string indexing. If the absolute value of the offset exceeds the string length, the search starts from position 1 instead.
string.find("/tmp/file.lua", "/", -5) --> 5 5
Negative offsets are especially useful when you are searching for delimiters that appear predictably near the end of a string — file extensions, trailing slashes, or the last segment of a path. Rather than scanning the whole string from position 1, you can jump straight to the tail and let string.find locate the target in a handful of byte comparisons. Combined with a loop that advances init, you can walk backwards through a string without reversing it in memory first.
Captures in the return
When the pattern includes () captures, those captures follow the start/end indices:
local date = "2026-05-19"
local i, j, y, m, d = string.find(date, "(%d+)-(%d+)-(%d+)")
print(i, j, y, m, d) --> 1 10 2026 05 19
If you only need the captures and don’t care about positions, use string.match instead — it returns the captures directly without the index pair. That said, string.find with captures is still the right tool when you need both the span of the match (for highlighting or replacement) and the captured sub-patterns extracted in a single pass.
Method syntax
Because the string library is set as the metatable for strings, you can call string.find as a method:
local i, j = ("hello"):find("ll")
print(i, j) --> 3 4
Common patterns
The method form s:find(...) is equivalent to string.find(s, ...) and is the preferred style in most Lua codebases — it reads left-to-right and chains naturally with other string methods. Under the hood, Lua dispatches the find call through the string metatable, so there is no overhead compared to the library-function form.
-- Check whether a substring exists
if msg:find("ERROR", 1, true) then
io.write("alert\n")
end
-- Split-by-delimiter loop using string.find
local function split(s, sep)
local out, prev = {}, 1
while true do
local i, j = s:find(sep, prev, true)
if not i then
out[#out + 1] = s:sub(prev)
return out
end
out[#out + 1] = s:sub(prev, i - 1)
prev = j + 1
end
end
Gotchas
- Indices are 1-based and inclusive on both ends, unlike many languages.
- The dot (
.) in a pattern matches any character; passplain = truefor a literal dot. - A failed match returns
nil, not0, always test the first return value.
See also
- /reference/string-methods/ref-string-gsub/ , substitute pattern matches with a replacement.
- /tutorials/lua-fundamentals/strings-and-patterns/ , walkthrough of Lua’s pattern syntax.
- /guides/lua-string-patterns/ , deeper coverage of patterns and captures.