In Progress
Unit 1, Lesson 1
In Progress

Read and Write

In this episode, you’ll learn concise shortcuts for some common file-reading and file-writing scenarios.

Video transcript & code

Today's topic is a little tip about reading and writing files in Ruby, with the minimum of fuss and bother. It's actually something you've seen used in many RubyTapas episodes already. But it's something I haven't drawn attention to, and from my experience of reading Ruby code in the wild, it's something not everyone knows about.

Let's say you need to open a file, read in its contents, update the contents, and then write the contents out to a new file. How would you go about this?

Maybe you do it in the traditional, procedural way. First, you open the input file. Then, you read it in.. Then you close the input file.

Next you update the text of the file (for the sake of example, we'll just reverse it). You open up the output file , write the modified text to it , and close the file.

infile = File.open("input.txt", "r")
text   = infile.read
infile.close
text.reverse!
outfile = File.open("output.txt", "w")
outfile.write(text)
outfile.close

Or, maybe you're accustomed to the more idiomatic Ruby block form of open. We supply blocks to the File.open sends, doing our reading and writing inside the blocks. Closing the files is handled for us in the block version, so we can remove the explicit close invocations.

text = File.open("input.txt", "r") do |infile|
  infile.read
end
text.reverse!
File.open("output.txt", "w") do |outfile|
  outfile.write(text)
end

Perhaps you also prefer to omit the File class receiver for the open message, and rely on the open alias provided globally by the Kernel module instead.

text = open("input.txt", "r") do |infile|
  infile.read
end
text.reverse!
open("output.txt", "w") do |outfile|
  outfile.write(text)
end

There's nothing wrong with any of these approaches. But if all we need to do is read a file's contents into a variable, or write a variable out to a file, even the version we have here is way more code than necessary.

Because instead of opening files and then reading or writing, we can just use the IO.read and IO.write methods.

text = IO.read("input.txt")
text.reverse!
IO.write("output.txt", text)

These methods combine the open, the read or write operation, and the close into a single message send.

In past episodes you might have seen me use File.read and File.write instead of IO.read and IO.write.

text = File.read("input.txt")
text.reverse!
File.write("output.txt", text)

Wondering what the difference is? The answer is: Absolutely nothing. Why? Because the File class is a child of the IO class, and these methods are inherited unchanged.

File.superclass                 # => IO

So why do I sometimes use the File versions instead of the IO versions? Quite honestly, because I'm using them to read and write files, and so I usually think of the File class first and forget that these methods are actually defined on IO. I might even argue that using the File class as the receiver is a little more intention-revealing.

Without a doubt, these methods are shortcuts for a very specific, simple type of file I/O scenario. However, don't let the fact that they are intended for a simple case lead you to believe that they are completely inflexible. We can pass in most of the usual extra options we might otherwise pass to the open method.

For instance, we can specify that the files should be opened in binary mode.

text = File.read("input.txt", mode: "rb")
text.reverse!
File.write("output.txt", text, mode: "wb")

Note that the read-or-write part of the mode must agree with the method being invoked, or Ruby will get upset with us.

text = File.read("input.txt", mode: "wb") # ~> IOError: not opened for reading

# ~> IOError
# ~> not opened for reading
# ~>
# ~> xmptmp-in7484KuG.rb:1:in `read'
# ~> xmptmp-in7484KuG.rb:1:in `<main>'

If you get into the habit of using these methods, there's one bit of memorization you might have trouble with. Namely, since IO.write takes two required arguments, both of which are strings, you might have trouble remembering which one comes first: the name of the file, or the contents to be written.

IO.write(string1, string2)

My advice is to think of it this way: first, remember which argument comes first for IO.read. This one is easy, because IO.read only has one required argument: the name of the file.

Then just remember that IO.write takes its arguments in the same order as IO.read. So the filename must come first.

text = IO.read("input.txt")
text.reverse!
IO.write("output.txt", text)

And that's how you can eliminate a bunch of tedious boilerplate code from your simplest file reads and writes. Happy hacking!

Responses