Read and Write
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