irb echoes every value; a script does not¶
Level: 101 · a Python programmer who lives in the >>> prompt and wants the Ruby one, and the flags that make it scriptable
One line: irb is a read–eval–print loop: each line's value is printed with inspect, _ holds the last one, an exception is reported and the loop continues, exit leaves — the same loop as python3 -i, whose >>> prompts go to stderr and whose values go to stdout, and the same object as code.InteractiveConsole.
The program starts a child Ruby with require "irb"; IRB.start and feeds it lines through a pipe. The flags matter for anyone who wants to script irb: -f skips ~/.irbrc, --noprompt drops the irb(main):001> prompts, --nocolorize drops the ANSI colours, --noverbose drops the "Switch to inspect mode." notice, and --no-pager keeps long output out of less. What is left on stdout is exactly the values irb echoes, one per line, plus whatever the code itself prints — so the transcript below is the whole conversation. Row 3 adds --noecho, which turns the echo off and makes irb behave like ruby. At a terminal, irb also offers multi-line editing, completion, and commands such as ls (what the current object responds to), show_source method_name, show_doc and help (Not machine-checked here.); and binding.irb anywhere in a program opens this same session at that line, with that scope, which is the simplest debugger Ruby has.
Verified output of irb_and_the_repl_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. every expression's value is echoed, one per line:
in: 1 + 1
in: x = 2
in: x * 3
in: _
in: [1, 2].map { it * 2 }
in: puts "side effect"
in: nil
in: def f(x) = x * 2
in: f(21)
out: 2
out: 2
out: 6
out: 6
out: [2, 4]
out: side effect
out: nil
out: nil
out: :f
out: 42
stderr empty: true exit: 0
2. an error is printed and the session goes on:
in: 1 / 0
in: "after"
out: (irb):1:in 'Integer#/': divided by 0 (ZeroDivisionError)
out: from (irb):1:in '<main>'
out: from -e:1:in '<main>'
out: "after"
stderr empty: true exit: 0
3. --noecho keeps only what the code prints itself:
in: 1 + 1
in: puts "side effect"
out: side effect
stderr empty: true exit: 0
4. exit ends the session; later lines are never read:
in: puts "before"
in: exit
in: puts "never"
out: before
out: nil
stderr empty: true exit: 0
Reading the output¶
Row 1 is nine inputs and ten outputs. 1 + 1 echoes 2; x = 2 echoes 2 because assignment is an expression whose value is the right-hand side; _ echoes 6, the value of the previous line; puts "side effect" prints side effect and then echoes nil, the value puts returns — the two lines a newcomer always asks about; an endless def echoes :f, because a method definition evaluates to the method's name as a Symbol; and the call echoes 42. Row 2 divides by zero: the error is reported with the (irb):1 location (the session's own file name and line), the two from lines that follow are the backtrace, and the next line runs as if nothing happened. Row 3 shows --noecho: only side effect remains. Row 4 shows that exit ends the session — before printed, nil echoed for the puts, and the third line never ran. Every session exits 0 and writes nothing to stderr, which is what makes a piped irb usable from a script or a test.
Compared with Python¶
python3 -q -i fed by a pipe is the same loop: -i says "stay interactive", -q drops the banner, and -c pass gives it an empty program to run first. Row 1's outputs line up with Ruby's — 2, 6, 6 for _, the list — with two differences that are the point: x = 2 and None echo nothing, because the Python REPL prints a value only when it is not None (assignment is a statement and has no value); and the def needed a blank line to finish, because the console reads a compound statement until an empty line, and it echoes nothing for it. The prompts (>>> and ...) go to stderr, which the twin counts instead of printing. Row 2's ZeroDivisionError is a traceback on stderr, again with the loop continuing. There is no --noecho (row 3): the non-echoing mode is simply running a file. Row 4's exit() behaves like Ruby's exit. Row 5 is the loop as an object: code.InteractiveConsole().push(line) evaluates a line in the console's own namespace, prints the value through sys.displayhook (captured here with redirect_stdout), returns True when the statement is incomplete — the def g(): case — and stores the last value in builtins._, which is why _ works at the prompt but not in a script. The 3.13 REPL adds colours, multi-line editing and exit without parentheses when a terminal is attached; through a pipe both interpreters fall back to the basic loop measured here (Not machine-checked here.). python3 -i script.py runs the script and then opens the prompt with its globals, the counterpart of a binding.irb on the last line. The Python library's -c is not the prompt ↗ measures the same echo difference between the prompt and -c.
Verified output of irb_and_the_repl_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. every expression's value is echoed, one per line:
in: 1 + 1
in: x = 2
in: x * 3
in: _
in: [v * 2 for v in [1, 2]]
in: print('side effect')
in: None
in: def f(x): return x * 2
in:
in: f(21)
out: 2
out: 6
out: 6
out: [2, 4]
out: side effect
out: 42
stderr: 10 prompts exit: 0
2. an error is printed (to stderr) and the session goes on:
in: 1 / 0
in: 'after'
out: 'after'
stderr: 3 prompts, and: ZeroDivisionError: division by zero exit: 0
3. no --noecho flag: a script (python3 file.py) is the mode that never echoes
4. exit() ends the session; later lines are never read:
in: print('before')
in: exit()
in: print('never')
out: before
stderr: 2 prompts exit: 0
5. the same loop in-process, code.InteractiveConsole:
push '1 + 1' -> printed: '2' needs more input: False
push 'y = 5' -> printed: '' needs more input: False
push 'y * 2' -> printed: '10' needs more input: False
push '_' -> printed: '10' needs more input: False
push 'def g():' -> printed: '' needs more input: True
_ lives in builtins: True value: 10
Try it¶
- Write a
MiniReplclass in about a dozen lines: it keeps onebinding, evaluates each line of an array in it, prints=> value.inspect, keeps_as the last value, and reports aStandardErrorwithout stopping. Feed it1 + 1,x = 2,x * 3,_ + 1,1 / 0,[_, x]and"str".upcase. - Drive a child
irbwith the flags above, but add--echo-on-assignmentand then--noecho-on-assignment; sendx = 5andx + 1to each and print both transcripts. Then send a two-linedef(no endless form) and see what the multi-line reader echoes.
Solution to 1
irb_and_the_repl_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: a REPL of your own in a dozen lines -- read, eval in one binding,
# print with inspect, keep `_` as the last value, survive errors.
class MiniRepl
def initialize
@binding = binding
end
def run(lines)
lines.each do |line|
value = @binding.eval(line)
@binding.local_variable_set(:_, value)
puts "=> #{value.inspect}"
rescue StandardError => e
puts "!! #{e.class}: #{e.message}"
end
end
end
MiniRepl.new.run(["1 + 1", "x = 2", "x * 3", "_ + 1", "1 / 0", "[_, x]", "\"str\".upcase"])
Verified output of irb_and_the_repl_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Backtraces and
caller— the(irb):1:in …lines in row 2, andbinding.irbnext to thedebuggem - The command-line flags —
-efor a one-off, and every other letter the child interpreters take - Everything is an expression — why
x = 2anddefecho values in Ruby but not in Python puts,print,pandpp— thenilafterputsthat row 1 shows twice- Binding and
eval— the one-binding trick the kata'sMiniReplis built on - Python:
-cis not the prompt ↗ — the echo difference between the Python prompt and a script, measured