I/O
Ruby I/O is built on the IO class — the abstract base for file, socket, and pipe operations. The principal subclass is File (file system access). Standard streams (STDIN, STDOUT, STDERR) are IO instances accessible globally. The conventional discipline favours block-form methods (File.open(path) { |f| ... }, File.foreach, File.read) over manual open/close — admits automatic cleanup. The combination — IO as the foundation, File for filesystem operations, the substantial helper methods (File.read, File.write, File.foreach), the conventional block-form for resource management — is the substance of Ruby’s I/O surface.
Reading files
The conventional helpers:
# Read whole file as string:
content = File.read("file.txt")
content = File.read("file.txt", encoding: "UTF-8")
# Read whole file as bytes:
bytes = File.binread("file.bin")
# Read into an array of lines:
lines = File.readlines("file.txt")
lines = File.readlines("file.txt", chomp: true) # without trailing newlines
# Streaming line iteration (efficient for substantial files):
File.foreach("file.txt") do |line|
puts line
end
File.foreach("file.txt", chomp: true) do |line|
puts line
end
The File.foreach is the conventional choice for line-based processing of substantial files — admits substantial efficiency without loading the whole file.
File.open with blocks
For substantial control, File.open:
File.open("file.txt", "r") do |f|
while line = f.gets
puts line
end
end # closed automatically
The block form admits automatic cleanup — f.close is called on block exit (even if an exception is raised).
The mode argument:
| Mode | Description |
|---|---|
"r" | read (default) |
"w" | write (truncate) |
"a" | append |
"r+" | read and write |
"w+" | read and write (truncate) |
"a+" | read and write (append) |
"rb", "wb" | binary |
"rt", "wt" | text (default on most systems) |
Writing files
# Whole-file write:
File.write("file.txt", "content")
File.write("file.txt", "content", mode: "a") # append
File.binwrite("file.bin", binary_data)
# Streaming write:
File.open("output.txt", "w") do |f|
f.write("line 1\n")
f.write("line 2\n")
f.puts "line 3" # adds newline
f.puts ["line 4", "line 5"] # array of lines
end
IO operations
The principal IO methods (also on File):
# Reading:
io.read # entire content
io.read(100) # 100 bytes
io.gets # one line (or nil at EOF)
io.readline # one line (raises at EOF)
io.readlines # array of lines
io.readchar # one character
io.each_line { |line| ... }
io.each_char { |c| ... }
io.each_byte { |b| ... }
# Writing:
io.write("data") # returns bytes written
io.print "no newline"
io.puts "with newline"
io.printf "formatted %d", 42
io << "hello" # alias for write
# Position:
io.pos # current byte position
io.pos = 100 # seek
io.seek(100, IO::SEEK_SET) # absolute
io.seek(50, IO::SEEK_CUR) # relative
io.seek(0, IO::SEEK_END) # to end
io.rewind # to start
io.eof? # at end?
io.size # total size
# Closing:
io.close
io.closed?
Standard streams
STDIN.read # read all of stdin
STDIN.gets # one line
STDIN.each_line { |line| process(line) }
STDOUT.puts "output"
STDOUT.write "no newline"
STDERR.puts "error message"
$stdout.puts "alias for STDOUT"
$stderr.puts "alias for STDERR"
# In a script:
ARGF.each_line { |line| puts line } # iterates over files in ARGV (or stdin)
The ARGF (also called $<) admits Unix-style file iteration: if ARGV has files, iterates them; otherwise reads stdin.
ruby script.rb file1.txt file2.txt # reads from files
echo "input" | ruby script.rb # reads from stdin
Pathname for path manipulation
The Pathname class admits object-oriented path operations:
require "pathname"
p = Pathname.new("/etc/hosts")
p.basename # #<Pathname:hosts>
p.dirname # #<Pathname:/etc>
p.extname # ""
p.absolute? # true
p.exist?
p.file?
p.directory?
p.executable?
p + "subdir" + "file.txt" # path joining via +
# #<Pathname:/etc/hosts/subdir/file.txt> (not literal; see below)
p = Pathname.new("/etc")
(p + "hosts").read # file contents
# Path operations:
Pathname.new(".").realpath # absolute, symlinks resolved
Pathname.new("file").expand_path # absolute relative to current dir
Pathname.glob("*.rb") # array of matching paths
# Iteration:
Pathname.new("dir").each_child { |child| puts child }
Pathname.new("dir").find { |path| puts path } # recursive
The Pathname admits substantial fluent path manipulation; conventional in modern Ruby for substantial path work.
Directory operations
Dir.entries(".") # all entries (including . and ..)
Dir.children(".") # all entries (excluding . and ..)
Dir.glob("*.rb") # array of matching paths
Dir["*.{rb,txt}"] # alternative syntax
Dir.glob("**/*.rb") # recursive
Dir.exist?("path")
Dir.empty?("path")
Dir.mkdir("new_dir") # single directory
Dir.mkdir("new_dir", 0755) # with permissions
Dir.chdir("/tmp") # change current directory
Dir.chdir("/tmp") do
# work in /tmp; auto-restore on block exit
end
Dir.pwd # current directory
Dir.foreach(".") { |entry| puts entry } # iterate
For higher-level operations, the FileUtils (treated below).
FileUtils for higher-level operations
require "fileutils"
FileUtils.mkdir("dir")
FileUtils.mkdir_p("a/b/c") # like mkdir -p (creates all intermediate)
FileUtils.rm("file")
FileUtils.rm_rf("dir") # like rm -rf
FileUtils.cp("src", "dest")
FileUtils.cp_r("src_dir", "dest_dir") # recursive
FileUtils.mv("old", "new")
FileUtils.touch("file.txt")
FileUtils.chmod(0644, "file.txt")
FileUtils.chmod_R(0755, "dir")
FileUtils.chown("user", "group", "file")
FileUtils.ln_s("target", "link") # symlink
The FileUtils admits substantial Unix-style file operations.
File inspection
File.exist?("file.txt")
File.file?("path") # is regular file?
File.directory?("path")
File.symlink?("path")
File.executable?("path")
File.readable?("path")
File.writable?("path")
File.zero?("file") # empty?
File.size("file")
File.size?("file") # size or nil if 0/missing
# Times:
File.atime("file") # access time
File.mtime("file") # modification time
File.ctime("file") # change time
# Permissions:
File.stat("file").mode # mode bits
File.chmod(0644, "file")
# Symbolic link operations:
File.symlink?("link")
File.readlink("link") # target path
Tempfiles
require "tempfile"
# With a block (auto-cleanup):
Tempfile.create("prefix") do |f|
f.write("temporary data")
f.flush
process(f.path)
end
# Or manual:
file = Tempfile.new("prefix")
begin
file.write("data")
process(file.path)
ensure
file.close
file.unlink
end
# Temp directory:
require "tmpdir"
Dir.mktmpdir do |dir|
# work in a temp directory; auto-cleaned
File.write("#{dir}/file.txt", "data")
end
Pipes and Open3
# Reading from a command:
output = `ls -la` # backtick
output = %x{ls -la} # alternative
# system — runs command, returns true/false:
if system("git", "status")
puts "succeeded"
end
# IO.popen for streaming:
IO.popen("long_running") do |io|
io.each_line { |line| puts line }
end
# Open3 for separate stdin/stdout/stderr:
require "open3"
# Capture all:
stdout, stderr, status = Open3.capture3("git", "status")
# Streaming:
Open3.popen3("command") do |stdin, stdout, stderr, wait_thr|
stdin.write("input\n")
stdin.close
stdout.each_line { |line| puts line }
status = wait_thr.value
end
Sockets and networking
require "socket"
# TCP client:
TCPSocket.open("example.com", 80) do |s|
s.write "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
puts s.read
end
# TCP server:
server = TCPServer.new(8080)
loop do
client = server.accept
client.write "Hello\n"
client.close
end
# UDP:
udp = UDPSocket.new
udp.bind("0.0.0.0", 8080)
data, addr = udp.recvfrom(1024)
udp.send("response", 0, addr[3], addr[1])
For HTTP, Net::HTTP (treated in Standard library) admits a higher-level interface.
Encoding
content = File.read("file.txt", encoding: "UTF-8")
content = File.read("file.txt", encoding: "ISO-8859-1")
content = File.read("file.txt", encoding: "binary") # raw bytes
# External and internal encoding:
File.open("file.txt", "r:UTF-8") # external = UTF-8
File.open("file.txt", "r:UTF-16:UTF-8") # external = UTF-16, internal = UTF-8
# String methods:
s.encoding # current encoding
s.force_encoding("UTF-8") # reinterpret bytes
s.encode("ASCII-8BIT") # convert
Common patterns
Read whole file
content = File.read("file.txt")
Read line-by-line
File.foreach("file.txt") do |line|
process(line.chomp)
end
Write atomically
def atomic_write(path, content)
tmp = "#{path}.tmp"
File.write(tmp, content)
File.rename(tmp, path)
end
The pattern admits “either the new content is fully written or the old file remains”.
Process CSV file
require "csv"
CSV.foreach("data.csv", headers: true) do |row|
puts "#{row['name']}: #{row['age']}"
end
Write CSV file
CSV.open("out.csv", "w") do |csv|
csv << ["name", "age"] # header
records.each { |r| csv << [r.name, r.age] }
end
Read and parse JSON
require "json"
data = JSON.parse(File.read("config.json"), symbolize_names: true)
Stream stdin to stdout
ARGF.each_line { |line| puts line.upcase }
The ARGF admits Unix-style “files in ARGV or stdin”.
Find files matching pattern
ruby_files = Dir.glob("**/*.rb")
recent = Dir.glob("**/*.log").select { |f| File.mtime(f) > Time.now - 86400 }
Create directory if needed
require "fileutils"
FileUtils.mkdir_p("logs")
Read from a URL
require "open-uri"
content = URI.open("https://example.com/data.json").read
data = JSON.parse(content)
The open-uri extends Kernel#open to admit URLs:
open("https://example.com") { |f| puts f.read }
(The conventional contemporary discipline uses URI.open rather than open directly, since the latter admits both URLs and file paths — substantial security implications for untrusted input.)
Lock a file
File.open("data.txt", "r+") do |f|
f.flock(File::LOCK_EX) # exclusive lock
# ... read and modify ...
# lock released on close
end
Pipe with redirection
IO.popen(["sort"], "r+") do |io|
io.write("apple\nbanana\ncherry\n")
io.close_write
puts io.read
end
Walk a directory tree
require "find"
Find.find("dir") do |path|
next if File.directory?(path)
puts path if path.end_with?(".rb")
end
Or with Pathname:
Pathname.new("dir").find do |path|
next if path.directory?
puts path if path.extname == ".rb"
end
Copy with progress
File.open(src, "rb") do |from|
File.open(dest, "wb") do |to|
chunk_size = 64 * 1024
total = File.size(src)
copied = 0
while chunk = from.read(chunk_size)
to.write(chunk)
copied += chunk.bytesize
print "\r#{copied}/#{total} bytes"
end
end
end
Tail a log file
File.open("app.log") do |f|
f.seek(0, IO::SEEK_END)
loop do
line = f.gets
if line
puts line
else
sleep 0.5
end
end
end
The pattern admits Unix-style tail -f behaviour.
A note on the conventional discipline
The contemporary Ruby I/O advice:
- Use block forms (
File.open { ... }) for automatic cleanup. - Use
File.read,File.writefor whole-file operations. - Use
File.foreachfor line-based processing (substantial efficiency for substantial files). - Use
Pathnamefor substantial path manipulation. - Use
FileUtilsfor higher-level file operations (mkdir_p, rm_rf, cp_r). - Use
Tempfile.createandDir.mktmpdirwith blocks for temp resources. - Use
Open3for spawning subprocesses. - Use
URI.open(withopen-uri) for HTTP fetching; gems for substantial HTTP. - Use
ARGFfor Unix-style “files or stdin” iteration. - Use
flockfor file locking. - Specify encoding explicitly for non-UTF-8 content.
The combination — IO as the foundation, File and Dir for filesystem operations, Pathname for path manipulation, FileUtils for higher-level file work, the standard streams, the substantial block-form conventions for cleanup — is the substance of Ruby’s I/O surface. The discipline produces concise, safe, expressive I/O code with substantial automation around resource management.