Back to Scaling Ruby Applications guides

Working With Files in Ruby: A Complete Guide

Stanley Ulili
Updated on August 27, 2025

File operations form the backbone of most applications, from reading configuration files to processing data uploads and generating reports. Ruby provides comprehensive file handling capabilities through its built-in classes and modules, offering both simple interfaces for common tasks and powerful tools for complex file manipulation.

Ruby's file handling philosophy emphasizes readable, expressive code that handles edge cases gracefully. The language offers multiple approaches for most file operations, enabling you to select the method that best suits your specific use case and error handling requirements.

This guide covers essential file operations in Ruby through practical examples, from basic file reading and writing to directory traversal, pattern matching, and archive processing.

Prerequisites

Ruby 2.7 or later provides the most stable file handling APIs and performance improvements covered in this tutorial.

Basic familiarity with Ruby blocks, exception handling, and enumerable methods will help you apply the file processing patterns demonstrated throughout this guide.

Ruby's file handling approach

Ruby treats files as first-class objects with rich APIs for manipulation. The File class provides low-level operations, while higher-level methods offer convenient shortcuts for common tasks. Ruby's block-based iteration makes file processing both memory-efficient and expressive.

The core file classes follow consistent patterns:

Screenshot of diagram. This architecture shows the relationship between Ruby's core file classes and their primary responsibilities. The File class handles individual file operations, Dir manages directory-level tasks, Pathname provides cross-platform path manipulation, and error handling ensures robust operation across all components.

  • File: Reading, writing, and metadata operations
  • Dir: Directory listing and manipulation
  • Pathname: Object-oriented path handling
  • IO: Low-level input/output operations

Create a working directory to follow along:

 
mkdir ruby-files-demo && cd ruby-files-demo

With the directory created, you can proceed to explore Ruby's file handling capabilities through hands-on examples.

Reading and writing files

Ruby's File class provides straightforward methods for file operations. The most common approach uses class methods that handle file opening and closing automatically.

Before exploring Ruby's file operations, you'll need sample data to work with. Create a simple text file that the examples can read and manipulate.

Create a sample data file manually:

data.txt
Line 1: Hello World
Line 2: Ruby Files
Line 3: Processing Data

Reading entire files

The simplest way to read files in Ruby is using class methods that load content in a single operation. This approach works well for small to medium-sized files where you need the entire content available immediately.

read_files.rb
# Read entire file content
content = File.read('data.txt')
puts content

# Read and split into lines
lines = File.readlines('data.txt')
lines.each_with_index { |line, index| puts "#{index + 1}: #{line.chomp}" }

The File.read method loads the entire file into a string, while File.readlines splits content into an array where each element represents one line. The chomp method removes trailing newline characters for cleaner output formatting.

Run the reading example:

 
ruby read_files.rb
Output
Line 1: Hello World
Line 2: Ruby Files
Line 3: Processing Data
1: Line 1: Hello World
2: Line 2: Ruby Files
3: Line 3: Processing Data

This output shows how Ruby handles file content both as a single string and as individual lines, providing flexibility for different processing needs.

Writing files safely

Writing files requires consideration of whether you want to replace existing content or add to it. Ruby provides options for both scenarios, along with encoding controls for international text.

write_files.rb
# Write new content (overwrites existing)
File.write('output.txt', "New content here")

# Append to existing file
File.write('output.txt', "\nAppended line", mode: 'a')

# Write with explicit encoding
File.write('utf8_file.txt', "Unicode: 你好", encoding: 'utf-8')

puts "Files created successfully"

Ruby's File.write method handles file opening and closing automatically. The mode: 'a' parameter appends content instead of overwriting, while the encoding: option ensures proper character handling for international text.

Execute the writing example:

 
ruby write_files.rb
Output
Files created successfully

Ruby's class methods provide a foundation for basic file operations, but they load entire files into memory at once. When processing large files or implementing streaming workflows, you need more memory-efficient approaches that can handle data piece by piece without overwhelming system resources.

Block-based file processing

While Ruby's class methods handle simple file operations elegantly, block-based processing provides more control and memory efficiency. This approach is particularly valuable when working with large files or when you need to process content line by line without loading everything into memory.

Ruby's block syntax ensures files are automatically closed when processing completes, even if errors occur during execution.

