Polyglot
Languages Ruby io
Ruby § io

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:

ModeDescription
"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.write for whole-file operations.
  • Use File.foreach for line-based processing (substantial efficiency for substantial files).
  • Use Pathname for substantial path manipulation.
  • Use FileUtils for higher-level file operations (mkdir_p, rm_rf, cp_r).
  • Use Tempfile.create and Dir.mktmpdir with blocks for temp resources.
  • Use Open3 for spawning subprocesses.
  • Use URI.open (with open-uri) for HTTP fetching; gems for substantial HTTP.
  • Use ARGF for Unix-style “files or stdin” iteration.
  • Use flock for 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.