Lua with Neowin: File I/O — io.open, Read Modes, Write Modes & io.lines | Episode 26
Video: Lua with Neowin: File I/O — io.open, Read Modes, Write Modes & io.lines | Episode 26 by Taught by Celeste AI - AI Coding Coach
Lua File I/O: io.open, Read Modes, Write Modes, and io.lines
io.open(path, mode)to open."r"read,"w"write,"a"append.file:lines()for line-by-line iteration.io.lines(path)for the convenient one-shot. Build a small text-processing example.
Reading and writing files is foundational for any non-trivial script. Lua's I/O API is small but covers the cases.
Reading a file line by line
local file = io.open("data.txt", "r")
if file then
for line in file:lines() do
print(" " .. line)
end
file:close()
end
io.open(path, mode) returns a file handle (or nil on failure — always guard).
file:lines() returns an iterator that yields one line per call, without the trailing newline.
file:close() releases the OS file descriptor. Skip it and Lua's GC eventually closes the file — but for predictable behaviour (especially during writes), close explicitly.
Open modes
| Mode | Effect |
|---|---|
"r" |
read (default) |
"w" |
write — truncates if file exists |
"a" |
append — preserves existing content |
"r+" |
read and write |
"w+" |
read and write — truncates first |
"a+" |
read and append |
"rb" etc. |
binary mode (Windows; on Unix, b is a no-op) |
Writing a file
local out = io.open("output.txt", "w")
if out then
out:write("Line one\n")
out:write("Line two\n")
out:write("Line three\n")
out:close()
end
file:write(...) writes raw bytes. No automatic newline — add "\n" yourself.
write accepts multiple arguments:
out:write("Name: ", name, "\n")
Each argument is converted to string and concatenated, no separator.
Appending
local log = io.open("output.txt", "a")
if log then
log:write("Appended line\n")
log:close()
end
Open with "a" (append) to add to the end without erasing existing content.
io.lines: line iterator without explicit open/close
for line in io.lines("output.txt") do
print(" " .. line)
end
io.lines(path) opens the file, returns a line iterator, and automatically closes the file when iteration finishes. Convenient for the common case "process each line."
If the file can't be opened, io.lines raises an error — wrap in pcall (next episode) for error-tolerant code.
Reading whole file at once
local file = io.open("data.txt", "r")
local content = file:read("a") -- "all"
file:close()
print(#content .. " bytes")
file:read("a") (or "*a" in Lua 5.1) reads the entire remaining file.
Other read modes:
file:read("l")— one line, newline stripped (default).file:read("L")— one line, newline included.file:read("n")— one number.file:read(N)— N characters/bytes.
You can chain modes: local first, second = file:read("l", "l") reads two lines.
Processing structured data
local total = 0
local count = 0
for line in io.lines("data.txt") do
local name, score = string.match(line, "(%a+)%s+(%d+)")
if name and score then
print(name .. ": " .. score)
total = total + tonumber(score)
count = count + 1
end
end
print("Average: " .. string.format("%.1f", total / count))
If data.txt looks like:
Alice 90
Bob 85
Charlie 92
The loop:
- Reads each line.
- Matches
name scorewith the pattern from episode 25. - If both captured, prints and accumulates.
Output:
Alice: 90
Bob: 85
Charlie: 92
Average: 89.0
Pattern + line iteration is the core of most text processing.
Default streams: stdin, stdout, stderr
io.write("Hello\n") -- writes to stdout
io.read() -- reads from stdin
io.stderr:write("Warning\n") -- writes to stderr
io.write is io.stdout:write shorthand. io.read is io.stdin:read shorthand.
Useful for command-line tools: separate diagnostic output (stderr) from data (stdout) so users can pipe.
Error handling
local file, err = io.open("missing.txt", "r")
if not file then
print("Open failed: " .. err)
return
end
io.open returns nil, errmsg on failure. The local x, err = ... pattern is the canonical "try and check" form in Lua. We dive into errors next episode.
Binary files
local f = io.open("image.png", "rb")
local bytes = f:read("a")
f:close()
print(#bytes)
"rb" for binary read; "wb" for binary write. On Unix, b is a no-op; on Windows, it disables CRLF translation. Always use the b variant for non-text content to be portable.
Common stumbles
Forgetting to close the file. Especially after writing — the OS may not flush until close. Always file:close() at the end.
Writing without newlines. out:write("a", "b") produces "ab", not two lines. Add "\n" between or after.
Reading after EOF. file:read() returns nil at end of file. Loops should test for nil to know when to stop. (io.lines and file:lines handle this for you.)
Forgetting to handle io.open failure. Calling methods on nil crashes. Always check.
Mixing read and write modes carelessly. "r+" requires careful seeking between reads and writes. For most cases, open once for read or write, not both.
What's next
Episode 27: error handling. error() to throw, pcall() to catch, assert() for invariants, xpcall() with custom handlers. The result, err return convention.
Recap
io.open(path, mode) → file handle or nil, err. Modes: "r" read, "w" write (truncates), "a" append; add b for binary. file:lines() iterates lines (no newlines). file:read("a") for whole file, ("l") for one line, (N) for N bytes. io.lines(path) is the convenient open-iter-close shortcut. Always file:close() after writing. io.stdout, io.stdin, io.stderr for the standard streams.
Next episode: error handling.