luaguides

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.compose
  • pl.compat — compatibility shims for Lua version differences (e.g., compat.unpack for table.unpack)
  • pl.seq — iterators as sequences with seq.keys(), seq.values()
  • pl.Date — date formatting and parsing
  • pl.data — reading and querying CSV-like data files

See Also