Skip to content

Lua File I/O

Lua’s I/O library provides functions for reading from and writing to files. It offers two main models for file operations: implicit file descriptors (using a default input/output file) and explicit file descriptors (working with specific file objects).

For the examples below, let’s assume we have a sample file named data.txt with the following content:

Hello Lua Learner!
This is the second line.
End of file.

Before running examples that write to this file, ensure you have a backup or are okay with it being modified/overwritten.

The primary function to open a file is io.open(filename [, mode]).

  • filename: The name (or path) of the file to open.
  • mode: A string specifying the file access mode. If omitted, the default mode depends on the Lua version and OS but is often read-only.

Common file modes:

ModeDescription
"r"Read mode (default). File must exist. Reading starts at the beginning.
"w"Write mode. Creates a new file or truncates an existing file to zero length. Writing starts at the beginning.
"a"Append mode. Creates a new file if it doesn’t exist. Writing starts at the end of the file.
"r+"Update mode (read and write). File must exist. Reading/writing can occur at any position.
"w+"Update mode (read and write). Creates a new file or truncates an existing file. Reading/writing can occur at any position.
"a+"Append update mode (read and write). Creates a new file if it doesn’t exist. Reading can occur at any position, writing is always appended.

You can also append "b" to the mode (e.g., "rb", "wb") to open the file in binary mode, which is important on some systems (like Windows) to prevent translation of newline characters. On Unix-like systems, binary and text modes are usually the same.

If io.open succeeds, it returns a file object (also called a file handle). If it fails (e.g., file not found in "r" mode, permissions error), it returns nil followed by an error message and an error code.

local file, err = io.open("non_existent_file.txt", "r")
if not file then
print("Error opening file: ", err)
else
-- Process file
file:close()
end

It’s crucial to close files using file:close() when you are done with them to release system resources and ensure data is flushed to disk.

Lua provides default file handles for standard input (io.stdin), standard output (io.stdout), and standard error (io.stderr). Functions like io.read(), io.write(), and print() use these by default.

io.input(file_or_filename) and io.output(file_or_filename) can change the default input/output files.

-- Create data.txt if it doesn't exist with some content
local initial_file_setup, err_setup = io.open("data.txt", "w")
if initial_file_setup then
initial_file_setup:write("Hello Lua Learner!\nThis is the second line.\nEnd of file.\n")
initial_file_setup:close()
else
print("Setup error: ", err_setup)
return -- Exit if setup failed
end
-- Example using implicit descriptors
local original_input = io.input() -- Save original default input
local original_output = io.output() -- Save original default output
-- Set default input to data.txt
local file_in, err_in = io.input("data.txt")
if not file_in then
print("Error setting input: ", err_in)
else
local first_line = io.read("*l") -- Read a line from default input (now data.txt)
print("First line read: ", first_line)
io.close(file_in) -- Close the file associated with the default input
end
-- Restore original default input
io.input(original_input)
-- Append to data.txt using default output
local file_out, err_out = io.output("data.txt", "a") -- Mode 'a' for append is not standard for io.output, usually opens in write mode
-- Better to use explicit file handling for append
-- For safety and clarity, let's use explicit file handling for append here:
file_out, err_out = io.open("data.txt", "a")
if not file_out then
print("Error opening for append: ", err_out)
else
file_out:write("\nAppended via explicit handle.")
file_out:close()
end
-- Verify append
local verify_file, err_verify = io.open("data.txt", "r")
if verify_file then
print("\n--- Content of data.txt after append ---")
print(verify_file:read("*a"))
verify_file:close()
else
print("Error verifying: ", err_verify)
end
-- Note: Changing default input/output is less common than explicit file handling.

The file:read(...) method (for explicit file objects) or io.read(...) (for default input) reads data. It accepts one or more format strings:

FormatDescription
"*n" or "n"Reads a number. Skips spaces before trying to read. Returns nil if a number cannot be parsed or at EOF.
"*a" or "a"Reads the whole file from the current position. Returns an empty string at EOF.
"*l" or "l"Reads the next line, discarding the newline character. Returns nil at EOF.
"*L" or "L"Reads the next line, keeping the newline character. Returns nil at EOF.
numberReads up to number bytes (a string of that many characters). Returns nil at EOF.
local file, err = io.open("data.txt", "r")
if file then
print("\n--- Reading data.txt line by line ---")
local line1 = file:read("*l")
print("Line 1: ", line1)
local rest_of_file = file:read("*a")
print("Rest of file: ", rest_of_file)
file:close()
else
print("Error reading file: ", err)
end

The file:lines() iterator is very convenient for reading a file line by line:

print("\n--- Reading data.txt with file:lines() ---")
local file_lines, err_lines = io.open("data.txt", "r")
if file_lines then
for line in file_lines:lines() do
print("Iterator Line: ", line)
end
file_lines:close() -- lines() iterator does not close the file automatically when file object is passed
else
print("Error opening for lines: ", err_lines)
end
-- io.lines(filename) opens, iterates, and closes the file automatically
print("\n--- Reading data.txt with io.lines(filename) ---")
for line in io.lines("data.txt") do
print("io.lines Line: ", line)
end

The file:write(...) method (or io.write(...) for default output) writes its arguments to the file. Arguments must be strings or numbers (which are converted to strings). It does not automatically add newlines or separators.

local file, err = io.open("output.txt", "w") -- Opens in write mode, overwrites if exists
if file then
file:write("Hello, Lua file output!\n")
file:write("This is line 2.", " And this is part of line 2.\n")
local num = 123
file:write("A number: ", num, "\n")
print("Data written to output.txt")
file:close()
else
print("Error writing to file: ", err)
end
  • file:seek(whence, offset): Sets the file position. whence can be "set" (from beginning), "cur" (from current position), or "end" (from end). offset is a byte count. file:seek() returns the current position. file:seek("cur", 0) gets current position.
  • file:flush(): Flushes any buffered writes to the file.
  • io.type(obj): Returns "file" if obj is an open file handle, "closed file" if it’s a closed file handle, or nil otherwise.
  • io.tmpfile(): Returns a handle to a temporary file, opened in update mode ("w+b"). This file is automatically removed when the program exits or when the handle is closed.

Example with seek:

local file, err = io.open("data.txt", "r")
if file then
file:seek("set", 6) -- Seek to the 7th byte (0-indexed)
local partial_read = file:read(5) -- Read 5 bytes
print("\nRead after seek: '", partial_read, "'") -- Should be "Lua L"
local current_pos = file:seek("cur", 0)
print("Current position: ", current_pos)
file:seek("end", -12) -- Seek 12 bytes from the end
local end_data = file:read("*a")
print("Reading last part: '", end_data, "'")
file:close()
else
print("Error in seek example: ", err)
end

Error Handling: Always check the return values of I/O operations, especially io.open, file:read, and file:write, to handle potential errors gracefully.

Practical Application: File I/O is essential for reading configuration files, writing log files, processing data sets, and saving/loading application state.