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