process_lines.rb
# Process file line by line (memory efficient)
puts "Processing line by line:"
File.foreach('data.txt') do |line|
  puts "Processing: #{line.strip}"
end

puts "\nReading with manual control:"
File.open('data.txt', 'r') do |file|
  while line = file.gets
    puts "Line read: #{line.chomp}"
  end
end

The File.foreach method yields each line to the block without storing the entire file in memory. The File.open with a block provides manual control while maintaining automatic resource cleanup.

Run the block processing example:

 
ruby process_lines.rb
Output
Processing line by line:
Processing: Line 1: Hello World
Processing: Line 2: Ruby Files
Processing: Line 3: Processing Data

Reading with manual control:
Line read: Line 1: Hello World
Line read: Line 2: Ruby Files
Line read: Line 3: Processing Data

Block-based processing gives you memory efficiency and automatic cleanup, but before building complex file workflows, you need to understand what you're working with.

Checking file properties, permissions, and metadata prevents runtime errors and helps you make informed decisions about how to process different types of files.

File information and metadata

Before performing file operations, it's often necessary to check if files exist, verify permissions, or retrieve metadata like file size and modification dates. Ruby provides comprehensive methods for gathering file system information safely.

These checks help prevent common errors and enable defensive programming practices in file handling code.

file_info.rb
require 'pathname'

# Check basic file properties
puts "File existence and properties:"
puts "data.txt exists: #{File.exist?('data.txt')}"
puts "data.txt readable: #{File.readable?('data.txt')}"
puts "data.txt writable: #{File.writable?('data.txt')}"

# Get detailed file statistics
if File.exist?('data.txt')
  stat = File.stat('data.txt')
  puts "\nDetailed file information:"
  puts "Size: #{stat.size} bytes"
  puts "Modified: #{stat.mtime}"
  puts "Is directory: #{File.directory?('data.txt')}"
  puts "Is file: #{File.file?('data.txt')}"
end

# Using Pathname for object-oriented approach
path = Pathname.new('data.txt')
puts "\nPathname methods:"
puts "Absolute path: #{path.expand_path}"
puts "Extension: #{path.extname}"
puts "Basename: #{path.basename}"

The File.stat method returns detailed metadata, while boolean methods like File.exist? provide quick condition checks. The Pathname class offers object-oriented path manipulation.

Run the file information example:

 
ruby file_info.rb
Output
File existence and properties:
data.txt exists: true
data.txt readable: true
data.txt writable: true

Detailed file information:
Size: 55 bytes
Modified: 2024-08-27 14:30:15 -0700
Is directory: false
Is file: true

Pathname methods:
Absolute path: /Users/demo/ruby-files-demo/data.txt
Extension: .txt
Basename: data.txt

File existence and properties:
data.txt exists: true
data.txt readable: true
data.txt writable: true

Detailed file information:
Size: 63 bytes
Modified: 2025-08-27 09:25:54 +0200
Is directory: false
Is file: true

Pathname methods:
Absolute path: /Users/<username>/ruby-files-demo/data.txt
Extension: .txt
Basename: data.txt

File metadata checking protects individual file operations, but most applications work with collections of files organized in directories. Understanding how to navigate, list, and manage directory structures is essential for building scalable file processing systems that can handle entire folder hierarchies.

Directory operations

Working with directories is essential for organizing files and building scalable file processing workflows. Ruby's Dir class provides methods for listing, creating, and traversing directories, while pattern matching helps filter results effectively.

Understanding directory operations enables you to build applications that can process entire folder structures and organize files systematically.

First, create some sample directory structure and files:

setup_dirs.rb
# Create sample directory structure
Dir.mkdir('test_dir') unless Dir.exist?('test_dir')
Dir.mkdir('test_dir/subdir') unless Dir.exist?('test_dir/subdir')

# Create sample files
File.write('test_dir/file1.rb', 'puts "Ruby file 1"')
File.write('test_dir/file2.txt', 'Text file content')
File.write('test_dir/subdir/nested.rb', 'puts "Nested Ruby file"')

puts "Directory structure created successfully"

Now explore directory listing and traversal:

list_dirs.rb
# List directory contents
puts "Contents of test_dir:"
Dir.entries('test_dir').each { |entry| puts "  #{entry}" }

