string.sub
string.sub(s, i [, j]) Overview
string.sub returns a slice of a string between two 1-based byte positions. The bounds are inclusive on both ends, and negative indices count back from the last byte: -1 is the final byte, -2 is the one before it, and so on. The result is always a fresh string, since Lua strings are immutable, and that includes the empty string when the slice collapses to nothing.
The manual’s own summary is the cleanest way to remember the function:
Returns the substring of
sthat starts atiand continues untilj. Ifjis absent, then it is assumed to be equal to-1(which is the same as the string length). In particular, the callstring.sub(s,1,j)returns a prefix ofswith lengthj, andstring.sub(s, -i)(for a positivei) returns a suffix ofswith lengthi.
The two boundary conditions that catch newcomers are inclusive bounds and byte-level indexing. string.sub("abc", 1, 1) is "a", not "", and string.sub("é", 1, 1) is the first byte of the two-byte UTF-8 sequence, not the character é. For character-aware slicing of UTF-8 text, use utf8.sub from the standard utf8 library.
Signature
The function form and the method form are exactly equivalent. The colon form goes through the same code path via the string metatable.
string.sub(s, i [, j]) -- canonical form
s:sub(i [, j]) -- method form
Parameters
string.sub takes one required string and two integer indices, the second of which is optional.
| # | Name | Type | Default | Description |
|---|---|---|---|---|
| 1 | s | string | required | The source string. Non-string types raise bad argument #1 to 'sub' (string expected, got <type>). There is no number-to-string coercion, unlike string.len. |
| 2 | i | integer | required | Start position, 1-based. Negative values count back from the end: -1 is the last byte. Float values with an exact integer representation are accepted in Lua 5.3+. |
| 3 | j | integer | -1 (last byte) | End position, inclusive. Same 1-based and negative-index rules as i. Defaults to -1, which is the last byte. |
After negative-index translation, both bounds go through a clamping pass: i is lifted to 1 if it ended up below 1, and j is dropped to #s if it ended up above #s. The result of that correction is what actually gets sliced.
Return Value
A string. The slice is s[i'..j'] where i' and j' are the post-correction bounds. Three cases collapse to the empty string:
i' > j'after clamping (the requested range is inverted).- The source string itself is
"". - The corrected range is empty for any other reason.
Out-of-range bounds are silent. string.sub("abc", 50, 100) returns "" rather than raising, which is by design: it makes string.sub safe to call with offsets produced by string.find even when the search wrapped past the end of the string. Lua strings are immutable, so the result is always a freshly allocated string, never a view into s.
Examples
The examples below assume a default Lua 5.4 build. Run them in lua -i to see the printed output.
Basic positive indices
print(string.sub("Hello Lua user", 7)) --> Lua user
print(string.sub("Hello Lua user", 7, 9)) --> Lua
print(string.sub("Hello Lua user", 1, 5)) --> Hello
print(string.sub("Hello Lua user", 1, 1)) --> H
The last call is the inclusive-bound gotcha: a single-byte slice is still a one-character string, not the empty string. Python users sometimes expect the end index to be exclusive and write s:sub(1, j - 1) out of habit; do not do that here.
Negative indices
Negative bounds resolve to #s + i + 1 (and the same for j) before the clamping pass runs. For a length-12 string, -1 becomes 12, -2 becomes 11, -8 becomes 5.
print(string.sub("Hello Lua user", -1)) --> r
print(string.sub("Hello Lua user", -3)) --> ser
print(string.sub("Hello Lua user", -8, -6)) --> Lua
print(string.sub("Hello Lua user", -8, 9)) --> Lua
The middle two lines are worth a closer look. -3 to the end takes the last three bytes, "ser". -8 to -6 takes the 5th through 7th bytes from the end, which happen to be "Lua". The fourth line mixes a negative start with a positive end, and both translate to the same 5..9 range.
Prefix and suffix idioms
The manual’s two canonical calls are string.sub(s, 1, j) for a prefix of length j and string.sub(s, -i) for a suffix of length i. The suffix form is the workhorse of file-extension and “is this a Lua file?” checks.
local s = "config.lua"
print(string.sub(s, 1, 6)) --> config
print(string.sub(s, -4)) --> .lua
print(string.sub(s, -#s)) --> config.lua (full string, via negative length)
print(string.sub(s, 1, -2)) --> config.lu (drop last byte)
The string.sub(s, 1, -2) form is the common “trim last character” idiom and shows up in CSV/JSON helpers. To trim both first and last bytes, slice [2, -2]:
local function trim_ends(s) return s:sub(2, -2) end
print(trim_ends("[payload]")) --> payload
print(trim_ends("<>")) --> (empty string)
Clamping and out-of-range bounds
Both indices are silently clamped to [1, #s]. Overshooting in either direction does not error, which is convenient for slicing around string.find results that might wrap past the end.
print(string.sub("abc", -100, 100)) --> abc (i clamped to 1, j clamped to 3)
print(string.sub("abc", 0, 2)) --> ab (i = 0 clamped to 1)
print(string.sub("abc", 2, 1)) --> (empty) (i > j after correction)
print(string.sub("abc", 1, 0)) --> (empty) (i = 1 > j = 0)
print(string.sub("", 1, 1)) --> (empty) (empty source)
print(string.sub("abc", 1.0, 2.0)) --> ab (integral floats accepted)
The string.sub("abc", 1, 0) case is the most counterintuitive: j = 0 is below #s = 3, so the upper clamp does not move it, and the final range 1 > 0 is empty. This is a real footgun when refactoring code that mixes zero-based and one-based indexing.
Bytes, not characters
string.sub indexes bytes. For ASCII text this is invisible. For UTF-8 text it can split a multibyte code point in half and return invalid UTF-8.
print(string.sub("héllo", 1, 1)) --> h
print(string.sub("héllo", 2, 2)) --> <first byte of é, 0xC3>
print(string.sub("héllo", 1, 2)) --> <h plus first byte of é, invalid UTF-8>
-- Character-aware slicing via the utf8 library (Lua 5.3+)
print(utf8.sub("héllo", 1, 1)) --> h
print(utf8.sub("héllo", 2, 2)) --> é
print(utf8.sub("héllo", 2, 4)) --> éll
utf8.sub follows the same signature but counts code points rather than bytes. If your input might contain non-ASCII text, reach for it. Embedded NULs are fine in the byte view: string.sub("a\0b", 2, 2) is "\0", a one-byte string.
Method form
local s = "Lua 5.4"
print(s:sub(1, 3)) --> Lua
print(s:sub(-3)) --> 5.4
print(("hi"):sub(2)) --> i
print(("hi"):sub(2, 2)) --> i
-- "hi":sub(2) -- syntax error: method call on a string literal needs parens
A bare method call on a string literal is a parse error in Lua. Wrap the literal in parentheses: ("hi"):sub(2). The method form itself is just sugar over string.sub via the string metatable that the string library installs.
Pairing with string.find
string.find returns the start and end positions of a match as byte offsets, and the end is inclusive. The natural way to extract the match is to feed those offsets straight back into string.sub.
local s = "version = 1.2.3-rc1"
local key, val = s:match("(%w+)%s*=%s*(%S+)")
print(key, val) --> version 1.2.3-rc1
-- For a plain substring search, the end offset is inclusive:
local start, finish = string.find(s, "1.2.3")
print(start, finish) --> 11 15
print(string.sub(s, start, finish)) --> 1.2.3
-- NOT this:
print(string.sub(s, start, finish - 1)) --> 1.2. (off by one)
The last two lines show the classic off-by-one bug. string.find returns inclusive offsets, so the natural pair is s:sub(start, finish), not s:sub(start, finish - 1).
Extracting a file extension
A short, idiomatic helper built on string.find and string.sub.
local function extension(name)
local dot = name:find("%.[^.]*$")
if not dot then return "" end
return name:sub(dot + 1)
end
print(extension("readme.md")) --> md
print(extension("archive.tar.gz")) --> gz
print(extension("Makefile")) --> (empty) (no dot)
print(extension(".bashrc")) --> bashrc (leading dot is excluded)
The pattern %.[^.]*$ matches the last . and everything after it, anchored to end-of-string. string.find gives the position of the dot; string.sub takes everything after it.
Common Pitfalls
- Indices are inclusive on both ends.
string.sub("abc", 1, 1)is"a", not"". Python users expect the end to be exclusive; Lua does not. - Indices are 1-based, never 0.
string.sub("abc", 0, 1)is"a"(clamped to1..1), andstring.sub("abc", 1, 0)is""becausej = 0is not above#sand1 > 0after correction. Off-by-one bugs here are common when porting from other languages. - Bytes, not characters. Slicing a UTF-8 string can split a multibyte code point in half and return invalid UTF-8. Use
utf8.subfor character-aware slicing. jdefaults to-1, not#s. That is a convenient shorthand for “to the end”, but if you want to slice up to the byte before the last one, you need to passjexplicitly as#s - 1or-2.- Out-of-range is silent, not an error.
string.sub("abc", 50, 100)returns"". That is by design and makesstring.subsafe withstring.findoffsets that wrapped past the end, but it also means bugs in your index math produce empty results, not loud failures. i > jafter correction returns"". Tests that checkresult ~= nilwill not catch an unintended empty substring. Check the length or the bounds explicitly.- Negative indices resolve via
#s + i + 1. For a length-3 string,-1becomes 3,-2becomes 2,-3becomes 1,-4becomes 0 (then clamped to 1). The-#scase resolves to1, which is whystring.sub(s, 1, -#s)returns the full string. - No auto-coercion of
sfrom numbers.string.sub(42, 1, 1)raisesbad argument #1 to 'sub' (string expected, got number). Unlikestring.len, this function does not coerce numbers. Pass numbers throughtostringfirst. s:sub(...)on a string literal needs parens."abc":sub(2)is a parse error. Use("abc"):sub(2). The string metatable is the reason the method form works at all on plain values.- Strings are immutable;
string.subreturns a fresh string. There is no in-place variant. Assigning back (s = s:sub(...)) is the only way to “truncate”. string.findoffsets are end-inclusive. The natural pair iss:sub(start, finish), nots:sub(start, finish - 1). The off-by-one here is one of the most common Lua substring bugs.
See Also
string.byte— read individual bytes by index, the natural pair for inspecting a slice.string.char— the inverse direction: bytes back into a string.string.find— returns inclusive byte offsets in the same coordinate system; the canonical feeder forstring.subarguments.string.format— for fixed-width padding (%10s,%-10s), often a substitute forstring.subwhen you want to align output.string.gsub— substitute matches;string.subis the right tool for slicing around a match instead of replacing it.string.len—#s, the natural upper bound when slicing relative to the end.string.match— extract captures directly from a pattern, often instead ofstring.findplusstring.sub.string.rep— repeats a string; complementary when building repeated output.- Strings and Patterns tutorial — beginner walkthrough of the string library.
- Pattern Matching tutorial — patterns use the same byte indices.
- Lua String Patterns guide — pattern cookbook.
- Working with binary data in Lua — where the byte semantics really matter.