Input & Output (IO) In Ruby: The Definitive Guide

I/O stands for Input/Output.

By input we mean all the data & information that comes into something (computer, Ruby method, your brain).

Examples of input:

  • Keys your press on the keyboard
  • Mouse click
  • Books you read

By output we mean everything that comes out as a result of the input.

Examples of output:

  • The result of 1 + 1
  • Writing a summary of an article you read
  • Coffee

In Ruby, when we talk about i/o we usually refer to reading files, working with network sockets & printing information on the screen.

Understanding The IO Class

IO is also a class in Ruby.

It’s the parent class for other objects like File & Socket.

All IO objects have these operations:

  • open / close
  • read
  • write

IO objects are based on file descriptors.

Every running process in your computer has an internal information table.

Part of this table includes a list of file descriptors.

A file descriptor is a mapping between an integer & a file name.

You can see this list of file descriptors in Linux.

With this command:

# Assuming you have irb open on another window

ls -lh /proc/`pgrep irb`/fd/

# lrwx------ 1 jesus jesus 64 Feb  3 15:54 0 -> /dev/pts/3
# lrwx------ 1 jesus jesus 64 Feb  3 15:54 1 -> /dev/pts/3
# lrwx------ 1 jesus jesus 64 Feb  3 15:54 2 -> /dev/pts/3

You can create a new file descriptor in Ruby with the sysopen method.

Example:

fd = IO.sysopen('/dev/null', 'w')

# 10

Now you can open a communication channel with this file descriptor (10).

Like this:

io = IO.new(fd)

And write to it:

io << 'orange'

Notice that IO doesn't care if your file descriptor maps to an actual file, a device (like a hard disk), or a network socket.

How to Open A File Descriptor For Writing

When you create a new file descriptor using sysopen, or open a file using File.open, you have a few options.

By default the communication is read-only.

You can specify a mode:

  • r (read, default)
  • w (write)
  • w+ (read & write)
  • a (append)

Example:

fd = IO.sysopen('/dev/tty1', 'w+')

This opens /dev/tty1 with read + write mode.

3 Standard IO Streams

When you open a new program (your editor, terminal, Ruby interpreter, etc.) it opens 3 channels of communication by default.

3 file descriptors.

They are:

  • stdin (input)
  • stdout (output)
  • stderr (errors)

In Ruby you can access these directly, using their constant form, or their global variable form.

Example:

$stdout << "abc\n"

When you write to $stdout, it's the same as calling the puts method.

Standard output is associated with your terminal.

lrwx------ 1 jesus jesus 64 Feb  3 15:54 0 -> /dev/pts/3

This /dev/pts/3 is a virtual device that your terminal reads from to display text on the screen.

Standard error is also used for output, but it gives your terminal a chance to present this content in a different way, like in a bigger font, or in a different color than regular text.

You can use the warn method to print to standard error.

Standard input is where user input is read from.

You can use the gets method to read from standard input.

How to Flush STDOUT & Disable Buffering

IO objects have an internal buffer.

A certain amount of input data will be saved before it can be read.

That's why sometimes you use puts...

But you don't see anything until the program ends, or the buffer is full!

You can disable buffering like this:

$stdout.sync = true

Or you can manually flush the buffer:

$stdout.flush

With the buffer disabled you'll see the output without having to wait.

Reading In Chunks

When you're reading from an IO object, you read until the stream ends with an END-OF-FILE (EOF) signal, the stream is closed, or you receive a specific amount of bytes.

For network programming & big files, it's helpful to read in small chunks.

You can read in chunks like this:

data = ""

until io.eof?
  data << io.read(2048)
end

With this code you can process data as it becomes available, without having to wait for the whole download to finish.

Notice that eof? will "block".

Blocking means that your code will go to sleep until the operating system signals that there is new data to be read.

How to Use IO Pipes For Two-Way Communication

Ruby allows you to create pipes.

A pipe is a pair of file descriptors that are connected to each other.

This is used for Inter-Process Communication (IPC).

Example:

read, write = IO.pipe

You can use this with the fork method (not available in Windows) to create a copy of the current process.

These two processes can communicate using the pipe.

Example:

if fork
  write.close

  puts "Message received: #{read.gets}"

  read.close
else
  read.close

  write << "Buy some bananas!"

  write.close
end

Which results in:

# Message received: Buy some bananas!

How to Use The Kernel Open Method

Ruby has a very flexible open method.

It allows you to open files & read data from a URL, and even open pipes to run external commands.

Example:

open "abc.txt"

That's why this method is to be avoided.

It's a security risk that it can do all these things.

Instead, it's better to use the specific open method that you need.

Example:

File.open           # File
IO.popen            # Process open
URI.parse(...).open # URL open

All of these return an IO object.

Summary

You've learned about input/output in Ruby! You've learned about file descriptors, the IO class, buffering, reading in chunks & standard IO streams.

Ruby IO Mindmap

Don't forget to share this article if you like it.

Thanks for reading.

6 thoughts on “Input & Output (IO) In Ruby: The Definitive Guide”

Comments are closed.