# Filter specific file types using glob patterns
puts "\nRuby files (*.rb):"
Dir.glob('test_dir/**/*.rb').each { |file| puts "  #{file}" }

# Get all files recursively
puts "\nAll files with sizes:"
Dir.glob('test_dir/**/*').select { |f| File.file?(f) }.each do |file|
  size = File.size(file)
  puts "  #{file} (#{size} bytes)"
end

# Count items by type
files = Dir.glob('test_dir/**/*').select { |f| File.file?(f) }
dirs = Dir.glob('test_dir/**/*').select { |f| File.directory?(f) }
puts "\nSummary: #{files.length} files, #{dirs.length} directories"

The Dir.glob method supports powerful wildcards like ** for recursive traversal and * for filename matching. The select method filters results based on file type.

Set up the directory structure:

 
ruby setup_dirs.rb
Output
Directory structure created successfully

List and analyze directories:

 
ruby list_dirs.rb
Output
Contents of test_dir:
  .
  ..
  file2.txt
  file1.rb
  subdir

Ruby files (*.rb):
  test_dir/file1.rb
  test_dir/subdir/nested.rb

All files with sizes:
  test_dir/file1.rb (18 bytes)
  test_dir/file2.txt (17 bytes)
  test_dir/subdir/nested.rb (23 bytes)

Summary: 3 files, 1 directories

Directory operations let you find and organize files, but real-world file processing requires robust error handling. Files can be missing, locked, or corrupted, and production applications must gracefully handle these situations while providing meaningful feedback to users about what went wrong.

Error handling and safe file operations

File operations can fail for various reasons including missing files, permission issues, or insufficient disk space. Ruby provides specific exception classes that help you handle different error conditions gracefully and provide meaningful feedback to users.

Screenshot of a diagram showing a error handling flow

Proper error handling prevents application crashes and enables recovery from common file system issues.

safe_operations.rb
def safe_file_read(filename)
  begin
    content = File.read(filename)
    puts "Successfully read #{content.length} characters from #{filename}"
    content
  rescue Errno::ENOENT
    puts "Error: File '#{filename}' not found"
    nil
  rescue Errno::EACCES
    puts "Error: Permission denied for '#{filename}'"
    nil
  rescue => e
    puts "Unexpected error reading '#{filename}': #{e.message}"
    nil
  end
end

# Test with different scenarios
puts "Testing file reading with error handling:"
safe_file_read('data.txt')           # Should succeed
safe_file_read('missing_file.txt')   # File not found
safe_file_read('/etc/shadow')        # Permission denied (on Unix systems)

Ruby's specific exception classes like Errno::ENOENT and Errno::EACCES allow targeted error handling. The generic rescue catches unexpected errors.

Now demonstrate safe file operations with validation:

validate_operations.rb
def safe_copy_file(source, destination)
  # Pre-flight checks
  unless File.exist?(source)
    return "Source file '#{source}' does not exist"
  end

  unless File.readable?(source)
    return "Cannot read source file '#{source}'"
  end

  if File.exist?(destination)
    return "Destination '#{destination}' already exists"
  end

  begin
    content = File.read(source)
    File.write(destination, content)
    "Successfully copied '#{source}' to '#{destination}'"
  rescue => e
    "Copy failed: #{e.message}"
  end
end

# Test copy operations
puts "Testing safe copy operations:"
puts safe_copy_file('data.txt', 'backup.txt')
puts safe_copy_file('missing.txt', 'backup2.txt')
puts safe_copy_file('data.txt', 'backup.txt')  # Already exists

This approach validates conditions before attempting operations and provides clear error messages for each failure scenario.

Run the safe operations example:

 
ruby safe_operations.rb
Output

Testing file reading with error handling:
Successfully read 63 characters from data.txt
Error: File 'missing_file.txt' not found
Error: File '/etc/shadow' not found

Test the validation example:

 
ruby validate_operations.rb
Output
Testing safe copy operations:
Successfully copied 'data.txt' to 'backup.txt'
Source file 'missing.txt' does not exist
Destination 'backup.txt' already exists

Safe file operations handle individual cases reliably, but when working with large file collections, you need sophisticated filtering capabilities. Applications often must find specific files based on naming patterns, dates, or content types from hundreds or thousands of candidates scattered across directory trees.

