Kernel#open
open(path, mode="r") -> file or io The open method is versatile, opening files for reading/writing and also creating IO pipes for command execution. It’s a core method for file and process interaction.
Opening Files
# Read mode (default)
file = open("data.txt")
content = file.read
file.close
# Block form (auto-closes)
open("data.txt") { |f| f.read }
The first example uses the manual form, where you are responsible for calling close. The second uses the block form, where Ruby calls close automatically when the block exits, even if an exception is raised during the block body. The block form is almost always the safer choice for production code because it guarantees resource cleanup.
File Modes
# Various modes
open("file.txt", "r") # Read
open("file.txt", "w") # Write (truncates)
open("file.txt", "a") # Append
open("file.txt", "r+") # Read/write
open("file.txt", "w+") # Read/write (truncates)
open("file.txt", "a+") # Read/append
Each mode string controls how the file is opened and what operations are allowed. "r" is the default and only permits reading. "w" creates or truncates the file for writing, while "a" appends to an existing file without erasing its contents. The + variants add read or write capability to the base mode, giving you bidirectional access when needed.
Command execution with pipes
# Read output from command
output = open("|ls -la") { |p| p.read }
puts output
# Write to command
open("|tr a-z A-Z", "w") { |p| p.puts "hello world" }
A leading pipe character | in the filename tells open to spawn a subprocess instead of opening a file. The return value is an IO object connected to the process’s stdin or stdout. This gives you a concise way to shell out for data conversion, filtering, or system interrogation without managing file descriptors by hand.
Practical Examples
Reading Files
# Line by line
open("file.txt") do |f|
f.each_line { |line| puts line }
end
# All content
content = open("file.txt", &:read)
The first approach reads incrementally and works well with large files because only one line is in memory at a time. The second reads the entire file contents into a string, which is fine for small files but can cause memory pressure with very large inputs. Choose the streaming approach when file size is unknown or unbounded.
Writing Files
# Write data
open("output.txt", "w") do |f|
f.puts "Hello"
f.puts "World"
end
# Append to file
open("log.txt", "a") do |f|
f.puts "#{Time.now}: Log entry"
end
The "w" mode creates a fresh file each time, overwriting any previous content. The "a" mode preserves existing content and adds new lines at the end, making it ideal for log files and audit trails. Using the block form in both cases ensures the file handle is released as soon as the block finishes.
Processing CSV
# Simple CSV reading
open("data.csv", "r") do |f|
f.each_line do |line|
fields = line.chomp.split(",")
# Process fields
end
end
The block processes one line at a time, splitting on commas to produce field arrays. For simple CSV files without quoted fields or embedded commas, this approach is fast and requires no extra dependencies. For production CSV work, the csv standard library handles edge cases like quoted values and multi-line fields.
Network-like Operations
# Reading from URL (with open-uri)
require 'open-uri'
open("https://example.com") { |f| puts f.read }
Loading open-uri patches open to accept HTTP and HTTPS URLs, so the same block-based API works for both local files and remote resources. The IO object behaves the same way regardless of the source, which means your processing logic can stay unchanged whether the input comes from disk or the network.
Binary Files
# Read binary data
open("image.png", "rb") do |f|
data = f.read
# Process binary
end
# Write binary
open("output.bin", "wb") do |f|
f.write(bytes)
end
Binary mode ("rb" and "wb") prevents Ruby from applying encoding conversions or line-ending translations to the data. This is essential for images, audio, compressed files, and any format where every byte matters. Without the b flag, Ruby may alter line endings on Windows or misinterpret bytes as multi-byte character sequences.
With Encoding
# Specify encoding
open("file.txt", "r:UTF-8") { |f| f.read }
open("file.txt", "r:UTF-16LE:UTF-8") { |f| f.read }
The encoding annotation in the mode string tells Ruby how to interpret the file’s bytes. The first form reads the file as UTF-8. The second reads it as UTF-16LE and transcodes to UTF-8 on the fly, letting you work with external data in its native encoding while keeping your program’s internal strings consistent.
Error Handling
begin
file = open("missing.txt")
# Process
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
ensure
file.close if file
end
The ensure clause guarantees that close runs regardless of whether an exception occurs. Without it, a missing file would leave a stale handle if one existed from a previous open. The block form of open handles this cleanup automatically, which is why the next section recommends it as the default approach for most file work.
Prefer the block form
open is easy to reach for, but the block form is usually the safer default because it closes the file as soon as the work is done. That matters when you are opening many files or when an exception could interrupt the code before close runs. For command pipes, the same idea helps keep the resource lifetime short and obvious. When you need binary mode or a specific encoding, spell it out so the call site shows exactly how the stream should behave. Small details here prevent confusing file bugs later.
It is also worth remembering that open can hide a few different behaviors behind the same name, so the surrounding code should make the intent obvious. If the call is reading a file, piping to a command, or working with a network source through open-uri, the mode and block shape should make that clear. Explicit code is easier to debug when the input source or output target changes, which is common in small scripts that grow over time.
When the code needs only a quick read or write, open can still be a fine fit, but the call site should show the lifetime of the resource as clearly as possible. That usually means a block, a mode string, and a short body that does one thing. The cleaner that boundary is, the easier it is to reason about files, pipes, and cleanup together.
That shape also makes errors less mysterious, because the file or pipe is usually opened and closed in the same visible span of code. When a bug does show up, the resource lifetime is easier to trace and the cleanup logic is much less likely to drift away from the work it supports.
The open method is fundamental to Ruby’s IO system, supporting everything from simple file operations to complex pipe-based inter-process communication.
# Open a log file and filter lines containing errors
error_lines = open("app.log", "r") do |f|
f.each_line.select { |line| line.include?("ERROR") }
end
puts "Found #{error_lines.size} error lines"