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.foreachorfile.each_lineto avoid loading everything into memory - Binary Files: Use
"rb"mode for images, executables, etc. - Encoding: Specify encoding explicitly for international text
- chomp: Use
.chompto 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"