Pattern matching and file filtering

Beyond basic directory listing, applications often need to find files based on complex criteria like naming patterns, file types, or content characteristics. Ruby provides multiple approaches for filtering files, from simple string methods to powerful regular expressions and glob patterns.

Effective pattern matching enables automated file processing workflows and helps organize large collections of files systematically.

Create some sample files with various patterns:

create_samples.rb
# Create sample files with different patterns
sample_files = [
  'report_2024_01.pdf', 'report_2024_02.pdf', 'report_draft.pdf',
  'config.yml', 'database.yml', 'secrets.yml.example',
  'user_data.csv', 'sales_data.csv', 'backup_data.csv.old',
  'script.rb', 'test_script.rb', 'lib_utils.rb'
]

sample_files.each do |filename|
  File.write(filename, "Sample content for #{filename}")
end

puts "Created #{sample_files.length} sample files for pattern matching"

Now demonstrate different pattern matching techniques:

pattern_matching.rb
puts "=== String-based filtering ==="
# Find files ending with specific extensions
Dir.entries('.').select { |f| f.end_with?('.rb') }.each do |file|
  puts "Ruby file: #{file}"
end

puts "\n=== Glob pattern matching ==="
# Use glob patterns for flexible matching
puts "YAML files:"
Dir.glob('*.yml').each { |f| puts "  #{f}" }

puts "\nPDF reports (not drafts):"
Dir.glob('report_*.pdf').reject { |f| f.include?('draft') }.each do |file|
  puts "  #{file}"
end

puts "\n=== Regular expression filtering ==="
# Use regex for complex patterns
backup_pattern = /backup.*\.(csv|txt|sql)(\.|$)/
Dir.entries('.').select { |f| f.match?(backup_pattern) }.each do |file|
  puts "Backup file: #{file}"
end

puts "\n=== Combined criteria filtering ==="
# Filter by multiple conditions
large_recent_files = Dir.glob('*').select do |file|
  File.file?(file) && 
  File.size(file) > 20 &&
  File.extname(file) == '.csv'
end

puts "CSV files larger than 20 bytes:"
large_recent_files.each { |f| puts "  #{f} (#{File.size(f)} bytes)" }

The examples show different filtering approaches: string methods for simple cases, glob patterns for filename matching, and regular expressions for complex patterns.

Create the sample files:

 
ruby create_samples.rb
Output
Created 12 sample files for pattern matching

Run the pattern matching examples:

 
ruby pattern_matching.rb
Output

=== String-based filtering ===
Ruby file: create_samples.rb
Ruby file: safe_operations.rb
Ruby file: file_info.rb
Ruby file: process_lines.rb
Ruby file: script.rb
Ruby file: write_files.rb
Ruby file: list_dirs.rb
Ruby file: setup_dirs.rb
Ruby file: lib_utils.rb
Ruby file: validate_operations.rb
Ruby file: read_files.rb
Ruby file: test_script.rb
Ruby file: pattern_matching.rb

=== Glob pattern matching ===
YAML files:
  config.yml
  database.yml

PDF reports (not drafts):
  report_2024_01.pdf
  report_2024_02.pdf

=== Regular expression filtering ===
Backup file: backup.txt
Backup file: backup_data.csv.old

=== Combined criteria filtering ===
CSV files larger than 20 bytes:
  sales_data.csv (33 bytes)
  user_data.csv (32 bytes)

Pattern matching enables automated file discovery and organization workflows. With the ability to safely read, process, and filter files established, you now have the foundation for building complete file processing applications that can handle the diverse file management challenges found in real-world software development.

Final thoughts

Ruby's file handling capabilities reflect the language's core philosophy of making complex tasks approachable without sacrificing power. The built-in classes provide both simple methods for common operations and sophisticated tools for enterprise-level file processing workflows.

The combination of automatic resource management, expressive enumerable methods, and comprehensive error handling makes Ruby particularly well-suited for data processing applications and system administration tasks. Whether building configuration parsers, log analyzers, or file organization tools, these patterns provide a solid foundation for development.

For advanced file processing requirements, including handling binary data, network file systems, and specialized formats, explore the Ruby documentation.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.