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

  1. What's the difference between "w" and "a" file modes?
  2. How do you read a file line by line efficiently?
  3. What happens if you don't close a file handle?
  4. How do you check if a file opened successfully?
Show answers
  1. "w" overwrites the entire file, "a" appends to the end
  2. Use for line in file:lines() do iterator
  3. Resource leak - OS may limit number of open files
  4. Check if io.open() returns non-nil value

Exercises

  1. Create a log file analyzer that counts error messages
  2. Build a simple backup utility that copies files
  3. Implement a file-based database using tables