skip to content

In Ruby, what does IO#gets return at the end of a file, and what does its chomp: true keyword change?

level: middleimportance: should knowfreq 45%

answer

  1. the line includes its separator
  2. nil at end of file
  3. while (line = f.gets) idiom
  4. readline raises EOFError instead
  5. chomp: true strips \n and \r\n

basics

~20 s

IO#gets returns the next line including its trailing newline, and nil at end of file, which is why while (line = f.gets) terminates. With chomp: true the line separator, including a preceding carriage return, is removed.

solid answer

~40 s

On an open `File`, `gets` reads up to and including the next line separator (`$/`, normally `"\n"`) and returns it as a `String`; at end of file it returns `nil`, so `while (line = f.gets)` stops cleanly. It also assigns the line to `$_`. `gets(chomp: true)` drops the separator, and with the default separator also a `"\r"` before it, so Windows-style `"\r\n"` lines come back clean. A blank line then returns `""`, which is truthy in Ruby, so the loop still continues until the real `nil`. `IO#readline` reads the same way but raises `EOFError` at end of file instead of returning `nil`. `gets` also takes a separator (`nil` reads everything, `""` reads paragraphs) and a byte limit.

code

ruby · 13 lines
ruby
# orders.csv contains "id,total\r\n1,9.50\r\n\r\n2,12.00\r\n"
File.open("orders.csv") do |f|
  f.gets                    # => "id,total\r\n"
  f.gets(chomp: true)       # => "1,9.50"
  f.gets(chomp: true)       # => "" (blank line, still truthy)
  f.gets(chomp: true)       # => "2,12.00"
  f.gets                    # => nil at end of file
  begin
    f.readline
  rescue EOFError
    puts "readline raises at EOF"
  end
end

go deeper

for a junior

Recall that gets returns the line with its newline and nil at end of file, and that chomp: true removes the newline.

for a middle

Explain why while (line = f.gets) terminates, why a blank line does not stop it, how readline differs, and which other line methods accept chomp.

for a senior

Use readline where missing input is an error, cap line length with a limit on untrusted files, and normalise CRLF input with chomp rather than ad-hoc string surgery.

for a principal

Decide where a parser should fail fast on truncated input versus stop quietly, and make that choice explicit through readline versus gets.

## What IO#gets does `IO#gets` reads **one line** from an open stream — here a `File` you opened with `File.open` — and returns it. Its signatures, from the rdoc in `io.c`: - `gets(sep = $/, chomp: false)` → `String` or `nil` - `gets(limit, chomp: false)` → `String` or `nil` - `gets(sep, limit, chomp: false)` → `String` or `nil` By default the separator is `$/`, the input record separator, which is `"\n"`. The returned string **includes** that separator: the first call on a file whose first line is `First line` returns `"First line\n"`. `gets` also assigns its result to the special variable `$_`. ## End of file: nil, not an exception When nothing is left, `gets` returns **`nil`**. That is what makes the classic loop work: ```ruby File.open("orders.csv") do |f| header = f.gets(chomp: true) while (line = f.gets(chomp: true)) process(header, line) end end ``` The assignment inside the `while` condition evaluates to the line, and the loop ends on `nil`. Two details interviewers check: 1. **A blank line is not end of file.** With `chomp: true`, an empty line comes back as `""`. In Ruby only `nil` and `false` are falsy, so `""` keeps the loop going. 2. **`readline` is the raising twin.** `IO#readline` accepts the same arguments but raises `EOFError` at end of file. Use it when running out of input is an error, as in "the header line must exist". ## What chomp: true changes | Call | Line `"abc\n"` | Line `"abc\r\n"` | Blank line | At EOF | |---|---|---|---|---| | `f.gets` | `"abc\n"` | `"abc\r\n"` | `"\n"` | `nil` | | `f.gets(chomp: true)` | `"abc"` | `"abc"` | `""` | `nil` | | `f.readline` | `"abc\n"` | `"abc\r\n"` | `"\n"` | raises `EOFError` | With the default newline separator, `chomp: true` also removes a carriage return in front of the newline, so files written on Windows do not leave a stray `"\r"` on every value. The same keyword exists on `IO#each_line`, `IO#readlines`, `File.foreach` and `File.readlines`, so you rarely need to call `String#chomp` on each line yourself. ## Separator and limit arguments - **Custom separator.** `f.gets(";")` reads up to and including the next semicolon. - **`nil` separator.** `f.gets(nil)` reads the rest of the stream as one string. - **Empty-string separator.** `f.gets("")` is paragraph mode: it reads up to a run of blank lines. - **Limit.** `f.gets(1024)` returns at most about 1024 bytes, even if the line is longer; the next call continues where it stopped. This guards against a single enormous line. ## Line numbers for error messages Every successful `gets` (and every line `each_line` yields) increments the stream's line counter, readable with `IO#lineno`. That makes precise error messages cheap: ```ruby File.open("orders.csv") do |f| header = f.readline(chomp: true) while (line = f.gets(chomp: true)) next if line.empty? id, total = line.split(",") raise ArgumentError, "orders.csv:#{f.lineno}: missing total" if total.nil? end end ``` Here `readline` makes an empty file fail loudly at the header, `line.empty?` skips blank lines without ending the loop, and `f.lineno` names the exact line of a bad record. `IO#rewind` resets the counter to zero along with the position. ## gets versus each_line `gets` gives you **pull** control: read a header, skip a line, stop early, or interleave reads with other logic. `each_line` is **push**: it yields every remaining line and returns the file. Both read through the same buffer and both stream, so neither loads the file; pick whichever reads more clearly. ## Common mistakes - Comparing a `gets` result to a string without chomping: `line == "END"` is false for `"END\n"`. - Expecting `gets` to raise at end of file and wrapping the loop in `rescue EOFError`; that is `readline`'s behaviour.

  • In Ruby, when would you choose IO#readline over IO#gets?
    When reaching end of file is a bug rather than the normal way to stop. `readline` raises `EOFError` instead of returning `nil`, so reading a required header with `f.readline(chomp: true)` fails loudly on an empty file instead of passing `nil` into code that expects a string.
  • Why is gets(limit) useful on input you do not control?
    Without a limit, one line can be arbitrarily long: a file with no newlines is a single line of its full size. `f.gets(65_536)` returns at most about that many bytes and leaves the rest for the next call, so a malformed or hostile file cannot force one huge allocation.

saying these in an interview costs you the question

  • IO#gets raises EOFError when the file has no more lines.
  • IO#gets strips the trailing newline from each line by default.
  • A blank line returns nil from gets, so the while loop stops early.
  • chomp: true leaves the carriage return from Windows line endings.
  • gets reads the whole file into memory and returns one line at a time.