Penlight: A Batteries-Included Library
Lua’s standard library is intentionally small. That’s a strength, but it means you end up rewriting the same utilities over and over — table deep copies, path joining, pretty-printing, config file parsing. Penlight fills those gaps. It’s pure Lua, has no C dependencies, and works across Lua 5.1 through 5.4.
Installation
luarocks install penlight
Loading Penlight
The main entry point is the pl module. The most ergonomic way to use it is pl.import_into(), which loads individual modules on demand into a table you control:
local pl = require("pl")
local pretty = pl.import_into().pretty
pretty.write({a = 1, b = {c = 2}})
Or load everything into global space for scripts:
require("pl")
-- now tablex, path, pretty, etc. are globally available
For libraries, importing into a local table keeps you from polluting the global namespace.
Extended Tables with pl.tablex
tablex fills the gaps in Lua’s table library. Where Lua gives you table.insert and table.remove, Penlight gives you tablex.update, tablex.merge, tablex.deep_copy, and more:
local tablex = require("pl.tablex")
-- Deep copy
local original = {a = {b = 1}}
local copy = tablex.deep_copy(original)
copy.a.b = 99
print(original.a.b) -- 1 (unchanged)
-- Merge two tables (right into left)
local t1 = {a = 1, b = 2}
local t2 = {b = 3, c = 4}
tablex.update(t1, t2)
-- t1 is now {a = 1, b = 3, c = 4}
-- Keys and values as lists
local t = {a = 1, b = 2, c = 3}
local keys = tablex.keys(t) -- {"a", "b", "c"}
local vals = tablex.values(t) -- {1, 2, 3}
-- Find element in a list
local idx = tablex.find({10, 20, 30}, 20) -- 2
Table Comparison and Set Operations
tablex also handles equality and set operations:
local tablex = require("pl.tablex")
-- Compare tables deeply
local t1 = {a = 1, b = {c = 2}}
local t2 = {a = 1, b = {c = 2}}
print(tablex.deep_equal(t1, t2)) -- true
-- Set difference
local a = {1, 2, 3, 4}
local b = {3, 4, 5}
local diff = tablex.difference(a, b) -- {1, 2}
local union = tablex.union(a, b) -- {1, 2, 3, 4, 5}
Path Manipulation with pl.path
path wraps the filesystem path logic that’s tedious to write by hand:
local path = require("pl.path")
-- Join path components
local full = path.join("src", "lib", "init.lua")
print(full) -- "src/lib/init.lua" (Unix) or "src\lib\init.lua" (Windows)
-- Split a path
print(path.dirname "/home/user/file.txt") -- "/home/user"
print(path.basename "/home/user/file.txt") -- "file.txt"
print(path.extname "/home/user/file.txt") -- ".txt"
-- Check what something is
print(path.is_absolute "/home/user") -- true
print(path.exists "/etc/hosts") -- true
print(path.isdir "/etc") -- true
print(path.isfile "/etc/hosts") -- true
The path functions are portable — they handle both Unix forward slashes and Windows backslashes correctly.
Directory Operations with pl.dir
dir works alongside LuaFileSystem (lfs) but focuses on listing and managing directories:
local dir = require("pl.dir")
-- List all files in a directory
for filename in dir.files("/etc") do
print(filename)
end
-- Create a directory tree
dir.makepath("/tmp/myproject/src/lib")
-- Remove a directory (must be empty)
dir.rmdir("/tmp/myproject")
-- Copy a directory tree recursively
dir.copydir("/source/project", "/dest/project")
File Operations with pl.file
file provides one-liners for common file tasks:
local file = require("pl.file")
-- Read entire file contents
local contents = file.read("/etc/hostname")
-- Write string to file
file.write("/tmp/test.txt", "hello world")
-- Copy and move
file.copy("/src/file.txt", "/dest/file.txt")
file.move("/tmp/file.txt", "/archive/file.txt")
-- Delete a file
file.delete("/tmp/garbage.txt")
This saves you from wrapping io.open/f:read/f:close every time.
Pretty-Printing with pl.pretty
pretty writes and reads Lua tables in a human-readable format:
local pretty = require("pl.pretty")
-- Write a table to a string
local s = pretty.write({a = 1, b = {c = 2}})
print(s)
-- a = 1
-- b = {
-- c = 2
-- }
-- Dump table to stdout
pretty.dump({hello = "world"})
-- Write table to a file (valid Lua code)
pretty.write({settings = {timeout = 30}}, "config.lua")
-- Read it back
local config = pretty.read("config.lua")
print(config.settings.timeout) -- 30
The format pretty.write produces is valid Lua code — you can dofile it directly.
Extended Strings with pl.stringx
stringx adds Python-style methods that Lua’s string library lacks:
local stringx = require("pl.stringx")
-- Split and join
local parts = stringx.split("one,two,three", ",") -- {"one", "two", "three"}
local joined = stringx.join(", ", {"a", "b", "c"}) -- "a, b, c"
-- Strip whitespace
local s = " hello "
print(stringx.strip(s)) -- "hello"
print(stringx.lstrip(s)) -- "hello "
print(stringx.rstrip(s)) -- " hello"
-- Startswith / endswith
print(stringx.startswith("hello world", "hello")) -- true
print(stringx.endswith("file.txt", ".txt")) -- true
-- Python-style replace
print(stringx.replace("hello world", "world", "lua")) -- "hello lua"
Reading Config Files with pl.config
config parses several common config file formats into Lua tables:
# example.conf
timeout = 30
host = localhost
debug = true
local config = require("pl.config")
local settings = config.read("example.conf")
print(settings.timeout) -- 30
print(settings.host) -- "localhost"
config.read automatically handles key=value pairs, Apache-style Name Value lines, and basic JSON-like tables.
CLI Arguments with pl.lapp
lapp parses command-line arguments using a readable specification:
lua myscript.lua --count 5 --name "Alice" --verbose
local lapp = require("pl.lapp")
local args = lapp.launch [[
Usage: myscript [options]
-i, --input (default: stdin) Input file
-o, --output (default: stdout) Output file
-c, --count (default: 1) Number of iterations
-n, --name (default: world) Name to greet
-v, --verbose Verbose mode
]]
print(args.count)
print(args.name)
if args.verbose then print("verbose on") end
Flags, options with defaults, positional arguments — all come out as a simple table.
Classes with pl.class
pl.class is a lightweight class system:
local class = require("pl.class")
local Animal = class()
function Animal:_init(name)
self.name = name
end
function Animal:speak()
print(self.name .. " makes a sound")
end
local Dog = class(Animal)
function Dog:speak()
print(self.name .. " barks")
end
local d = Dog("Rex")
d:speak() -- "Rex barks"
It’s single-inheritance with _init as the constructor.
Other Notable Modules
pl.operator— Lua operators as callable functions:operator.add(a, b),operator.eq(a, b),operator.mul(a, b)pl.func— functional helpers:func.curry,func.bind,func.composepl.compat— compatibility shims for Lua version differences (e.g.,compat.unpackfortable.unpack)pl.seq— iterators as sequences withseq.keys(),seq.values()pl.Date— date formatting and parsingpl.data— reading and querying CSV-like data files
See Also
- lua-table-sorting — sorting tables in Lua with and without Penlight
- lua-lfs-filesystem — the lfs library for deeper filesystem control
- lua-metatables — Lua’s OOP model, which Penlight’s
classbuilds on