DS DevShelfHub Projects · AI tools
Cheatsheets / Lua
Cheatsheet · Languages

Lua Cheatsheet: Tables, Metatables and Coroutines Reference

By DevShelfHub

Tables, strings, functions, closures, metatables, coroutines, modules, OOP idioms, stdlib, LuaJIT FFI — the Lua 5.4 / LuaJIT surface.

114 items 8 min Tables Metatables Coroutines

Start hereQuick start · 6 you’ll reach for daily

Locallocal x = 5
Tablet = { a = 1, b = 2 }
Iteratefor i, v in ipairs(xs) do …
Methodobj:method(arg)
Coroutineco = coroutine.create(fn)
Modulelocal M = {}; return M

Target versions · paceVersions

Targets: Lua 5.4 (current) Lua 5.1 / LuaJIT 2.1 (embedded, Neovim, OpenResty) LuaRocks (package manager)

Lua versions drift incompatibly — the runtime you target matters. Lua 5.4 adds integer division (//), bitwise ops, goto, and a generational GC. LuaJIT tracks the Lua 5.1 dialect (with selected 5.2 features) but is dramatically faster and ubiquitous in embedded contexts — Neovim, OpenResty, Wireshark, redis-cli (eval), Roblox, Defold, LÖVE2D. Check _VERSION before relying on a feature; portability matters.

Install · rocksSetup

bash
# Install
brew install lua                    # macOS — stable Lua 5.4
brew install luajit                 # LuaJIT 2.1 — much faster, Lua 5.1 dialect
sudo apt install lua5.4             # Debian/Ubuntu

# Package manager
brew install luarocks
luarocks install --local penlight    # functional + OO utility kit
luarocks install --local luasocket
luarocks install --local cjson       # if not using built-in JSON

# Run
lua script.lua arg1 arg2
luajit script.lua                    # LuaJIT — same surface, faster

# REPL
lua

# Daily commands
lua -e 'print(_VERSION)'             # one-liner
lua -l mymod                         # load a module before running
luarocks list                        # installed rocks
stylua --check src/                  # formatter (rust-based, optional)
selene src/                          # linter (rust-based, optional)

Where things liveStandard library map

stringformat, find, match, gsub, sub, rep, byte, char.
tableinsert, remove, concat, sort, unpack (5.2+: table.unpack).
mathConstants (math.pi, math.huge), random, floor, log, min/max.
ioio.open, io.read, io.lines, io.popen, file-method :lines().
osos.time, os.date, os.getenv, os.execute, os.exit.
coroutinecreate, resume, yield, wrap, status.
debugtraceback, introspection. Reach for it sparingly.
require "modname"Load + cache a module. Returns whatever the module returned.
package.path / package.cpathLookup patterns for Lua / C modules.
ffi (LuaJIT only)Native C interop. local ffi = require "ffi".

local · nil · 8 typesVariables & types

Bindings

local x = 5Preferred Lexically scoped. Always use local.
x = 5Implicit global. Avoid.
local a, b = 1, 2Multiple assignment.
local a, b = b, aSwap idiom.
local x <const> = 5Constant (Lua 5.4+).
local f <close> = io.open(…)To-be-closed variable. Runs __close at scope exit (5.4+).
do … endAnonymous block — introduces a new scope for locals.

The 8 types

nilSingle value nil. Falsy. Unset.
booleantrue / false. Only false + nil are falsy — 0 and "" are truthy.
numberLua 5.4: separate integer + float. LuaJIT / 5.1: doubles only.
stringImmutable byte sequences. Interned for equality.
functionFirst-class. Closures by default.
tableThe only composite. Arrays, maps, modules, objects — all tables.
userdataOpaque values from C extensions.
threadCoroutine handle.

Type-checks & coercions

type(x)Returns the type name as a string.
tonumber("42") / tonumber("ff", 16)Parse. Returns nil on failure.
tostring(x)String coerce. Uses __tostring if defined.
10 .. " items"String concat. Note the space10..s is a number-method lookup.

if · for · whileControl flow

if x > 0 then … elseif … then … else … endKeywords end with end. No braces.
v = cond and a or bTernary idiom. Caveat: fails when a is itself falsy.
for i = 1, 10 do … endNumeric for. Inclusive end.
for i = 10, 1, -1 do … endStep parameter.
for i, v in ipairs(t) do … endArray iteration. Stops at first nil.
for k, v in pairs(t) do … endAll keys. Order is undefined.
while cond do … endPre-test.
repeat … until condPost-test. Note: until not while.
break / goto label / ::label::Loop exit / labeled jump.
No continue keywordUse goto continue with ::continue:: at the end of the loop body.

Lua patterns ≠ regexStrings

s = "hello"Strings are immutable.
[[multi\nline]] / [==[…]==]Long literals. Bracket levels disambiguate when content contains brackets.
s:len() / #sByte length. UTF-8 needs utf8.len (5.3+).
s:upper() / s:lower() / s:reverse()Method call syntax dispatches to string.
s:sub(1, 3) / s:sub(-3)Substring. 1-based, inclusive. Negative = from end.
string.format("%5.2f", pi)printf-style.
s:rep(3, "-")Repeat with separator.
s:find("pat") / s:match("(%w+)")Search. Returns indices / captures.
s:gsub("pat", "rep")Global substitute. Returns new string + count.
for w in s:gmatch("%w+") do … endIterator over matches.

Pattern syntax (NOT PCRE)

%a / %A / %d / %D / %s / %S / %w / %WLetter / non- / digit / non- / space / non- / alnum / non-.
. (dot)Any byte.
* / + / - / ?0+ greedy / 1+ greedy / 0+ lazy / 0–1.
^pat$Anchors.
[abc] / [^abc]Char class / negated.
()Capture group.
%1, %2 (in replacement)Backreferences.
No alternation, no backtracking capturesLua patterns are intentionally smaller than regex.

The one composite typeTables

t = { 10, 20, 30 }Array-style. 1-indexed.
t = { name = "Ada", age = 36 }Hash-style.
t = { 1, 2, 3, label = "primes" }Mixed. Same table.
t[1] / t.name / t["name"]Three access forms.
#tLength of the array part. Stops at first nil — can be ambiguous.
next(t, k)Low-level iterator. pairs is built on it.
table.insert(t, v) / table.insert(t, 1, v)Append / insert at index. Shifts.
table.remove(t) / table.remove(t, i)Pop tail / remove at index.
table.sort(t, cmp)In-place sort. Optional comparator.
table.concat(t, ", ")Join array of strings.
t[k] = nilRemove a key.
{ table.unpack(t) }Shallow copy of array part.

Worked example

lua
-- Tables are the ONE composite type in Lua.
-- Arrays, hashes, namespaces, objects, modules — all tables.

-- Array-style (1-indexed!)
local fruits = { "apple", "banana", "cherry" }
print(fruits[1])                          -- apple
print(#fruits)                            -- length (3)

-- Hash-style
local user = { name = "Ada", age = 36 }
print(user.name)                          -- Ada
print(user["name"])                       -- same thing

-- Mixed (both at once)
local row = { 1, 2, 3, label = "primes", verified = true }

-- Iterate
for i, v in ipairs(fruits) do             -- arrays: stops at first nil
    print(i, v)
end

for k, v in pairs(user) do                -- any keys, any order
    print(k, v)
end

-- Insert / remove
table.insert(fruits, "date")              -- append
table.insert(fruits, 1, "apricot")        -- insert at index 1
table.remove(fruits, 2)                   -- remove index 2

-- Sort + concat
table.sort(fruits)
print(table.concat(fruits, ", "))

-- Shallow copy
local copy = { table.unpack(fruits) }     -- Lua 5.2+ (or `unpack` in 5.1/LuaJIT)

Closures · multiple returnsFunctions

local function f(a, b) return a + b endDefine. local function hoists the name — recursion works.
local f = function(a, b) return a + b endEquivalent. Doesn’t hoist — f can’t call itself.
return a, b, cMultiple return values. First-class.
local x, y = f()Destructure returns.
local x = (f())Parens truncate to first value — useful trick.
function f(...) local args = { ... } endVararg. select("#", ...) for count, select(n, ...) for tail.
obj:method(arg)Sugar for obj.method(obj, arg). Injects self.
function obj:method() … endMirror sugar on the def side.
Closures capture upvalues by referenceFunctions defined inside another see + share its locals.

Hooks · classesMetatables & OOP

setmetatable(t, mt)Attach a metatable. getmetatable(t) retrieves.
__index = TFallback for missing keys. The foundation of OO.
__index = function(t, k) … endLazy / computed properties.
__newindexHook on assignment to a missing key. Used for read-only tables.
__callMake a table callable like a function.
__tostringUsed by print and tostring.
__add / __sub / __mul / __div / __mod / __pow / __unmArithmetic overload.
__eq / __lt / __leComparison. __eq only fires when both operands have the same metatable.
__lenCustom #t.
__gcFinalizer. Runs during garbage collection.
__closeRuns at to-be-closed variable scope exit (5.4).

Worked example

lua
-- Metatables = the hook layer that powers OOP, operator overloading, defaults.

-- 1 · Class via __index — fallback for missing keys
local Vector = {}
Vector.__index = Vector                   -- when key missing on instance, look here

function Vector.new(x, y)
    return setmetatable({ x = x, y = y }, Vector)
end

function Vector:length()                  -- `:` injects `self`
    return math.sqrt(self.x^2 + self.y^2)
end

function Vector:__add(other)              -- v + w
    return Vector.new(self.x + other.x, self.y + other.y)
end

function Vector:__tostring()              -- print(v)
    return string.format("Vec(%g, %g)", self.x, self.y)
end

local v = Vector.new(3, 4)
print(v:length())                         -- 5.0
print(v + Vector.new(1, 1))               -- Vec(4, 5)

-- 2 · Default-valued table
local function defaults(defaults_tbl)
    return setmetatable({}, { __index = defaults_tbl })
end

local config = defaults{ port = 8080, tls = false }
config.tls = true
print(config.port, config.tls)            -- 8080  true

Cooperative concurrencyCoroutines

co = coroutine.create(fn)Create. Not started.
coroutine.resume(co, args…)Run / continue. Returns ok, ....
coroutine.yield(values…)Suspend, returning values to the resumer.
coroutine.status(co)"suspended" | "running" | "dead" | "normal".
coroutine.wrap(fn)Wraps in a regular function. Errors propagate instead of ok-flag.
coroutine.isyieldable()True inside a coroutine.
Iterator pattern via coroutine.wrapIdiomatic lazy sequences (range, fibonacci, file scan).
No preemptionCoroutines only switch on explicit yield — they aren’t OS threads.

require · packageModules

local M = {}; function M.greet() … end; return MIdiomatic module skeleton.
local mod = require "myapp.util"Dotted path → myapp/util.lua. Cached after first call.
package.pathSemicolon-separated lookup patterns: ./?.lua;./?/init.lua.
package.loaded[name]The require cache. package.loaded[name] = nil forces a reload.
module … end (5.1)Legacy Old syntax. Avoid in new code.
_G / _ENVGlobal table. Lua 5.2+ uses _ENV as the current env upvalue.
luarocks make / install / listBuild / install / list packages.
rockspecPer-rock manifest. Like package.json.

pcall · error · assertErrors

error("msg") / error({ code = 42 })Throw. Any value, not just strings.
error("msg", 2)Second arg is stack level — report from caller.
assert(cond, "msg")If cond is falsy, error. Returns cond otherwise — chainable.
local ok, err = pcall(fn, args…)Protected call. ok is false on error; err is the value passed to error.
local ok, … = xpcall(fn, traceback, args…)Like pcall but with a message handler. Use debug.traceback for stacks.
No try / catch keywordsIdiomatic Lua wraps risky calls in pcall.
return nil, "reason"Common convention — return nil-plus-error-string for recoverable failure.

Word counter · ~40 linesEnd-to-end · Word count

Walk a directory via io.popen, tokenize with patterns, accumulate counts in a default-valued table, top-20 by frequency. Pure stdlib.

lua
-- Save as: word_count.lua
-- Run    :  lua word_count.lua path/to/dir
--
-- Walk a directory, tally word frequencies across every .txt and .md file,
-- print the top 20. Pure stdlib + `io.popen` for portable directory walk.

local function each_file(root, ext_pat)
    local cmd = ('find %q -type f \\( -name "*.txt" -o -name "*.md" \\)'):format(root)
    local pipe = assert(io.popen(cmd, "r"))
    return function()
        local line = pipe:read("*l")
        if not line then pipe:close(); return nil end
        return line
    end
end

local function tokenize(line)
    return line:lower():gmatch("[%a']+")     -- letters + apostrophe
end

local function topn(map, n)
    local list = {}
    for word, count in pairs(map) do
        list[#list + 1] = { word = word, count = count }
    end
    table.sort(list, function(a, b) return a.count > b.count end)
    for i = 1, math.min(n, #list) do
        print(string.format("%6d  %s", list[i].count, list[i].word))
    end
end

local function main()
    local root = arg[1] or "."
    local counts = setmetatable({}, { __index = function() return 0 end })

    for path in each_file(root) do
        local f = io.open(path, "r")
        if f then
            for line in f:lines() do
                for w in tokenize(line) do
                    counts[w] = counts[w] + 1
                end
            end
            f:close()
        end
    end

    topn(counts, 20)
end

main()

Best practiceGood to know

Declare local everywhere. Forget the keyword and the binding becomes a global — visible everywhere, slow to access, and prone to clobbering library names. Most lint warnings boil down to this.
Cache method lookups in hot loops. local ins = table.insert before a tight loop avoids the per-iteration table lookup. LuaJIT removes some of this overhead; reference Lua keeps it.
Pick a target dialect early. Lua 5.4 vs LuaJIT (5.1+) is a fork in the road — integer division, bitwise ops, goto, generational GC vs raw speed and FFI. Embedded contexts (Neovim, OpenResty, game engines) usually pick one for you.

Common trapsWatch out for

Arrays are 1-indexed, but # stops at the first nil. A "sparse" array with a gap returns one of several lengths. Treat #t as well-defined only for densely-packed arrays.
0 and "" are truthy. Coming from Python / JS this surprises everyone. Only false and nil are falsy. if x then isn’t the null-check you think it is.
The and / or ternary fails on a falsy true-branch. cond and value or default returns default when value is nil or false — even if cond is true. Use an explicit if when the true branch can be falsy.

Go deeperSee also

Lua FAQ

What is Lua used for?

Lua is a lightweight, embeddable scripting language widely used for game scripting (Roblox, World of Warcraft add-ons, LÖVE), configuration (NeoVim, Redis scripting, nginx via OpenResty), and embedded systems where a small footprint matters. Its C API makes it easy to embed inside any application.

What is a table in Lua?

A table is Lua's only data structure — it acts as an array, dictionary, object, namespace, and module all at once. Numeric keys form the array part (1-indexed); string or other keys form the hash part. Tables are reference types and the foundation of all OOP in Lua.

What are metatables in Lua?

A metatable is a regular table attached to another table (or userdata) via setmetatable(). Its metamethods (__index, __newindex, __add, __tostring, etc.) let you intercept operations on the parent table. __index enables prototype-based inheritance: if a key is missing, Lua looks it up in the metatable's __index table or calls the __index function.

How do coroutines work in Lua?

Coroutines are cooperative, stackful threads. Create one with coroutine.create(fn) or coroutine.wrap(fn). Resume with coroutine.resume(co, ...) and pause with coroutine.yield(). Unlike OS threads, only one coroutine runs at a time — control transfers explicitly, not preemptively. They are used for async I/O patterns, iterators, and game state machines.

What is the difference between Lua 5.4 and LuaJIT?

Lua 5.4 is the official reference implementation — stable, maintained by PUC-Rio, and ships integers, generational GC, and to-be-closed variables. LuaJIT is a high-performance JIT-compiled fork of Lua 5.1 with a C FFI for calling C libraries directly without binding code. LuaJIT is significantly faster for numeric code but tracks Lua 5.1 semantics, not 5.4.

Is Lua free and open source?

Yes. Lua is MIT-licensed and maintained by PUC-Rio. The reference implementation, LuaJIT, and most libraries are free to use commercially. The Lua runtime is tiny enough to ship inside proprietary products without restriction.