luaguides

string.rep

string.rep(s, n [, sep])

Overview

string.rep builds a new string by repeating its input a fixed number of times. It is the standard way to create padded or repeated output in Lua and ships with the language as part of the stdlib, so no extra module is required. The repetition happens in a single concatenation pass, which sidesteps the quadratic cost of building the result with .. inside a Lua loop. In Lua 5.3 and later the function also accepts a separator that gets placed between copies, useful for delimiter-joined repetition (CSV cells, ASCII dividers, repeated template fragments).

s:rep(n [, sep]) is exactly the same call as string.rep(s, n [, sep]), because the colon syntax passes s in as the first argument. Reach for the method form in chained expressions; it behaves identically to the dotted call in every Lua version that supports it.

Signature

string.rep(s, n [, sep])

Parameters

  • s (string) — the string to repeat. Any string value, including the empty string. Passing "" is valid; the result is empty when n <= 0 and is just the separators when n > 0.
  • n (integer) — number of copies. Must be a positive integer. When n is not positive (that is, n <= 0), the function returns the empty string instead of raising an error.
  • sep (string, optional) — separator inserted between copies of s. Defaults to "" (no separator). This argument was added in Lua 5.3. Lua 5.1 and 5.2 accept only the two-argument form string.rep(s, n).

Return Value

A string. For positive n without sep, the result is n copies of s concatenated end to end. With a positive n and a sep, the result is n copies of s joined by sep between them, and sep never appears at the start or end of the result. For non-positive n, the result is "".

Basic Examples

print(string.rep("ab", 3))
-- ababab

print(string.rep("ab", 3, "-"))
-- ab-ab-ab

print(string.rep("=", 10))
-- ==========

print(string.rep("ab", 2, "---"))
-- ab---ab

print(string.rep("ab", 1, "---"))
-- ab

Padding, ASCII dividers, and test fixtures are the bread-and-butter uses for the no-separator form. Pass a separator and the function turns into a tiny join utility, handy when you want a delimiter-joined repetition without writing a loop. Note that the leading and trailing characters of the result are always copies of s, never sep; the separator is spliced only between adjacent copies of the source string.

print(("ab"):rep(3, "x"))
-- abxabxab

print(("-"):rep(0))
-- (empty line)

The method form shines inside table constructors and in expression contexts where threading a string. prefix is awkward, or when the string is itself the result of a function call that needs parentheses anyway. Both forms go through the same C implementation, so there is no performance difference between s:rep(n) and string.rep(s, n).

Edge Cases

A non-positive n is silent: no error, just an empty result. Callers can pass the raw result of a subtraction or a count pulled from a table without first clamping it to a positive integer. Behavior is well-defined for the boundary value n = 0, which yields the empty string rather than a single copy.

local count = -3
print("[" .. string.rep("*", count) .. "]")
-- []

local count = 0
print("[" .. string.rep("*", count) .. "]")
-- []

The separator argument is only inserted between copies. With n = 1, it is ignored entirely. This is easy to overlook: passing a separator with a count of one does not pad the single copy, it just gets dropped on the floor. If you need single-copy padding, reach for string.format with a width specifier instead, since string.rep is the wrong tool for left- or right-padding a single string.

print(string.rep("ab", 1, "---"))
-- ab

print(string.rep("ab", 2, "---"))
-- ab---ab

-- Boundary cases in one pass
for n = -1, 3 do
    print(n, "[" .. string.rep("ab", n, "-") .. "]")
end
-- -1 []
-- 0  []
-- 1  [ab]
-- 2  [ab-ab]
-- 3  [ab-ab-ab]

With n = 2, exactly one separator sits between the two copies. The general rule is n - 1 separators for n copies, regardless of separator length, and that rule holds for any n >= 2. For very large counts with a multi-character separator the result can grow quickly, so keep an eye on the final length if you are pre-allocating a buffer or building strings that get sent over the wire.

string.rep runs in C, so it avoids the quadratic cost of building the result with .. inside a Lua loop. Prefer it whenever the count is known up front. For dynamic, lazy, or stream-style repetition where the count is not known until iteration time, fall back to a small explicit loop with .. and a table buffer that you table.concat at the end. string.rep itself does not support generators or iterators as the count argument.

Common Patterns

A few patterns come up again and again in real codebases. Drawing ASCII section dividers in CLI tools is a one-liner with string.rep:

print(string.rep("-", 40))
-- ----------------------------------------

print(string.rep("=", 20) .. " Section " .. string.rep("=", 20))
-- ==================== Section ====================

-- Symmetric padding around a title
local function header(title, width)
    local pad = math.max(0, width - #title - 2)
    local left = math.floor(pad / 2)
    return string.rep("=", left) .. " " .. title .. " " .. string.rep("=", pad - left)
end

print(header("OK", 10))
-- === OK ===

For indentation, multiplying a tab or a fixed number of spaces by the current depth keeps code that emits nested output readable. The separator argument doubles as a join character when you want a trailing comma or pipe between fields, which helps when serializing a fixed-width list.

local function indent(level)
    return string.rep("    ", level)
end

print(indent(2) .. "inside two levels")
--         inside two levels

-- Repeat a delimiter to build a trailing pattern
local n = 3
print(string.rep(".|", n) .. " end")
-- .|.|.| end

In test code, string.rep is the cheapest way to fabricate long inputs that need to exceed some buffer size, trigger an off-by-one, or stress a parser. Because the result is a real Lua string, it is also a reasonable stand-in for binary data when a function expects a non-trivial payload and you do not care about the actual byte values.

See Also