luaguides

Penlight Batteries-Included Library: Lua Utility Guide

Lua’s standard library is intentionally small, which is why the Penlight batteries-included philosophy matters so much for real-world scripting. 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

Once installed through LuaRocks, Penlight’s modules are available to any Lua script on your system. The library keeps its components separated so you only load what you need — each module is independently require-able, which keeps startup time low and avoids pulling in unused code.

Loading Penlight

The main entry point is the pl module, which acts as a namespace hub for all of Penlight. 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:

The import_into() approach pulls each module into a local table you control, which is the recommended pattern for libraries and larger scripts — it avoids name collisions and makes every dependency explicit. But for quick one-off scripts where brevity matters more than namespace hygiene, you can skip the ceremony and dump everything straight into _G. Just be aware that this pollutes the global namespace, which can cause subtle bugs if another module happens to define a variable with the same name.

require("pl")
-- now tablex, path, pretty, etc. are globally available

For libraries, importing into a local table keeps you from polluting the global namespace: a practice that becomes essential once your project grows beyond a single file.

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

Deep copying is particularly important in Lua because tables are reference types: assigning a table to a new variable copies the pointer, not the data. Two variables pointing at the same table will see each other’s mutations, which is a frequent source of hard-to-debug state corruption. Penlight’s deep_copy recursively clones nested tables so you get a genuinely independent copy.

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}

Knowing whether two tables hold identical data is essential when caching computation results or comparing configuration snapshots across runs. The set operations, meanwhile, treat Lua tables as mathematical sets: a surprisingly useful pattern for filtering user input, computing permission overlaps, or finding which files changed between two directory listings.

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. This cross-platform behavior is built into every path operation, so you can write scripts that traverse directory trees without a single if platform == "windows" branch.

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")

While pl.dir handles directory-level operations like listing contents and creating directory trees, you’ll often need to work with individual files as well. Penlight splits these concerns cleanly: dir for structure, pl.file for content.

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. The functions handle edge cases like missing files and permission errors internally, raising Lua errors that you can catch with pcall when you need graceful failure handling.

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. This makes pretty a natural choice for configuration files: you get human-readable formatting during development and native Lua loading at runtime, with no parsing step required.

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"

These string utilities fill a real gap: Lua’s native string library has no split, no startswith, and no strip. Writing these from scratch with Lua patterns is error-prone, especially when handling edge cases like empty fields in CSV lines or strings with mixed whitespace at both ends.

Reading Config Files with pl.config

config parses several common config file formats into Lua tables. A typical INI-style file looks like this:

# example.conf
timeout = 30
host = localhost
debug = true

With the config file in place, reading those values into a Lua table takes a single function call. config.read inspects the file extension and picks the right parser automatically, so the same code works whether you’re loading an INI file, a Unix-style conf, or a simple key-value list:

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. Values are type-coerced when possible: true and false become booleans, numeric strings become numbers: which saves you from writing manual conversion logic.

CLI Arguments with pl.lapp

lapp parses command-line arguments using a readable specification. Instead of manually looping through arg and matching flags with patterns, you declare what your script expects and lapp does the rest:

lua myscript.lua --count 5 --name "Alice" --verbose

The real power of lapp is that the usage string doubles as the specification. Long options with defaults, short flags, boolean switches, and positional arguments are all inferred from the description text. The resulting args table uses the long option names as keys, so args.count and args.name read naturally without any array-to-named-variable mapping:

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. This declarative style means you’re not writing the same 30 lines of argument parsing boilerplate in every script: you describe what you want once and lapp handles validation, type conversion, and error messages.

Classes with pl.class

pl.class is a lightweight class system built on top of Lua’s metatable-based OOP:

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

When to reach for Penlight

Penlight shines brightest when you’re building a Lua application that needs to touch the filesystem, parse configuration, or deal with complex table structures. If your script is a single file that does one thing and exits, standard Lua plus LuaFileSystem may be all you need. But once you find yourself writing the same split function for the third time or hand-rolling yet another argument parser, Penlight has already solved that problem. Many Lua developers treat it as the de facto standard library extension, and for good reason: it adds the utilities you reach for daily without pulling in framework-scale dependencies.

The library’s design philosophy mirrors Lua itself: small, composable modules that you load individually rather than one monolithic import. You can use pl.path without pulling in pl.class, or pl.pretty without depending on pl.lapp. This modularity keeps your runtime footprint lean while giving you access to a battle-tested standard library extension that has been in continuous development since the Lua 5.1 era. Whether you are writing a command-line tool, a game server, or an embedded scripting layer, Penlight fills the gap between Lua’s deliberately minimal core and the full-featured standard library developers expect. Any non-trivial Lua project benefits from at least a few of its modules.

See Also