# frozen_string_literal: false
#
# irb.rb - irb main module
# $Release Version: 0.9.6 $
# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
# --
#
#
#
require "ripper"
require "reline"
require_relative "irb/init"
require_relative "irb/context"
require_relative "irb/extend-command"
require_relative "irb/ruby-lex"
require_relative "irb/input-method"
require_relative "irb/locale"
require_relative "irb/color"
require_relative "irb/version"
require_relative "irb/easter-egg"
# IRB stands for "interactive Ruby" and is a tool to interactively execute Ruby
# expressions read from the standard input.
#
# The +irb+ command from your shell will start the interpreter.
#
# == Usage
#
# Use of irb is easy if you know Ruby.
#
# When executing irb, prompts are displayed as follows. Then, enter the Ruby
# expression. An input is executed when it is syntactically complete.
#
# $ irb
# irb(main):001:0> 1+2
# #=> 3
# irb(main):002:0> class Foo
# irb(main):003:1> def foo
# irb(main):004:2> print 1
# irb(main):005:2> end
# irb(main):006:1> end
# #=> nil
#
# The singleline editor module or multiline editor module can be used with irb.
# Use of multiline editor is default if it's installed.
#
# == Command line options
#
# Usage: irb.rb [options] [programfile] [arguments]
# -f Suppress read of ~/.irbrc
# -d Set $DEBUG to true (same as `ruby -d')
# -r load-module Same as `ruby -r'
# -I path Specify $LOAD_PATH directory
# -U Same as `ruby -U`
# -E enc Same as `ruby -E`
# -w Same as `ruby -w`
# -W[level=2] Same as `ruby -W`
# --inspect Use `inspect' for output (default except for bc mode)
# --noinspect Don't use inspect for output
# --multiline Use multiline editor module
# --nomultiline Don't use multiline editor module
# --singleline Use singleline editor module
# --nosingleline Don't use singleline editor module
# --colorize Use colorization
# --nocolorize Don't use colorization
# --prompt prompt-mode
# --prompt-mode prompt-mode
# Switch prompt mode. Pre-defined prompt modes are
# `default', `simple', `xmp' and `inf-ruby'
# --inf-ruby-mode Use prompt appropriate for inf-ruby-mode on emacs.
# Suppresses --multiline and --singleline.
# --simple-prompt Simple prompt mode
# --noprompt No prompt mode
# --tracer Display trace for each execution of commands.
# --back-trace-limit n
# Display backtrace top n and tail n. The default
# value is 16.
# -v, --version Print the version of irb
#
# == Configuration
#
# IRB reads from ~/.irbrc when it's invoked.
#
# If ~/.irbrc doesn't exist, +irb+ will try to read in the following order:
#
# * +.irbrc+
# * +irb.rc+
# * +_irbrc+
# * $irbrc
#
# The following are alternatives to the command line options. To use them type
# as follows in an +irb+ session:
#
# IRB.conf[:IRB_NAME]="irb"
# IRB.conf[:INSPECT_MODE]=nil
# IRB.conf[:IRB_RC] = nil
# IRB.conf[:BACK_TRACE_LIMIT]=16
# IRB.conf[:USE_LOADER] = false
# IRB.conf[:USE_MULTILINE] = nil
# IRB.conf[:USE_SINGLELINE] = nil
# IRB.conf[:USE_COLORIZE] = true
# IRB.conf[:USE_TRACER] = false
# IRB.conf[:IGNORE_SIGINT] = true
# IRB.conf[:IGNORE_EOF] = false
# IRB.conf[:PROMPT_MODE] = :DEFAULT
# IRB.conf[:PROMPT] = {...}
#
# === Auto indentation
#
# To disable auto-indent mode in irb, add the following to your +.irbrc+:
#
# IRB.conf[:AUTO_INDENT] = false
#
# === Autocompletion
#
# To enable autocompletion for irb, add the following to your +.irbrc+:
#
# require 'irb/completion'
#
# === History
#
# By default, irb will store the last 1000 commands you used in
# IRB.conf[:HISTORY_FILE] (~/.irb_history by default).
#
# If you want to disable history, add the following to your +.irbrc+:
#
# IRB.conf[:SAVE_HISTORY] = nil
#
# See IRB::Context#save_history= for more information.
#
# The history of _results_ of commands evaluated is not stored by default,
# but can be turned on to be stored with this +.irbrc+ setting:
#
# IRB.conf[:EVAL_HISTORY] = IRB.conf[:IRB_RC], its will be invoked after execution
# of that proc with the context of the current session as its argument. Each
# session can be configured using this mechanism.
#
# === Session variables
#
# There are a few variables in every Irb session that can come in handy:
#
# _::
# The value command executed, as a local variable
# __::
# The history of evaluated commands. Available only if
# IRB.conf[:EVAL_HISTORY] is not +nil+ (which is the default).
# See also IRB::Context#eval_history= and IRB::History.
# __[line_no]::
# Returns the evaluation value at the given line number, +line_no+.
# If +line_no+ is a negative, the return value +line_no+ many lines before
# the most recent return value.
#
# === Example using IRB Sessions
#
# # invoke a new session
# irb(main):001:0> irb
# # list open sessions
# irb.1(main):001:0> jobs
# #0->irb on main (#IRB.conf[:TA_EXIT] when the current session quits.
def IRB.irb_at_exit
@CONF[:AT_EXIT].each{|hook| hook.call}
end
# Quits irb
def IRB.irb_exit(irb, ret)
throw :IRB_EXIT, ret
end
# Aborts then interrupts irb.
#
# Will raise an Abort exception, or the given +exception+.
def IRB.irb_abort(irb, exception = Abort)
if defined? Thread
irb.context.thread.raise exception, "abort then interrupt!"
else
raise exception, "abort then interrupt!"
end
end
class Irb
ASSIGNMENT_NODE_TYPES = [
# Local, instance, global, class, constant, instance, and index assignment:
# "foo = bar",
# "@foo = bar",
# "$foo = bar",
# "@@foo = bar",
# "::Foo = bar",
# "a::Foo = bar",
# "Foo = bar"
# "foo.bar = 1"
# "foo[1] = bar"
:assign,
# Operation assignment:
# "foo += bar"
# "foo -= bar"
# "foo ||= bar"
# "foo &&= bar"
:opassign,
# Multiple assignment:
# "foo, bar = 1, 2
:massign,
]
# Note: instance and index assignment expressions could also be written like:
# "foo.bar=(1)" and "foo.[]=(1, bar)", when expressed that way, the former
# be parsed as :assign and echo will be suppressed, but the latter is
# parsed as a :method_add_arg and the output won't be suppressed
# Creates a new irb session
def initialize(workspace = nil, input_method = nil)
@context = Context.new(self, workspace, input_method)
@context.main.extend ExtendCommandBundle
@signal_status = :IN_IRB
@scanner = RubyLex.new
end
def run(conf = IRB.conf)
conf[:IRB_RC].call(context) if conf[:IRB_RC]
conf[:MAIN_CONTEXT] = context
trap("SIGINT") do
signal_handle
end
begin
catch(:IRB_EXIT) do
eval_input
end
ensure
conf[:AT_EXIT].each{|hook| hook.call}
end
end
# Returns the current context of this irb session
attr_reader :context
# The lexer used by this irb session
attr_accessor :scanner
# Evaluates input for this session.
def eval_input
exc = nil
@scanner.set_prompt do
|ltype, indent, continue, line_no|
if ltype
f = @context.prompt_s
elsif continue
f = @context.prompt_c
elsif indent > 0
f = @context.prompt_n
else
f = @context.prompt_i
end
f = "" unless f
if @context.prompting?
@context.io.prompt = p = prompt(f, ltype, indent, line_no)
else
@context.io.prompt = p = ""
end
if @context.auto_indent_mode and !@context.io.respond_to?(:auto_indent)
unless ltype
prompt_i = @context.prompt_i.nil? ? "" : @context.prompt_i
ind = prompt(prompt_i, ltype, indent, line_no)[/.*\z/].size +
indent * 2 - p.size
ind += 2 if continue
@context.io.prompt = p + " " * ind if ind > 0
end
end
@context.io.prompt
end
@scanner.set_input(@context.io) do
signal_status(:IN_INPUT) do
if l = @context.io.gets
print l if @context.verbose?
else
if @context.ignore_eof? and @context.io.readable_after_eof?
l = "\n"
if @context.verbose?
printf "Use \"exit\" to leave %s\n", @context.ap_name
end
else
print "\n"
end
end
l
end
end
@scanner.set_auto_indent(@context) if @context.auto_indent_mode
@scanner.each_top_level_statement do |line, line_no|
signal_status(:IN_EVAL) do
begin
line.untaint if RUBY_VERSION < '2.7'
@context.evaluate(line, line_no, exception: exc)
if @context.echo?
if assignment_expression?(line)
if @context.echo_on_assignment?
output_value(@context.omit_on_assignment?)
end
else
output_value
end
end
rescue Interrupt => exc
rescue SystemExit, SignalException
raise
rescue Exception => exc
else
exc = nil
next
end
handle_exception(exc)
end
end
end
def handle_exception(exc)
if exc.backtrace && exc.backtrace[0] =~ /\/irb(2)?(\/.*|-.*|\.rb)?:/ && exc.class.to_s !~ /^IRB/ &&
!(SyntaxError === exc) && !(EncodingError === exc)
# The backtrace of invalid encoding hash (ex. {"\xAE": 1}) raises EncodingError without lineno.
irb_bug = true
else
irb_bug = false
end
if STDOUT.tty?
attr = ATTR_TTY
print "#{attr[1]}Traceback#{attr[]} (most recent call last):\n"
else
attr = ATTR_PLAIN
end
messages = []
lasts = []
levels = 0
if exc.backtrace
count = 0
exc.backtrace.each do |m|
m = @context.workspace.filter_backtrace(m) or next unless irb_bug
count += 1
if attr == ATTR_TTY
m = sprintf("%9d: from %s", count, m)
else
m = "\tfrom #{m}"
end
if messages.size < @context.back_trace_limit
messages.push(m)
elsif lasts.size < @context.back_trace_limit
lasts.push(m).shift
levels += 1
end
end
end
if attr == ATTR_TTY
unless lasts.empty?
puts lasts.reverse
printf "... %d levels...\n", levels if levels > 0
end
puts messages.reverse
end
m = exc.to_s.split(/\n/)
print "#{attr[1]}#{exc.class} (#{attr[4]}#{m.shift}#{attr[0, 1]})#{attr[]}\n"
puts m.map {|s| "#{attr[1]}#{s}#{attr[]}\n"}
if attr == ATTR_PLAIN
puts messages
unless lasts.empty?
puts lasts
printf "... %d levels...\n", levels if levels > 0
end
end
print "Maybe IRB bug!\n" if irb_bug
end
# Evaluates the given block using the given +path+ as the Context#irb_path
# and +name+ as the Context#irb_name.
#
# Used by the irb command +source+, see IRB@IRB+Sessions for more
# information.
def suspend_name(path = nil, name = nil)
@context.irb_path, back_path = path, @context.irb_path if path
@context.irb_name, back_name = name, @context.irb_name if name
begin
yield back_path, back_name
ensure
@context.irb_path = back_path if path
@context.irb_name = back_name if name
end
end
# Evaluates the given block using the given +workspace+ as the
# Context#workspace.
#
# Used by the irb command +irb_load+, see IRB@IRB+Sessions for more
# information.
def suspend_workspace(workspace)
@context.workspace, back_workspace = workspace, @context.workspace
begin
yield back_workspace
ensure
@context.workspace = back_workspace
end
end
# Evaluates the given block using the given +input_method+ as the
# Context#io.
#
# Used by the irb commands +source+ and +irb_load+, see IRB@IRB+Sessions
# for more information.
def suspend_input_method(input_method)
back_io = @context.io
@context.instance_eval{@io = input_method}
begin
yield back_io
ensure
@context.instance_eval{@io = back_io}
end
end
# Evaluates the given block using the given +context+ as the Context.
def suspend_context(context)
@context, back_context = context, @context
begin
yield back_context
ensure
@context = back_context
end
end
# Handler for the signal SIGINT, see Kernel#trap for more information.
def signal_handle
unless @context.ignore_sigint?
print "\nabort!\n" if @context.verbose?
exit
end
case @signal_status
when :IN_INPUT
print "^C\n"
raise RubyLex::TerminateLineInput
when :IN_EVAL
IRB.irb_abort(self)
when :IN_LOAD
IRB.irb_abort(self, LoadAbort)
when :IN_IRB
# ignore
else
# ignore other cases as well
end
end
# Evaluates the given block using the given +status+.
def signal_status(status)
return yield if @signal_status == :IN_LOAD
signal_status_back = @signal_status
@signal_status = status
begin
yield
ensure
@signal_status = signal_status_back
end
end
def prompt(prompt, ltype, indent, line_no) # :nodoc:
p = prompt.dup
p.gsub!(/%([0-9]+)?([a-zA-Z])/) do
case $2
when "N"
@context.irb_name
when "m"
@context.main.to_s
when "M"
@context.main.inspect
when "l"
ltype
when "i"
if indent < 0
if $1
"-".rjust($1.to_i)
else
"-"
end
else
if $1
format("%" + $1 + "d", indent)
else
indent.to_s
end
end
when "n"
if $1
format("%" + $1 + "d", line_no)
else
line_no.to_s
end
when "%"
"%"
end
end
p
end
def output_value(omit = false) # :nodoc:
str = @context.inspect_last_value
multiline_p = str.include?("\n")
if omit
winwidth = @context.io.winsize.last
if multiline_p
first_line = str.split("\n").first
result = @context.newline_before_multiline_output? ? (@context.return_format % first_line) : first_line
output_width = Reline::Unicode.calculate_width(result, true)
diff_size = output_width - Reline::Unicode.calculate_width(first_line, true)
if diff_size.positive? and output_width > winwidth
lines, _ = Reline::Unicode.split_by_width(first_line, winwidth - diff_size - 3)
str = "%s...\e[0m" % lines.first
multiline_p = false
else
str.gsub!(/(\A.*?\n).*/m, "\\1...")
end
else
output_width = Reline::Unicode.calculate_width(@context.return_format % str, true)
diff_size = output_width - Reline::Unicode.calculate_width(str, true)
if diff_size.positive? and output_width > winwidth
lines, _ = Reline::Unicode.split_by_width(str, winwidth - diff_size - 3)
str = "%s...\e[0m" % lines.first
end
end
end
if multiline_p && @context.newline_before_multiline_output?
printf @context.return_format, "\n#{str}"
else
printf @context.return_format, str
end
end
# Outputs the local variables to this current session, including
# #signal_status and #context, using IRB::Locale.
def inspect
ary = []
for iv in instance_variables
case (iv = iv.to_s)
when "@signal_status"
ary.push format("%s=:%s", iv, @signal_status.id2name)
when "@context"
ary.push format("%s=%s", iv, eval(iv).__to_s__)
else
ary.push format("%s=%s", iv, eval(iv))
end
end
format("#<%s: %s>", self.class, ary.join(", "))
end
def assignment_expression?(line)
# Try to parse the line and check if the last of possibly multiple
# expressions is an assignment type.
# If the expression is invalid, Ripper.sexp should return nil which will
# result in false being returned. Any valid expression should return an
# s-expression where the second selement of the top level array is an
# array of parsed expressions. The first element of each expression is the
# expression's type.
verbose, $VERBOSE = $VERBOSE, nil
result = ASSIGNMENT_NODE_TYPES.include?(Ripper.sexp(line)&.dig(1,-1,0))
$VERBOSE = verbose
result
end
ATTR_TTY = "\e[%sm"
def ATTR_TTY.[](*a) self % a.join(";"); end
ATTR_PLAIN = ""
def ATTR_PLAIN.[](*) self; end
end
def @CONF.inspect
IRB.version unless self[:VERSION]
array = []
for k, v in sort{|a1, a2| a1[0].id2name <=> a2[0].id2name}
case k
when :MAIN_CONTEXT, :__TMP__EHV__
array.push format("CONF[:%s]=...myself...", k.id2name)
when :PROMPT
s = v.collect{
|kk, vv|
ss = vv.collect{|kkk, vvv| ":#{kkk.id2name}=>#{vvv.inspect}"}
format(":%s=>{%s}", kk.id2name, ss.join(", "))
}
array.push format("CONF[:%s]={%s}", k.id2name, s.join(", "))
else
array.push format("CONF[:%s]=%s", k.id2name, v.inspect)
end
end
array.join("\n")
end
end
class Binding
# Opens an IRB session where +binding.irb+ is called which allows for
# interactive debugging. You can call any methods or variables available in
# the current scope, and mutate state if you need to.
#
#
# Given a Ruby file called +potato.rb+ containing the following code:
#
# class Potato
# def initialize
# @cooked = false
# binding.irb
# puts "Cooked potato: #{@cooked}"
# end
# end
#
# Potato.new
#
# Running ruby potato.rb will open an IRB session where
# +binding.irb+ is called, and you will see the following:
#
# $ ruby potato.rb
#
# From: potato.rb @ line 4 :
#
# 1: class Potato
# 2: def initialize
# 3: @cooked = false
# => 4: binding.irb
# 5: puts "Cooked potato: #{@cooked}"
# 6: end
# 7: end
# 8:
# 9: Potato.new
#
# irb(#