Lua - File I/O
File I/O
Master file operations in Lua including reading, writing, and manipulating files. Essential for data processing, configuration management, and persistent storage.
Estimated time: 25-30 minutes
Learning Objectives
- Understand Lua's file I/O system and io library
- Read and write files using different modes
- Process large files efficiently with iterators
- Handle binary data and file positioning
- Build practical file processing applications
File Opening and Modes
Lua provides the io
library for file operations. Files are opened with different modes for reading, writing, and appending.
-- File modes demonstration
local function demonstrate_file_modes()
-- "r" - Read mode (default)
local read_file = io.open("data.txt", "r")
if read_file then
local content = read_file:read("*a")
print("File content:", content)
read_file:close()
end
-- "w" - Write mode (overwrites existing file)
local write_file = io.open("output.txt", "w")
if write_file then
write_file:write("Hello, World!\n")
write_file:write("Line 2\n")
write_file:close()
end
-- "a" - Append mode
local append_file = io.open("output.txt", "a")
if append_file then
append_file:write("Appended line\n")
append_file:close()
end
-- "r+" - Read/write mode
local rw_file = io.open("data.txt", "r+")
if rw_file then
local line = rw_file:read("*l")
rw_file:write("Modified: " .. (line or ""))
rw_file:close()
end
end
demonstrate_file_modes()
Expected Output: Creates and modifies files with different access patterns
Reading Files
Multiple ways to read file content: entire file, line by line, or specific amounts.
-- Reading techniques
local function read_file_examples()
-- Create sample file
local sample = io.open("sample.txt", "w")
sample:write("First line\nSecond line\nThird line\nFourth line\n")
sample:close()
-- Read entire file
local file = io.open("sample.txt", "r")
if file then
local all_content = file:read("*a")
print("Entire file:\n" .. all_content)
file:close()
end
-- Read line by line
print("\nLine by line:")
file = io.open("sample.txt", "r")
if file then
local line_num = 1
for line in file:lines() do
print(line_num .. ": " .. line)
line_num = line_num + 1
end
file:close()
end
-- Read specific number of characters
file = io.open("sample.txt", "r")
if file then
print("\nFirst 10 characters:")
local chunk = file:read(10)
print("'" .. chunk .. "'")
file:close()
end
end
read_file_examples()
Expected Output:
Entire file:
First line
Second line
Third line
Fourth line
Line by line:
1: First line
2: Second line
3: Third line
4: Fourth line
First 10 characters:
'First line'
Writing and Formatting
Write formatted data to files using various techniques including string formatting.
-- Writing formatted data
local function write_formatted_data()
local file = io.open("report.txt", "w")
if not file then
print("Error: Could not create report file")
return
end
-- Write header
file:write("SALES REPORT\n")
file:write(string.rep("=", 50) .. "\n\n")
-- Sample data
local sales_data = {
{name = "Alice", amount = 1250.75, region = "North"},
{name = "Bob", amount = 980.50, region = "South"},
{name = "Charlie", amount = 1500.25, region = "East"},
{name = "Diana", amount = 750.00, region = "West"}
}
-- Write formatted table
file:write(string.format("%-12s %-10s %-8s\n", "Name", "Amount", "Region"))
file:write(string.rep("-", 32) .. "\n")
local total = 0
for _, record in ipairs(sales_data) do
file:write(string.format("%-12s $%-9.2f %-8s\n",
record.name, record.amount, record.region))
total = total + record.amount
end
file:write(string.rep("-", 32) .. "\n")
file:write(string.format("%-12s $%-9.2f\n", "TOTAL", total))
file:close()
print("Report written to report.txt")
-- Read and display the report
local display_file = io.open("report.txt", "r")
if display_file then
print("\nReport contents:")
print(display_file:read("*a"))
display_file:close()
end
end
write_formatted_data()
Expected Output:
Report written to report.txt
Report contents:
SALES REPORT
==================================================
Name Amount Region
--------------------------------
Alice $1250.75 North
Bob $980.50 South
Charlie $1500.25 East
Diana $750.00 West
--------------------------------
TOTAL $4481.50
File Processing Patterns
Common patterns for processing files efficiently, including CSV parsing and log analysis.
-- CSV file processing
local function process_csv_file()
-- Create sample CSV
local csv_file = io.open("data.csv", "w")
csv_file:write("name,age,city,salary\n")
csv_file:write("John Doe,25,New York,50000\n")
csv_file:write("Jane Smith,30,Los Angeles,60000\n")
csv_file:write("Bob Johnson,35,Chicago,55000\n")
csv_file:write("Alice Brown,28,Houston,52000\n")
csv_file:close()
-- CSV parser function
local function parse_csv_line(line)
local fields = {}
local field = ""
local in_quotes = false
for i = 1, #line do
local char = line:sub(i, i)
if char == '"' then
in_quotes = not in_quotes
elseif char == ',' and not in_quotes then
table.insert(fields, field)
field = ""
else
field = field .. char
end
end
table.insert(fields, field)
return fields
end
-- Process CSV file
local file = io.open("data.csv", "r")
if not file then
print("Error: Could not open CSV file")
return
end
local headers = parse_csv_line(file:read("*l"))
local records = {}
for line in file:lines() do
local fields = parse_csv_line(line)
local record = {}
for i, header in ipairs(headers) do
record[header] = fields[i]
end
table.insert(records, record)
end
file:close()
-- Analyze data
print("CSV Analysis:")
print("Total records:", #records)
local total_salary = 0
local ages = {}
for _, record in ipairs(records) do
total_salary = total_salary + tonumber(record.salary)
table.insert(ages, tonumber(record.age))
end
-- Calculate average age
local sum_age = 0
for _, age in ipairs(ages) do
sum_age = sum_age + age
end
local avg_age = sum_age / #ages
print("Average salary: $" .. (total_salary / #records))
print("Average age: " .. string.format("%.1f", avg_age))
-- Find highest earner
local highest_earner = records[1]
for _, record in ipairs(records) do
if tonumber(record.salary) > tonumber(highest_earner.salary) then
highest_earner = record
end
end
print("Highest earner: " .. highest_earner.name ..
" ($" .. highest_earner.salary .. ")")
end
process_csv_file()
Expected Output:
CSV Analysis:
Total records: 4
Average salary: $54250
Average age: 29.5
Highest earner: Jane Smith ($60000)
Binary Files and File Positioning
Handle binary data and control file position for advanced file operations.
-- Binary file operations
local function binary_file_operations()
-- Create binary data file
local bin_file = io.open("data.bin", "wb")
if not bin_file then
print("Error: Could not create binary file")
return
end
-- Write binary data (integers as bytes)
local numbers = {10, 20, 30, 40, 50}
for _, num in ipairs(numbers) do
bin_file:write(string.char(num))
end
bin_file:close()
-- Read binary data
bin_file = io.open("data.bin", "rb")
if bin_file then
print("Binary file contents:")
local byte_count = 0
while true do
local byte = bin_file:read(1)
if not byte then break end
local value = string.byte(byte)
print("Byte " .. byte_count .. ": " .. value)
byte_count = byte_count + 1
end
bin_file:close()
end
-- File positioning example
local pos_file = io.open("position_test.txt", "w+")
if pos_file then
pos_file:write("0123456789ABCDEF")
-- Seek to position 5
pos_file:seek("set", 5)
local char_at_5 = pos_file:read(1)
print("Character at position 5: " .. char_at_5)
-- Seek to end and get file size
local file_size = pos_file:seek("end")
print("File size: " .. file_size .. " bytes")
-- Seek from current position
pos_file:seek("set", 10)
local current_pos = pos_file:seek()
print("Current position: " .. current_pos)
pos_file:close()
end
end
binary_file_operations()
Expected Output:
Binary file contents:
Byte 0: 10
Byte 1: 20
Byte 2: 30
Byte 3: 40
Byte 4: 50
Character at position 5: 5
File size: 16 bytes
Current position: 10
Configuration File Manager
A practical example: building a configuration file manager for applications.
-- Configuration file manager
local ConfigManager = {}
ConfigManager.__index = ConfigManager
function ConfigManager.new(filename)
local self = setmetatable({}, ConfigManager)
self.filename = filename or "config.ini"
self.data = {}
self:load()
return self
end
function ConfigManager:load()
local file = io.open(self.filename, "r")
if not file then
print("Config file not found, creating new one...")
return
end
local current_section = "default"
self.data[current_section] = {}
for line in file:lines() do
line = line:match("^%s*(.-)%s*$") -- trim whitespace
if line:match("^%[(.+)%]$") then
-- Section header
current_section = line:match("^%[(.+)%]$")
self.data[current_section] = {}
elseif line:match("^[^#;]") and line:match("=") then
-- Key-value pair (ignore comments starting with # or ;)
local key, value = line:match("^([^=]+)=(.*)$")
if key and value then
key = key:match("^%s*(.-)%s*$") -- trim key
value = value:match("^%s*(.-)%s*$") -- trim value
self.data[current_section][key] = value
end
end
end
file:close()
print("Config loaded from " .. self.filename)
end
function ConfigManager:save()
local file = io.open(self.filename, "w")
if not file then
print("Error: Could not save config file")
return false
end
for section, values in pairs(self.data) do
if section ~= "default" then
file:write("[" .. section .. "]\n")
end
for key, value in pairs(values) do
file:write(key .. "=" .. value .. "\n")
end
file:write("\n")
end
file:close()
print("Config saved to " .. self.filename)
return true
end
function ConfigManager:get(section, key, default)
section = section or "default"
if self.data[section] and self.data[section][key] then
return self.data[section][key]
end
return default
end
function ConfigManager:set(section, key, value)
section = section or "default"
if not self.data[section] then
self.data[section] = {}
end
self.data[section][key] = tostring(value)
end
-- Demo usage
local function demo_config_manager()
local config = ConfigManager.new("app_config.ini")
-- Set some configuration values
config:set("database", "host", "localhost")
config:set("database", "port", "5432")
config:set("database", "name", "myapp")
config:set("ui", "theme", "dark")
config:set("ui", "language", "en")
config:set("ui", "window_width", "1024")
config:set("ui", "window_height", "768")
config:set("default", "debug", "true")
config:set("default", "log_level", "info")
-- Save configuration
config:save()
-- Create new instance and load
local config2 = ConfigManager.new("app_config.ini")
-- Display loaded values
print("\nLoaded configuration:")
print("Database host: " .. config2:get("database", "host", "not set"))
print("Database port: " .. config2:get("database", "port", "not set"))
print("UI Theme: " .. config2:get("ui", "theme", "light"))
print("Debug mode: " .. config2:get("default", "debug", "false"))
print("Missing value: " .. config2:get("missing", "key", "default_value"))
end
demo_config_manager()
Expected Output:
Config file not found, creating new one...
Config saved to app_config.ini
Config loaded from app_config.ini
Loaded configuration:
Database host: localhost
Database port: 5432
UI Theme: dark
Debug mode: true
Missing value: default_value
Common Pitfalls
File Handle Management
- Always close files: Use
file:close()
to prevent resource leaks - Check file operations: Always verify that
io.open()
succeeded - Handle permissions: Consider file permissions and access rights
- Path separators: Use appropriate path separators for cross-platform compatibility
Checks for Understanding
- What's the difference between "w" and "a" file modes?
- How do you read a file line by line efficiently?
- What happens if you don't close a file handle?
- How do you check if a file opened successfully?
Show answers
- "w" overwrites the entire file, "a" appends to the end
- Use
for line in file:lines() do
iterator - Resource leak - OS may limit number of open files
- Check if
io.open()
returns non-nil value
Exercises
- Create a log file analyzer that counts error messages
- Build a simple backup utility that copies files
- Implement a file-based database using tables