Polyglot
Languages Lua io
Lua § io

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 via tostring.

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:

ModeDescription
"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 bufferedf: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.lines for substantial file iteration.
  • Use f:read("*a") for whole-file reads.
  • Use io.open with explicit mode; check for nil error.
  • Use f:close() (or <close> in 5.4+) — admit substantial cleanup.
  • Use io.popen for substantial subprocess output.
  • Use os.tmpname for temporary files.
  • Use string.char and 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.