Skip to content

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

  1. Write a MiniRepl class in about a dozen lines: it keeps one binding, evaluates each line of an array in it, prints => value.inspect, keeps _ as the last value, and reports a StandardError without stopping. Feed it 1 + 1, x = 2, x * 3, _ + 1, 1 / 0, [_, x] and "str".upcase.
  2. Drive a child irb with the flags above, but add --echo-on-assignment and then --noecho-on-assignment; send x = 5 and x + 1 to each and print both transcripts. Then send a two-line def (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.

=> 2
=> 2
=> 6
=> 7
!! ZeroDivisionError: divided by 0
=> [7, 2]
=> "STR"

See also