luaguides

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 when plain is true).
  • init , optional 1-based index where the search starts (defaults to 1). Negative values count from the end.
  • plain , optional boolean. When true, 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.

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; pass plain = true for a literal dot.
  • A failed match returns nil, not 0 , always test the first return value.

See also