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 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

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:

string.find("/tmp/file.lua", "/", -5)   --> 5   5

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.

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

-- 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