Ruby Logo

Ruby File I/O

Learn file reading, writing, and data processing in Ruby.

Home Ruby Ruby File I/O

Ruby File I/O Operations

Master Ruby's file and directory operations to read, write, and manipulate files effectively. Learn File.open, Dir operations, Pathname class, and best practices for file handling with proper error management and resource cleanup.

What is File I/O?

File I/O (Input/Output): The process of reading data from files, writing data to files, and managing file system operations. Essential for data persistence, configuration management, and interacting with the file system.

Real-World Applications

  • Log Files: Reading application logs for debugging
  • Configuration: Loading settings from config files
  • Data Processing: Reading CSV files or text data
  • File Management: Creating backups, organizing directories

File.open and Basic Operations

Opening Files Safely

# Block form - automatically closes file
File.open("data.txt", "r") do |file|
  content = file.read
  puts content
end
# File is automatically closed here

# Manual mode - must close explicitly
file = File.open("data.txt", "r")
content = file.read
file.close  # Important: must close manually

# Safe manual mode with ensure
file = nil
begin
  file = File.open("data.txt", "r")
  content = file.read
  puts content
ensure
  file&.close  # Safe navigation operator
end

File Modes

"r"
Read only (default)
"w"
Write only, truncates existing file
"a"
Append only, writes at end
"r+"
Read and write, starts at beginning
"w+"
Read and write, truncates file
"a+"
Read and write, starts at end

Reading Files

Various Reading Methods

# Read entire file at once
content = File.read("data.txt")
puts content

# Read file with encoding
content = File.read("data.txt", encoding: "UTF-8")

# Read file line by line (memory efficient)
File.foreach("data.txt") do |line|
  puts "Line: #{line.chomp}"
end

# Read all lines into array
lines = File.readlines("data.txt")
lines.each { |line| puts line.chomp }

# Read with File.open
File.open("data.txt", "r") do |file|
  # Read entire file
  content = file.read

  # Read line by line
  file.rewind  # Go back to beginning
  while line = file.gets
    puts line.chomp
  end

  # Read specific number of characters
  file.rewind
  chunk = file.read(100)  # Read first 100 characters
end

Pro Tips for Reading

  • Large Files: Use File.foreach or file.each_line to avoid loading everything into memory
  • Binary Files: Use "rb" mode for images, executables, etc.
  • Encoding: Specify encoding explicitly for international text
  • chomp: Use .chomp to remove newline characters

Writing Files

Writing Methods

# Write entire content to file (overwrites)
File.write("output.txt", "Hello, World!")

# Write with encoding
File.write("output.txt", "Hello, 世界!", encoding: "UTF-8")

# Append to file
File.write("log.txt", "New log entry\n", mode: "a")

# Using File.open for writing
File.open("data.txt", "w") do |file|
  file.write("Line 1\n")
  file.puts("Line 2")  # Automatically adds newline
  file.print("Line 3")  # No newline
  file << "Line 4\n"   # Shorthand for write
end

# Append mode
File.open("log.txt", "a") do |file|
  file.puts Time.now.strftime("%Y-%m-%d %H:%M:%S - Application started")
end

# Write multiple lines
lines = ["First line", "Second line", "Third line"]
File.open("multi.txt", "w") do |file|
  lines.each { |line| file.puts line }
end

Write vs Puts vs Print

write
Writes exactly what you give it
puts
Adds newline automatically
print
No newline, like write but for console similarity
<<
Shorthand for write, chainable

Directory Operations with Dir

Working with Directories

# Current directory operations
puts Dir.pwd               # Print working directory
Dir.chdir("/tmp")          # Change directory
puts Dir.pwd               # Now shows /tmp

# List directory contents
files = Dir.entries(".")   # All entries including . and ..
files = Dir.children(".")  # All entries excluding . and ..

# Pattern matching
ruby_files = Dir.glob("*.rb")          # All .rb files
all_files = Dir.glob("**/*")           # Recursive all files
txt_files = Dir.glob("**/*.txt")       # All .txt files recursively

# Directory iteration
Dir.foreach(".") do |entry|
  puts entry unless ["..", "."].include?(entry)
end

# Create and remove directories
Dir.mkdir("new_folder")              # Create directory
Dir.mkdir("nested/folder")           # Error: parent doesn't exist

# Create nested directories
require 'fileutils'
FileUtils.mkdir_p("nested/deep/folder")  # Creates all parent dirs

# Remove directories
Dir.rmdir("empty_folder")            # Only empty directories
FileUtils.rm_rf("folder_with_files") # Recursive removal

Directory Safety Tips

  • Check existence: Use Dir.exist? before operations
  • Permissions: Handle permission errors with rescue blocks
  • Recursive deletion: Be very careful with FileUtils.rm_rf
  • Current directory: Store original directory before changing

Pathname Class - Object-Oriented File Paths

Pathname Benefits

require 'pathname'

# Create Pathname objects
path = Pathname.new("/Users/john/documents/file.txt")
relative_path = Pathname.new("../data/input.csv")

# Path information
puts path.basename         # "file.txt"
puts path.dirname          # "/Users/john/documents"
puts path.extname          # ".txt"
puts path.basename(".txt") # "file"

# Path manipulation
new_path = path.parent / "backup" / "file_backup.txt"
puts new_path.to_s         # "/Users/john/documents/backup/file_backup.txt"

# Path queries
puts path.exist?           # true/false
puts path.file?            # true if it's a file
puts path.directory?       # true if it's a directory
puts path.absolute?        # true if absolute path
puts path.relative?        # true if relative path

# Reading and writing with Pathname
content = path.read
path.write("New content")

# Directory operations
dir = Pathname.new("/Users/john/documents")
dir.children.each do |child|
  puts "#{child.basename} is a #{child.file? ? 'file' : 'directory'}"
end

# Glob patterns
Pathname.glob("**/*.rb").each do |ruby_file|
  puts "Ruby file: #{ruby_file}"
end

Why Use Pathname?

  • Object-Oriented: Methods instead of string manipulation
  • Cross-Platform: Handles path separators automatically
  • Chainable: Clean, readable path construction
  • Rich API: Built-in file operations and queries

File Operations and Utilities

File System Operations

# File existence and type checking
puts File.exist?("data.txt")     # true/false
puts File.file?("data.txt")      # true if it's a file
puts File.directory?("folder")   # true if it's a directory
puts File.symlink?("link.txt")   # true if it's a symbolic link

# File permissions and attributes
puts File.readable?("data.txt")  # true if readable
puts File.writable?("data.txt")  # true if writable
puts File.executable?("script")  # true if executable

# File size and dates
puts File.size("data.txt")       # Size in bytes
puts File.empty?("data.txt")     # true if file is empty
puts File.mtime("data.txt")      # Last modified time
puts File.atime("data.txt")      # Last accessed time
puts File.ctime("data.txt")      # Creation time

# File operations
File.rename("old_name.txt", "new_name.txt")  # Rename file
File.delete("unwanted.txt")                  # Delete file
File.copy("source.txt", "destination.txt")  # Copy file

# Using FileUtils for advanced operations
require 'fileutils'

FileUtils.cp("source.txt", "dest.txt")         # Copy file
FileUtils.cp_r("source_dir", "dest_dir")       # Copy directory recursively
FileUtils.mv("old.txt", "new.txt")             # Move/rename
FileUtils.rm_rf("directory")                   # Remove recursively
FileUtils.chmod(0644, "file.txt")              # Change permissions
FileUtils.touch("new_file.txt")                # Create empty file

# Temporary files
require 'tempfile'

Tempfile.create("prefix") do |tmpfile|
  tmpfile.write("Temporary content")
  tmpfile.rewind
  puts tmpfile.read
  # File is automatically deleted when block ends
end

Error Handling and Best Practices

Robust File Operations

# Safe file reading with error handling
def safe_read_file(filename)
  begin
    File.read(filename)
  rescue Errno::ENOENT
    puts "File not found: #{filename}"
    nil
  rescue Errno::EACCES
    puts "Permission denied: #{filename}"
    nil
  rescue => e
    puts "Unexpected error reading #{filename}: #{e.message}"
    nil
  end
end

# Safe file writing
def safe_write_file(filename, content)
  begin
    File.write(filename, content)
    puts "Successfully wrote to #{filename}"
  rescue Errno::EACCES
    puts "Permission denied writing to: #{filename}"
  rescue Errno::ENOSPC
    puts "No space left on device"
  rescue => e
    puts "Error writing to #{filename}: #{e.message}"
  end
end

# Atomic file operations (prevent corruption)
def atomic_write(filename, content)
  temp_file = "#{filename}.tmp"
  begin
    File.write(temp_file, content)
    File.rename(temp_file, filename)
  rescue => e
    File.delete(temp_file) if File.exist?(temp_file)
    raise e
  end
end

# Check before operating
def process_file(filename)
  unless File.exist?(filename)
    puts "File doesn't exist: #{filename}"
    return
  end

  unless File.readable?(filename)
    puts "Cannot read file: #{filename}"
    return
  end

  content = File.read(filename)
  # Process content...
end

File I/O Best Practices

  • Always use blocks: File.open with blocks for automatic cleanup
  • Handle exceptions: Rescue file system errors appropriately
  • Check existence: Verify files exist before operations
  • Specify encoding: Be explicit about character encoding
  • Use Pathname: For cleaner, more maintainable path handling
  • Atomic operations: Use temporary files for critical writes
  • Memory efficiency: Stream large files instead of loading entirely

Practical Examples

Log File Analyzer

class LogAnalyzer
  def initialize(log_file)
    @log_file = log_file
  end

  def analyze
    return unless File.exist?(@log_file)

    error_count = 0
    warning_count = 0

    File.foreach(@log_file) do |line|
      error_count += 1 if line.include?("ERROR")
      warning_count += 1 if line.include?("WARN")
    end

    puts "Log Analysis for #{@log_file}:"
    puts "Errors: #{error_count}"
    puts "Warnings: #{warning_count}"
    puts "Total lines: #{File.readlines(@log_file).count}"
  end

  def extract_errors(output_file)
    File.open(output_file, "w") do |output|
      File.foreach(@log_file) do |line|
        output.puts line if line.include?("ERROR")
      end
    end
  end
end

# Usage
analyzer = LogAnalyzer.new("app.log")
analyzer.analyze
analyzer.extract_errors("errors_only.log")

Configuration Manager

class ConfigManager
  def initialize(config_path = "config.txt")
    @config_path = config_path
    @config = {}
    load_config
  end

  def get(key)
    @config[key]
  end

  def set(key, value)
    @config[key] = value
    save_config
  end

  private

  def load_config
    return unless File.exist?(@config_path)

    File.foreach(@config_path) do |line|
      next if line.strip.empty? || line.start_with?("#")
      key, value = line.strip.split("=", 2)
      @config[key] = value if key && value
    end
  end

  def save_config
    File.open(@config_path, "w") do |file|
      file.puts "# Configuration file"
      @config.each do |key, value|
        file.puts "#{key}=#{value}"
      end
    end
  end
end

# Usage
config = ConfigManager.new
config.set("database_host", "localhost")
config.set("database_port", "5432")
puts config.get("database_host")  # "localhost"

Quick Navigation

Related Topics

Video Tutorial

Watch and learn ruby file i/o

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!