Reading and writing files

The whole file at once

For a small file there are two methods, and neither needs a handle: File.write replaces the contents and File.read returns them as one string:

ruby -e 'File.write("a.txt", "x\n"); puts File.read("a.txt")'

x

The string keeps the newline, so puts adds a second one. That is why a line read from a file normally goes through chomp before it is compared or printed:

ruby -e 'File.write("a.txt", "x\ny\n"); p File.readlines("a.txt", chomp: true)'

["x", "y"]

Append and open modes

File.write opens with mode "w", which truncates. Pass mode: "a" to add to the end instead:

ruby -e 'File.write("a.txt", "x\n"); File.write("a.txt", "y\n", mode: "a"); puts File.read("a.txt")'

x
y

The modes are the C ones: "r" reads, "w" writes and truncates, "a" appends, "r+" reads and writes without truncating, and a trailing "b" opens in binary mode, which lesson 121 needs. The mode is the difference between a log that grows and a log that is replaced by its last line.

Line by line

A file that does not fit in memory is read one line at a time with File.foreach. The block runs per line and the file is never held as a whole:

ruby -e 'File.write("a.txt", "x\ny\n"); File.foreach("a.txt") { |l| puts l.chomp }'

x
y

Without a block, File.foreach returns an enumerator, so the whole Enumerable toolbox applies and the file is still read lazily:

ruby -e 'File.write("a.txt", "x\ny\n"); p File.foreach("a.txt").count'

2

For a log of a million lines, this is the difference between a script that uses a few megabytes and one that uses a gigabyte. Combine it with the lazy enumerators from lesson 103 when the pipeline has several steps.

When the file is missing

Reading a file that is not there raises Errno::ENOENT, which is a SystemCallError and a StandardError. A file that may legitimately be absent is checked first:

ruby -e 'p File.exist?("nope.txt")'

false

The check and the open are two steps, and a file can disappear between them. When the absence is expected, rescue the specific error; when it is not, let it raise with the path in the message.

What to watch for

A write is not durable until the file is closed or flushed. The block forms close for you; a long-lived handle that must survive a crash needs f.flush or f.fsync.

A path built by joining strings breaks on Windows when the separator differs, and a relative path is relative to the working directory, not to the script. Both are the next lesson.

Next: Paths and directories

Article author: Arthur Isaev

Banner image