Intermediate

I/O Operations in Ruby

Learn console input and output, formatted printing, file reading and writing, and I/O error handling in Ruby with Docker-ready examples

Input and output are where a program meets the outside world—reading what a user types, writing results to the screen, and persisting data to files. Ruby treats all of these through the same lens as everything else in the language: I/O is done with objects. The console is represented by the IO objects $stdin and $stdout, files are File objects (a subclass of IO), and familiar methods like puts and gets are just convenient shortcuts that delegate to them.

Because Ruby blends object-oriented and functional styles, its most idiomatic I/O pattern is passing a block to a method like File.open or File.foreach. The method opens the resource, yields it to your block, and guarantees it is closed afterward—even if an exception is raised. Where other languages rely on manual close() calls or special syntax like try-with-resources, Ruby gets automatic cleanup from an ordinary block.

The Hello World tutorial already introduced basic output with puts, print, and p. This tutorial goes further: formatted output with printf and format, reading user input from standard input with gets, writing and reading files several idiomatic ways, and handling I/O errors with begin/rescue/ensure.

Formatted Output

Beyond plain puts, Ruby offers C-style formatting through printf, the format method (also spelled sprintf), and the % operator on strings. All three use the same format specifiers: %s for strings, %d for integers, %f for floats, with optional width, precision, and alignment flags.

Create a file named formatted_output.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
name = "Ada"
score = 92.5

puts "Standard puts output"

# printf writes directly to standard output
printf("Name: %s, Score: %.1f\n", name, score)

# format returns a string instead of printing it
# %-10s left-justifies in 10 columns; %8.2f right-justifies in 8
formatted = format("%-10s|%8.2f|", name, score)
puts formatted

# The % operator formats a string against its arguments
puts "Line %04d" % 7

The difference between the three is where the result goes: printf prints immediately, while format and % return a string you can store, pass around, or print later. The width flags (%-10s, %8.2f, %04d) are handy for lining up columns in reports and tables.

Reading Console Input

The gets method reads one line from standard input, including the trailing newline. Chaining .chomp strips that newline—so gets.chomp is one of the most common idioms in Ruby. Input always arrives as a string; convert it with .to_i or .to_f when you need a number.

Create a file named console_input.rb:

1
2
3
4
5
6
7
8
puts "What is your name?"
name = gets.chomp

puts "How many years have you used Ruby?"
years = gets.chomp.to_i

puts "Hello, #{name}!"
puts "That's about #{years * 12} months of Ruby."

Note that .to_i never raises an error: "30".to_i is 30, but "abc".to_i is simply 0. That makes quick scripts forgiving, though you should validate input when correctness matters (Ruby also offers Integer("abc"), which raises an ArgumentError instead of silently returning 0).

Writing Files

Ruby gives you a spectrum of file-writing tools, from the one-line File.write for simple cases to File.open with a block for full control. The block form is the idiomatic choice when writing multiple times: the file closes automatically when the block ends.

Create a file named file_write.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Write a whole string in one call (creates or overwrites the file)
File.write("gemstones.txt", "ruby\npearl\nopal\n")

# Open in append mode ("a") with a block -- closed automatically
File.open("gemstones.txt", "a") do |file|
  file.puts "garnet"
  file.puts "topaz"
end

puts "Wrote #{File.size("gemstones.txt")} bytes to gemstones.txt"

The mode string works like C’s fopen: "w" truncates and writes, "a" appends, "r" reads (the default). Inside the block, the File object responds to the same output methods you already know—puts, print, write—because File inherits them from IO.

Reading Files

Reading mirrors writing: File.read slurps the whole file into one string, File.readlines returns an array of lines, and File.foreach streams the file one line at a time without loading it all into memory—the right choice for large files.

Create a file named file_read.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Read the entire file into one string
contents = File.read("gemstones.txt")
puts "The file has #{contents.length} characters"

# Read into an array of lines, stripping newlines
lines = File.readlines("gemstones.txt", chomp: true)
puts "First gemstone: #{lines.first}"
puts "Total gemstones: #{lines.length}"

# Stream line by line without loading the whole file
File.foreach("gemstones.txt").with_index(1) do |line, number|
  puts "#{number}: #{line.chomp}"
end

This example expects gemstones.txt to exist, so run file_write.rb first. Notice the functional flavor of the last example: File.foreach returns an enumerator, so it chains with with_index just like any other Ruby collection—no explicit loop counter needed.

Handling I/O Errors

File operations fail in the real world: files go missing, permissions are wrong, disks fill up. Ruby raises exceptions for these—a missing file raises Errno::ENOENT. Handle them with begin/rescue, and use ensure for cleanup code that must run no matter what. You can also avoid some exceptions entirely by asking first with File.exist?.

Create a file named io_errors.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
begin
  contents = File.read("missing.txt")
  puts contents
rescue Errno::ENOENT
  puts "Error: missing.txt does not exist"
ensure
  puts "Cleanup always runs"
end

# Check before opening to avoid the exception entirely
if File.exist?("gemstones.txt")
  puts "gemstones.txt is #{File.size("gemstones.txt")} bytes"
else
  puts "gemstones.txt not found"
end

The Errno family maps operating system error codes to exception classes (Errno::ENOENT for a missing file, Errno::EACCES for a permission error), and all of them inherit from SystemCallError, so you can rescue that class to catch any OS-level I/O failure at once.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Pull the official Ruby Alpine image (lightweight)
docker pull ruby:3.4-alpine

# Formatted output
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby formatted_output.rb

# Console input: -i keeps stdin open so we can pipe answers in
printf "Matz\n30\n" | docker run -i --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby console_input.rb

# File writing, then reading (order matters -- file_read.rb needs gemstones.txt)
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby file_write.rb
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby file_read.rb

# Error handling
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby io_errors.rb

Because the current directory is mounted at /app, gemstones.txt is written to your host machine and persists between the separate docker run commands.

Expected Output

formatted_output.rb

Standard puts output
Name: Ada, Score: 92.5
Ada       |   92.50|
Line 0007

console_input.rb

With Matz and 30 piped in as answers:

What is your name?
How many years have you used Ruby?
Hello, Matz!
That's about 360 months of Ruby.

file_write.rb

Wrote 29 bytes to gemstones.txt

file_read.rb

The file has 29 characters
First gemstone: ruby
Total gemstones: 5
1: ruby
2: pearl
3: opal
4: garnet
5: topaz

io_errors.rb

Error: missing.txt does not exist
Cleanup always runs
gemstones.txt is 29 bytes

Key Concepts

  • I/O is object-oriented: the console and files are all IO objects, so puts, print, and write work the same on $stdout and on an open File
  • gets includes the newlinegets.chomp is the standard idiom for reading a clean line of input, and .to_i/.to_f convert it to a number
  • Prefer the block form of File.open: the file is closed automatically when the block ends, even if an exception is raised mid-write
  • Pick the right reading tool: File.read for a whole file as one string, File.readlines for an array of lines, File.foreach to stream large files line by line
  • File.foreach returns an enumerator, so it composes with methods like with_index—Ruby’s functional side applied to I/O
  • Formatted output comes three ways: printf prints directly, while format and the % operator return a string using the same %s/%d/%f specifiers
  • I/O failures raise exceptions such as Errno::ENOENT; rescue them with begin/rescue, put mandatory cleanup in ensure, or pre-check with File.exist?
  • .to_i silently returns 0 for non-numeric input; use Integer(string) when you want invalid input to raise an error instead

Running Today

All examples can be run using Docker:

docker pull ruby:3.4-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining