I/O
Lua’s I/O is built on the io library — admit standard streams (io.stdin, io.stdout, io.stderr), file handles via io.open, line iteration via io.lines, and convenience functions io.read and io.write. The model is stream-oriented — reads and writes operate on file handles. The principal read formats are "*a" (entire file), "*l" (one line), "*n" (one number), "*L" (one line including newline), or a number (specified bytes). For substantial network and substantial file-system operations, third-party libraries (LuaSocket, LuaFileSystem) admit substantial functionality. The combination — io for files and standard streams, format-driven reading, the conventional io.open/handle pattern, the file-as-iterator via io.lines and f:lines() — is the substance of Lua’s I/O surface.
Standard streams
io.write("Hello, world!\n") -- to stdout
io.write("a", "b", "c", "\n") -- multiple values
print("Hello") -- alias for io.write + newline + tab separator
print("a", "b", "c") -- "a b c" (tab-separated)
io.stderr:write("error message\n") -- to stderr
local input = io.read() -- one line from stdin
The principal differences:
io.write— no newline, no separator, no implicit conversion of nil/false.print— admits multiple args (tab-separated), adds newline, accepts any types viatostring.
io.read
The io.read admits format-driven reading:
io.read("*l") -- one line (no newline)
io.read("*L") -- one line (with newline)
io.read("*a") -- entire stream
io.read("*n") -- one number
io.read(10) -- 10 bytes
io.read("*l", "*l") -- two lines (multiple formats)
In Lua 5.3+, the * is optional:
io.read("l") -- equivalent
io.read("a")
io.read("n")
File operations
local f, err = io.open("file.txt", "r")
if not f then
print("error:", err)
return
end
local content = f:read("*a")
f:close()
The io.open modes:
| Mode | Description |
|---|---|
"r" | read (default) |
"w" | write (truncate) |
"a" | append |
"r+" | read/write (existing file) |
"w+" | read/write (truncate or create) |
"a+" | read/write (append) |
"rb", "wb", etc. | binary modes (Windows) |
File handle methods
local f = io.open("data.txt", "r")
f:read(...) -- read with formats
f:write(...) -- write strings/numbers
f:lines(...) -- iterator
f:seek(whence, offset) -- position
f:close()
f:flush() -- flush buffered writes
seek
f:seek("set") -- to start
f:seek("end") -- to end
f:seek("cur", 100) -- 100 bytes from current
f:seek("set", 0) -- explicit start
local pos = f:seek("cur") -- current position
The seek returns the new position (or nil, err on failure).
File iteration
io.lines
for line in io.lines("file.txt") do
print(line)
end
-- With formats (5.3+):
for line in io.lines("file.txt", "*L") do -- include newlines
io.write(line)
end
-- From stdin:
for line in io.lines() do
print(line)
end
The io.lines opens the file, iterates, and closes when iteration ends. No explicit close needed.
Per-handle lines
local f = io.open("file.txt", "r")
for line in f:lines() do
process(line)
end
f:close()
The f:lines() admits substantial control — multiple iterations, mixed reads, etc.
Common patterns
Read entire file
local function read_file(path)
local f, err = io.open(path, "r")
if not f then return nil, err end
local content = f:read("*a")
f:close()
return content
end
local content, err = read_file("data.txt")
if content then
print(content)
else
print("error:", err)
end
Write to a file
local function write_file(path, content)
local f, err = io.open(path, "w")
if not f then return nil, err end
f:write(content)
f:close()
return true
end
write_file("output.txt", "hello world")
Append to a file
local f = io.open("log.txt", "a")
f:write(os.date(), " - ", message, "\n")
f:close()
Read line-by-line
for line in io.lines("file.txt") do
if line:match("ERROR") then
print(line)
end
end
The io.lines admits substantial efficient line streaming.
Read CSV-like
local function parse_csv(path)
local rows = {}
for line in io.lines(path) do
local row = {}
for field in line:gmatch("[^,]+") do
row[#row + 1] = field
end
rows[#rows + 1] = row
end
return rows
end
For substantial CSV with quoted fields, a library is conventional.
Read fixed-size chunks
local f = io.open("large.bin", "rb")
while true do
local chunk = f:read(4096)
if chunk == nil then break end
process(chunk)
end
f:close()
The "rb" mode admits binary reading (matters on Windows where "r" admits CRLF translation).
Write multiple values
local f = io.open("output.txt", "w")
f:write("line 1\n")
f:write("line 2\n")
f:write("line 3\n")
f:close()
-- Or single call:
f:write("line 1\n", "line 2\n", "line 3\n")
Atomic file write
local function atomic_write(path, content)
local tmp = path .. ".tmp"
local f, err = io.open(tmp, "w")
if not f then return nil, err end
f:write(content)
f:close()
local ok, err = os.rename(tmp, path)
if not ok then
os.remove(tmp)
return nil, err
end
return true
end
The pattern admits “either the new content is written or the old file remains”.
Reading from stdin
local input = io.read("*l")
print("got:", input)
-- All lines:
for line in io.lines() do
process(line)
end
-- With prompt:
io.write("Enter name: ")
local name = io.read("*l")
print("Hello, " .. name)
Reading numbers
io.write("Enter number: ")
local n = io.read("*n") -- reads one number
print(n * 2)
For strict parsing:
local input = io.read("*l")
local n = tonumber(input)
if n == nil then
print("not a number")
else
print(n * 2)
end
Pipe via io.popen
local f = io.popen("ls")
if f then
for line in f:lines() do
print(line)
end
f:close()
end
-- Write to a process:
local f = io.popen("grep ERROR > errors.txt", "w")
if f then
f:write(big_log_content)
f:close()
end
The io.popen admits substantial subprocess interaction.
Capturing command output
local function exec(cmd)
local f = io.popen(cmd)
if not f then return nil, "popen failed" end
local output = f:read("*a")
local ok, _, code = f:close()
return output, code
end
local result, code = exec("date")
print(result, code)
Temporary file
local tmp_path = os.tmpname() -- temp filename
local f = io.open(tmp_path, "w")
f:write("temporary data")
f:close()
-- Use the file...
os.remove(tmp_path) -- cleanup
Reading binary
local f = io.open("data.bin", "rb")
local bytes = f:read("*a")
f:close()
-- Iterate bytes:
for i = 1, #bytes do
local byte = bytes:byte(i)
process(byte)
end
Writing binary
local f = io.open("output.bin", "wb")
f:write(string.char(0x01, 0x02, 0x03, 0x04))
f:close()
The string.char admits substantial byte-level control.
Random access
local f = io.open("data.bin", "r+b")
f:seek("set", 100) -- seek to byte 100
local chunk = f:read(50) -- read 50 bytes from there
f:seek("set", 200)
f:write(modified_data)
f:close()
File info via stat-like
The standard library does not include file stat — use LuaFileSystem:
local lfs = require("lfs")
local attrs = lfs.attributes("file.txt")
if attrs then
print(attrs.size)
print(attrs.modification)
print(attrs.mode) -- "file", "directory", etc.
end
Directory listing
The standard library does not include directory listing — use LuaFileSystem:
local lfs = require("lfs")
for entry in lfs.dir(".") do
if entry ~= "." and entry ~= ".." then
print(entry)
end
end
Reading config file (Lua-as-config)
local function load_config(path)
local fn, err = loadfile(path)
if not fn then return nil, err end
local config = {}
setfenv(fn, config) -- 5.1
-- For 5.2+:
-- local fn, err = loadfile(path, "t", config)
fn()
return config
end
-- File: settings.lua
host = "localhost"
port = 8080
debug = true
-- Loading:
local cfg = load_config("settings.lua")
print(cfg.host, cfg.port, cfg.debug)
The mechanism admits substantial DSL-style configuration.
Buffered writes
The default writes are buffered — f:flush() admits explicit flushing:
local f = io.open("output.txt", "w")
f:write("important data")
f:flush() -- ensure on disk
-- ... other work ...
f:close()
Line counting
local function count_lines(path)
local n = 0
for _ in io.lines(path) do
n = n + 1
end
return n
end
print("lines:", count_lines("file.txt"))
Tail-like
local function tail(path, n)
n = n or 10
local lines = {}
for line in io.lines(path) do
lines[#lines + 1] = line
if #lines > n then table.remove(lines, 1) end
end
return lines
end
for _, line in ipairs(tail("log.txt", 10)) do
print(line)
end
Default I/O streams
io.input() -- get current input (default stdin)
io.output() -- get current output
io.input(file) -- set as default
io.output(file)
io.read() -- reads from current default
io.write() -- writes to current default
The mechanism admits substantial redirection without per-call file specification.
A note on setvbuf (buffer control)
local f = io.stdout
f:setvbuf("no") -- unbuffered
f:setvbuf("line") -- line-buffered (flush on newline)
f:setvbuf("full", 4096) -- block-buffered, size 4096
The mechanism admits substantial control over write buffering — substantial for log files and substantially-interactive programs.
A note on the absence of substantial network I/O
The standard library does not include network I/O. The conventional substitute:
luarocks install luasocket
local socket = require("socket")
local http = require("socket.http")
local body = http.request("https://example.com")
print(body)
-- Lower-level:
local client = socket.tcp()
client:connect("example.com", 80)
client:send("GET / HTTP/1.0\r\n\r\n")
local response = client:receive("*a")
client:close()
A note on the conventional discipline
The contemporary Lua I/O advice:
- Use
io.linesfor substantial file iteration. - Use
f:read("*a")for whole-file reads. - Use
io.openwith explicit mode; check for nil error. - Use
f:close()(or<close>in 5.4+) — admit substantial cleanup. - Use
io.popenfor substantial subprocess output. - Use
os.tmpnamefor temporary files. - Use
string.charand byte access for substantial binary I/O. - Use LuaFileSystem for substantial file-system operations.
- Use LuaSocket for substantial network I/O.
- Use atomic write pattern for substantial reliability.
The combination — io for files and streams, format-driven reading, the file-handle methods, the io.lines for substantial iteration, the os.popen for substantial subprocess interaction, the substantial third-party extensions for network and file-system — is the substance of Lua’s I/O surface. The discipline produces concise, explicit I/O code with substantial built-in functionality and substantial extensibility through the conventional libraries.