luaguides

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.

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 io library for reading and writing file contents