gets returns nil at EOF, and reads ARGV's files first¶
Level: 201 · anyone writing a filter, and anyone who has met undefined method 'chomp' for nil
One line: gets returns the next line with its newline and nil at end of input — so gets.chomp is a NoMethodError on empty input — and it is really ARGF.gets, which reads the files named in ARGV before it ever looks at standard input; Python's input() strips the newline, raises EOFError instead of returning None, and never looks at sys.argv — that job belongs to fileinput.
Three objects are in play. $stdin (the constant STDIN is the same IO) is standard input itself: gets, each_line, readlines, read, readline and eof? work on it as on any file. ARGF — also spelled $< — is a virtual file made of the files named in ARGV, read one after the other, consuming the names as it goes; when ARGV is empty it reads $stdin instead, and a bare "-" means stdin explicitly. The bare gets that every tutorial uses is Kernel#gets, which reads from ARGF, not from $stdin — invisible until your script takes a filename argument for some other purpose and gets opens it. The return values are the lesson: gets returns the line with its newline and nil at end of input, so while (line = gets) is the loop and gets.chomp is the bug; readline raises EOFError instead; read returns everything and readlines(chomp: true) an Array of stripped lines. This program cannot show any of that from the inside — its own stdin is whatever the runner gives it — so every row starts a child Ruby with RbConfig.ruby and feeds it through Open3.capture3(stdin_data:), in a temporary directory holding two small files.
Verified output of stdin_gets_and_argf_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. gets returns the next line, newline included "first\n"
2. gets three times on two lines: nil at EOF ["first\n", "second\n", nil]
3. so gets.chomp on an empty input raises NoMethodError: undefined method 'chomp' for nil
4. gets&.chomp and gets.to_s.chomp are the safe spellings [nil, ""]
5. while (line = gets) is the read loop "X\nY"
6. $stdin.each_line { |l| } is the same loop "cba\nfed"
7. readlines keeps newlines; readlines(chomp: true) strips ["x\n", "y\n"]; ["x", "y"]
8. $stdin.read(3) takes three bytes; read slurps the rest ["abc", "def"]
9. $stdin.readline at EOF raises instead of returning nil EOFError
10. gets("") reads a paragraph; gets(nil) slurps (Perl) ["p1\nl2\n\n", "p2\nrest"]
11. the trap: ruby -e 'p gets' a.txt reads the FILE "a1\n"
12. $stdin.gets ignores ARGV "from stdin\n"
13. gets is ARGF.gets: ARGF.read joins ARGV's files "a1\na2\nb1\n"
14. ARGV before and after: ARGF consumes the names [["a.txt", "b.txt"], []]
15. ARGF.filename, ARGF.lineno (total), ARGF.file.lineno (per file):
a.txt:1:1: a1
a.txt:2:2: a2
b.txt:3:1: b1
16. with ARGV empty, ARGF reads stdin; filename is ["from stdin\n", "-"]
17. a bare "-" in ARGV means stdin "dash means stdin\n"
18. a name in ARGV that does not exist Errno::ENOENT
19. $stdin.tty? when fed by a pipe, as here false
20. $stdin.eof? before and after the only line [false, "only\n", true]
21. $< is ARGF; $stdin and STDIN are one IO [true, true, IO]
Reading the output¶
Rows 1–4 are the nil story: gets returns "first\n", three calls on two lines give the two lines and nil, and gets.chomp on empty input is NoMethodError: undefined method 'chomp' for nil — the message quoted as Ruby 3.4 and later print it; gets&.chomp gives nil back and gets.to_s.chomp gives "". Rows 5–6 are the loop, in its while and each_line spellings. Rows 7–10 are the other readers: readlines with and without chomp: true, read(3) then read, readline raising EOFError, and Perl's two separators — gets("") reads a paragraph (up to a blank line, which it keeps as "\n\n") and gets(nil) slurps the rest. Rows 11–14 are the trap: the child was fed from stdin on its standard input and given a.txt in ARGV, and gets returned the file's first line; $stdin.gets returned the fed line; ARGF.read returned both files joined; and ARGV was ["a.txt", "b.txt"] before and [] after — ARGF shifts the names off as it opens them. Row 15 walks both files with ARGF.each_line: ARGF.filename names the current file, ARGF.lineno counts across files and ARGF.file.lineno restarts in each. Rows 16–18: with ARGV empty, ARGF reads stdin and reports its filename as "-"; a "-" argument means stdin; a name that does not exist raises Errno::ENOENT when ARGF reaches it. Rows 19–21: $stdin.tty? is false under a pipe (it is true at a terminal, which is how a filter knows whether to prompt), eof? is false before the only line and true after it, and $<, $stdin and STDIN are the objects the prose named.
Compared with Python¶
input() is gets with two differences: the newline is stripped (row 1) and end of input is an EOFError, not None (rows 2–3) — so the .chomp-on-nil bug cannot happen, and the loop is for line in sys.stdin (row 5) or sys.stdin.readline(), which returns "" at EOF (row 4) and keeps the newline. readlines() and read().splitlines() are the two readlines spellings (row 7), read(3) counts characters rather than bytes (row 8), and there is no paragraph mode — read().split("\n\n") is the idiom (row 10). The trap does not exist: input() and sys.stdin read standard input whatever sys.argv holds (rows 11–12). ARGF is the fileinput module: fileinput.input() iterates the lines of the files named in sys.argv[1:] in order (row 13), leaving sys.argv untouched (row 14), with filename(), lineno() and filelineno() matching Ruby's three counters (row 15); with no names it reads stdin and calls it <stdin> (row 16), a - means stdin (row 17), and a missing file raises FileNotFoundError (row 18). sys.stdin.isatty() is tty? (row 19); there is no eof? — a second readline() simply returns "" (row 20). The Python library's Standard in, standard out, and pipes ↗ measures the streams from Python's side, including what a pipe changes.
Verified output of stdin_gets_and_argf_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. input() returns the next line, newline stripped 'first'
2. input() three times on two lines: EOFError at EOF ['first', 'second', 'EOFError']
3. so there is no None to call a method on; input() raises EOFError
4. sys.stdin.readline() returns '' at EOF instead ['', '']
5. for line in sys.stdin is the read loop 'X\nY'
6. the same loop, another body 'cba\nfed'
7. readlines() keeps newlines; read().splitlines() strips ['x\n', 'y\n']; ['x', 'y']
8. sys.stdin.read(3) takes three characters; read() the rest ['abc', 'def']
9. input() at EOF raises, like Ruby's readline EOFError
10. no paragraph mode: read().split('\n\n') is the idiom ['p1\nl2', '\np2\nrest']
11. python -c 'print(input())' a.txt reads STDIN anyway 'from stdin'
12. sys.stdin.readline() too; argv is never a file to it 'from stdin\n'
13. fileinput.input() is ARGF: it joins argv's files 'a1\na2\nb1\n'
14. sys.argv before and after: fileinput leaves it alone [['a.txt', 'b.txt'], ['a.txt', 'b.txt']]
15. fileinput.filename(), lineno() (total), filelineno() (per file):
a.txt:1:1: a1
a.txt:2:2: a2
b.txt:3:1: b1
16. with argv empty, fileinput reads stdin; filename() is ['from stdin\n', '<stdin>']
17. a bare '-' in argv means stdin 'dash means stdin\n'
18. a name in argv that does not exist FileNotFoundError
19. sys.stdin.isatty() when fed by a pipe, as here False
20. no eof(): readline() twice on one line ['only\n', '']
21. sys.stdin is sys.__stdin__; its type [True, 'TextIOWrapper']
Try it¶
- Write
cat -nin one line —ARGF.each_linewithprintf("%6d\t%s", ARGF.lineno, line)— and run it as a child twice: once with a filename inARGV, once fed throughstdin_data:. It should not know the difference. - Write a filter that reads with
$stdin.each_lineand prints a prompt first only when$stdin.tty?is true; run it as a child withOpen3.capture3(a pipe) and show no prompt appeared, then reason about what a terminal would show.
Solution to 1
stdin_gets_and_argf_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: a `cat -n` in one line -- number every input line with ARGF.each_line,
# so the same program works on the files named in ARGV and on standard input.
# The parent runs it both ways as a child Ruby.
require "rbconfig"
require "open3"
require "tmpdir"
NUMBER = 'ARGF.each_line { |line| printf("%6d\t%s", ARGF.lineno, line) }'
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
File.write("poem.txt", "roses are red\nviolets are blue\n")
out, = Open3.capture2(RbConfig.ruby, "-e", NUMBER, "poem.txt")
puts "from a file named in ARGV:"
puts out
out, = Open3.capture2(RbConfig.ruby, "-e", NUMBER, stdin_data: "one\ntwo\nthree\n")
puts "from standard input:"
puts out
end
end
Verified output of stdin_gets_and_argf_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
ARGVholds strings andENVis not a Hash — theARGVthatARGFconsumesFile.openwith a block closes the file for you —each_line,readlinesandreadon a named filesystemreturns true, false or nil —Open3.capture3andstdin_data:, which this page's children are fed through- Safe navigation — the
&.ingets&.chomp ruby -n -p -a -F -l -icame from Perl ↗ — the flags that wrap a script in thewhile getsloop for you- Standard in, standard out, and pipes ↗ — Python's
sys.stdin, measured from its own side - A program knows it is piped ↗ —
isattyfrom the shell's point of view, the idea behind row 19 - Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…
putsadds a newline,printdoes not,pshowsinspect—putswritesto_splus a newline it does not double, flattening arrays, printingnilas an empty line…- Text lives in the Ruby text library — a Ruby
Stringis bytes plus an encoding label - Ruby has block comments and a data section — Ruby comments with
#, with=begin/=endblocks that must start at column 0, and with magic comments…