Filesystem Operations with LuaFileSystem
Lua’s standard io library gives you file reading and writing, but it doesn’t let you create directories, walk through folder contents, or read file metadata. That’s the gap LuaFileSystem fills. Usually installed as lfs, this library wraps POSIX and Windows filesystem calls into a clean Lua API.
It’s the go-to library for any Lua program that needs to work with the filesystem beyond opening files — build scripts, config loaders, project scaffolding tools, and server-side file handlers all tend to depend on it.
Installation
LuaFileSystem is distributed via LuaRocks:
luarocks install luafilesystem
Load it like any other library:
local lfs = require("lfs")
Directory Contents with lfs.dir
The lfs.dir() function iterates over entries in a directory:
local lfs = require("lfs")
for name in lfs.dir("/etc") do
if name ~= "." and name ~= ".." then
print(name)
end
end
Each call returns the next entry name, or nil when the directory is exhausted. Entries . and .. are included — you filter them out yourself if needed.
The iterator returns nil automatically when the loop ends, so there’s no need to close the directory handle explicitly in normal usage.
Reading File Attributes
lfs.attributes() returns a table of metadata for any file or directory:
local attr = lfs.attributes("/etc/passwd")
print(attr.mode) -- "file"
print(attr.size) -- file size in bytes
print(attr.access) -- last access time (os.time format)
print(attr.modification) -- last modification time
print(attr.change) -- last status change time
print(attr.permissions) -- e.g. "rw-r--r--"
The mode field tells you what you’re dealing with:
local attr = lfs.attributes("/tmp")
if attr.mode == "directory" then
print("it's a directory")
elseif attr.mode == "file" then
print("it's a file")
end
Other possible mode values: link, socket, char device, block device, named pipe.
If you only need one attribute, pass it as a second argument — this avoids building the whole table:
local size = lfs.attributes("/bin/ls", "size")
print(size) -- number of bytes
Creating and Removing Directories
local lfs = require("lfs")
-- Create a directory
local ok, err = lfs.mkdir("/tmp/myproject")
if not ok then
print("failed: " .. err)
end
-- Remove it
local ok2, err2 = lfs.rmdir("/tmp/myproject")
if not ok2 then
print("failed: " .. err2)
end
Both return true on success, or nil plus an error message on failure.
Working Directory
Lua programs run in a current working directory. lfs.chdir() changes it:
local lfs = require("lfs")
local original = lfs.currentdir()
print("was: " .. original)
lfs.chdir("/tmp")
print("now: " .. lfs.currentdir()) -- "/tmp"
lfs.chdir(original) -- go back
lfs.currentdir() returns the current directory as a string, or nil plus an error message if retrieval fails.
Modifying File Timestamps
lfs.touch() updates the access and modification times of a file:
local lfs = require("lfs")
local now = os.time()
-- Set both access and modification to now
lfs.touch("/tmp/afile", now, now)
-- Set only modification time (access time uses mtime)
lfs.touch("/tmp/afile", nil, now)
-- Set both to current time
lfs.touch("/tmp/afile")
Times are in the same format os.time() returns — seconds since epoch. Omitting both arguments sets both times to the current moment.
Symbolic Links
On systems that support them, lfs.link() creates links:
-- Hard link (default)
lfs.link("/path/to/original", "/path/to/newhard")
-- Symbolic / soft link
lfs.link("/path/to/original", "/path/to/symlink", true)
lfs.symlinkattributes() reads information about the link itself, rather than the file it points to:
local attr = lfs.symlinkattributes("/path/to/symlink")
print(attr.target) -- what the link points to
print(attr.mode) -- "link"
On Windows, symlinkattributes() currently behaves identically to attributes().
File Locking
File locks prevent concurrent processes from stepping on each other. lfs.lock() locks an open file handle:
local lfs = require("lfs")
local file = io.open("/tmp/mylock.txt", "w")
-- Lock for exclusive write access
local ok = lfs.lock(file, "w")
if not ok then
print("could not acquire lock")
end
-- ... do work ...
lfs.unlock(file)
file:close()
The lock is automatically released when the file is closed, but it’s good practice to explicitly unlock before closing.
For locking a directory rather than a file, lfs.lock_dir() creates a sentinel file:
local lfs = require("lfs")
local lock, err = lfs.lock_dir("/tmp/myproject", 30) -- 30s stale timeout
if not lock then
print("another instance is running: " .. err)
return
end
-- ... do work ...
lock:free() -- release the lock
The second argument is how long the lock file is considered stale — useful for cleaning up after a crashed process. The default is effectively forever.
Iterating a Directory Recursively
A common pattern is walking a directory tree:
local lfs = require("lfs")
local function walk(dir, depth)
depth = depth or 0
for entry in lfs.dir(dir) do
if entry ~= "." and entry ~= ".." then
local path = dir .. "/" .. entry
local attr = lfs.attributes(path)
print(string.rep(" ", depth) .. entry)
if attr.mode == "directory" then
walk(path, depth + 1)
end
end
end
end
walk(".")
This recursive walk prints every file and directory under the current folder, indented by depth.
Setting File Mode
lfs.setmode() controls whether a file is opened in text or binary mode (primarily relevant on Windows):
local lfs = require("lfs")
local file = io.open("data.bin", "rb")
local prev = lfs.setmode(file, "binary")
print("previous mode: " .. tostring(prev))
-- back to text
lfs.setmode(file, "text")
On unix systems, where text and binary modes are identical, this function has no practical effect.
See Also
- lua-closures — how closures capture upvalues, relevant when writing directory walker callbacks
- lua-metatables — metatables for building object-oriented file utilities
- file-io — Lua’s standard
iolibrary for reading and writing file contents