In Ruby, why does a script run as `ruby greet.rb Alice` fail with Errno::ENOENT when it calls gets to read a reply?
answer
- gets is not $stdin.gets
- ARGF: files named in ARGV
- falls back to $stdin when empty
- $stdin.gets, or empty ARGV first
- nil at end of input
basics
~20 sKernel#gets reads from ARGF, which treats every entry in ARGV as a file to read and uses $stdin only when ARGV starts out empty. With Alice in ARGV, gets tries to open a file named Alice. Call $stdin.gets instead.
solid answer
~40 s`Kernel#gets` is not `$stdin.gets`: it reads from **`ARGF`**, the stream that concatenates the files named on the command line. When `ARGF` needs input it shifts the next entry off `ARGV` and opens it as a file; only when `ARGV` is empty at the first read (or an entry is `"-"`) does it read the current `$stdin`. So with `Alice` still in `ARGV`, the first `gets` tries to open a file called `Alice` and raises `Errno::ENOENT`. Fixes: call `$stdin.gets` explicitly, or consume the arguments (`name = ARGV.shift`) before the first `gets`. The same `ARGF` behaviour is what makes `gets` perfect for `cat`-like filters that accept either file names or piped input. At end of input `gets` returns `nil`, so `gets.chomp` needs a guard.
code
ruby · 5 lines# greet.rb, run as: ruby greet.rb Alice
name = ARGV.first
print "Hello, #{name}. Reply: "
reply = $stdin.gets(chomp: true) # not bare gets: ARGF would open "Alice"
puts(reply.nil? ? "no reply" : "You said #{reply}")go deeper
Recall that bare gets reads files named in ARGV before standard input, and that $stdin.gets reads the keyboard or pipe directly.
Explain how ARGF shifts ARGV, falls back to $stdin, and returns nil at end of input, and fix the crash two ways.
Choose ARGF deliberately for filter-style tools, keep interactive input on an injectable $stdin, and handle nil at end of input.
Set conventions for command-line tools: argument parsing before any read, explicit input streams, and filter behaviour only where it is intended.
## `gets` means `ARGF.gets` Ruby's top-level `gets` is `Kernel#gets`, and its documentation says it returns "the next line from the list of files in `ARGV` (or `$*`), or from standard input if no files are present on the command line". It forwards to **`ARGF`**, a special stream object (also reachable as `$<`) designed for scripts that process files given as arguments or data piped into them. That design comes from Unix filters: `ruby count.rb a.txt b.txt` should read both files, and `cat a.txt | ruby count.rb` should read the pipe, with the same code. `ARGF` makes that possible, and it is also why a script that meant "read what the user types" breaks as soon as it receives an argument. ## How ARGF picks its source When `ARGF` needs its next input it follows these steps: 1. If `ARGV` still has entries, it **removes the first one** (`ARGV.shift`) and opens that path as a file. 2. If the entry is `"-"`, it reads the current `$stdin` instead of a file. 3. When one file is exhausted, it moves on to the next entry, so several files read as one continuous stream. 4. If `ARGV` was empty when reading started, it reads the current `$stdin`. `ARGF.filename` (also `$FILENAME`) names the file currently being read, `"-"` for standard input, and `ARGF.lineno` counts the lines read so far across all the files. ## The crash, step by step ```ruby # greet.rb, run as: ruby greet.rb Alice print "Your reply: " reply = gets # ARGF shifts "Alice" off ARGV and opens it as a file ``` With a file named `Alice` absent, the open fails and Ruby raises `Errno::ENOENT` ("No such file or directory") naming `Alice`. If such a file happened to exist, the script would silently read its first line instead of waiting for the user, which is worse. ## Same script, four invocations | Command | What bare `gets` reads | |---|---| | `ruby greet.rb` | the keyboard, because `ARGV` is empty | | `echo hi \| ruby greet.rb` | `"hi\n"` from the pipe, the current `$stdin` | | `ruby greet.rb Alice` | tries to open a file named `Alice` | | `ruby greet.rb -` | standard input, because `"-"` names it | The table shows why the bug often ships: every manual test without arguments passes, and the failure only appears once someone passes a name. ## Fixes | Fix | Effect | |---|---| | `reply = $stdin.gets` | reads the current standard input no matter what `ARGV` holds | | `reply = STDIN.gets` | reads the original standard input, ignoring any reassignment of `$stdin` | | `name = ARGV.shift` before the first `gets` | leaves `ARGV` empty, so `ARGF` falls back to `$stdin` | | parse options, then `ARGV.clear` | same idea when all arguments are options rather than files | `$stdin.gets` is the clearest: it states the intent and keeps working if someone adds another argument later. Prefer it over `STDIN.gets` so a test can substitute input by assigning `$stdin`. ## When ARGF is exactly what you want For filters, `ARGF` removes all the plumbing: - `ARGF.each_line { |line| ... }` iterates over every line of every named file, or of piped input; - `ARGF.read` returns everything as one string; - `ARGF.filename` tells you which file a line came from. The one-liner flags `-n` and `-p` wrap a script in a `gets` loop over this same stream; those flags belong to the command-line options topic. ## Handling end of input `gets` returns **`nil`** at end of input: end of the last file, a closed pipe, or Ctrl-D on a terminal. `gets.chomp` then raises `NoMethodError` on `nil`. Guard with `line = gets` in a `while` condition, `gets&.chomp`, or pass `chomp: true` (`gets(chomp: true)`), which strips the line terminator but still returns `nil` at the end.
- What does ARGF do to ARGV as it reads?It shifts each file name off `ARGV` as it opens that file, so by the time the files are read `ARGV` is empty. Code that inspects `ARGV` after calling `gets` therefore sees fewer entries than were passed, and code that wants its own arguments must take them before the first `gets`.
- Why prefer $stdin.gets over STDIN.gets in a command-line tool?`$stdin` is the current input stream and can be reassigned, so a test can set `$stdin = StringIO.new("yes\n")` and the tool reads it. `STDIN` always holds the original stream, which a test cannot replace without touching file descriptors.
saying these in an interview costs you the question
- Kernel#gets always reads from standard input
- gets ignores ARGV unless the script calls ARGF explicitly
- gets.chomp is safe because gets never returns nil
- ARGF reads ARGV entries as lines of input text