# frozen_string_literal: true
# :markup: markdown
# irb.rb - irb main module
# by Keiju ISHITSUKA([email protected])
#
require "prism"
require "reline"
require_relative "irb/init"
require_relative "irb/context"
require_relative "irb/default_commands"
require_relative "irb/ruby-lex"
require_relative "irb/statement"
require_relative "irb/history"
require_relative "irb/input-method"
require_relative "irb/locale"
require_relative "irb/color"
require_relative "irb/version"
require_relative "irb/easter-egg"
require_relative "irb/startup_message"
require_relative "irb/debug"
require_relative "irb/pager"
module IRB
# An exception raised by IRB.irb_abort
class Abort < Exception;end # :nodoc:
class << self
# The current IRB::Context of the session, see IRB.conf
#
# irb
# irb(main):001:0> IRB.CurrentContext.irb_name = "foo"
# foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo"
def CurrentContext # :nodoc:
conf[:MAIN_CONTEXT]
end
# Initializes IRB and creates a new Irb.irb object at the `TOPLEVEL_BINDING`
def start(ap_path = nil)
STDOUT.sync = true
$0 = File::basename(ap_path, ".rb") if ap_path
setup(ap_path)
if @CONF[:SCRIPT]
irb = Irb.new(nil, @CONF[:SCRIPT])
else
irb = Irb.new
# Only display the banner in the irb executable
if @CONF[:SHOW_BANNER] && ap_path&.end_with?("exe/irb")
StartupMessage.display
end
end
irb.run(@CONF)
end
# Quits irb
def irb_exit(*) # :nodoc:
throw :IRB_EXIT, false
end
# Aborts then interrupts irb.
#
# Will raise an Abort exception, or the given `exception`.
def irb_abort(irb, exception = Abort) # :nodoc:
irb.context.thread.raise exception, "abort then interrupt!"
end
end
class Irb
# 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
PROMPT_MAIN_TRUNCATE_LENGTH = 32
PROMPT_MAIN_TRUNCATE_OMISSION = '...'
CONTROL_CHARACTERS_PATTERN = "\x00-\x1F"
# Track the nesting depth of run loops. This is used for history management
# only the outermost run loop should load/save history.
@run_nesting_depth = 0
class << self
# TODO: When refactoring to v2.0, find a better way to manage and track nesting sessions.
attr_accessor :run_nesting_depth
end
# Returns the current context of this irb session
attr_reader :context
# The lexer used by this irb session
attr_accessor :scanner
attr_reader :from_binding
# Creates a new irb session
def initialize(workspace = nil, input_method = nil, from_binding: false)
@from_binding = from_binding
@prompt_part_cache = nil
@context = Context.new(self, workspace, input_method)
@context.workspace.load_helper_methods_to_main
@signal_status = :IN_IRB
@scanner = RubyLex.new
@line_no = 1
end
# A hook point for `debug` command's breakpoint after :IRB_EXIT as well as its
# clean-up
def debug_break
# it means the debug integration has been activated
if defined?(DEBUGGER__) && DEBUGGER__.respond_to?(:capture_frames_without_irb)
# after leaving this initial breakpoint, revert the capture_frames patch
DEBUGGER__.singleton_class.send(:alias_method, :capture_frames, :capture_frames_without_irb)
# and remove the redundant method
DEBUGGER__.singleton_class.send(:undef_method, :capture_frames_without_irb)
end
end
def debug_readline(binding)
workspace = IRB::WorkSpace.new(binding)
context.replace_workspace(workspace)
context.workspace.load_helper_methods_to_main
@line_no += 1
# When users run:
# 1. Debugging commands, like `step 2`
# 2. Any input that's not irb-command, like `foo = 123`
#
#
# Irb#eval_input will simply return the input, and we need to pass it to the
# debugger.
input = nil
forced_exit = catch(:IRB_EXIT) do
if History.save_history? && context.io.support_history_saving?
# Previous IRB session's history has been saved when `Irb#run` is exited We need
# to make sure the saved history is not saved again by resetting the counter
context.io.reset_history_counter
begin
input = eval_input
ensure
context.io.save_history
end
else
input = eval_input
end
false
end
Kernel.exit if forced_exit
if input&.include?("\n")
@line_no += input.count("\n") - 1
end
input
end
def run(conf = IRB.conf)
# Use run_nesting_depth to determine if we're in a nested session.
in_nested_session = Irb.run_nesting_depth > 0
Irb.run_nesting_depth += 1
conf[:IRB_RC].call(context) if conf[:IRB_RC]
prev_context = conf[:MAIN_CONTEXT]
conf[:MAIN_CONTEXT] = context
load_history = !in_nested_session && context.io.support_history_saving?
save_history = load_history && History.save_history?
if load_history
context.io.load_history
end
prev_trap = trap("SIGINT") do
signal_handle
end
begin
if defined?(RubyVM.keep_script_lines)
keep_script_lines_backup = RubyVM.keep_script_lines
RubyVM.keep_script_lines = true
end
forced_exit = catch(:IRB_EXIT) do
eval_input
end
ensure
Irb.run_nesting_depth -= 1
# Do not restore to nil. It will cause IRB crash when used with threads.
IRB.conf[:MAIN_CONTEXT] = prev_context if prev_context
RubyVM.keep_script_lines = keep_script_lines_backup if defined?(RubyVM.keep_script_lines)
trap("SIGINT", prev_trap)
conf[:AT_EXIT].each{|hook| hook.call}
context.io.save_history if save_history
Kernel.exit if forced_exit
end
end
# Evaluates input for this session.
def eval_input
configure_io
each_top_level_statement do |statement, line_no|
signal_status(:IN_EVAL) do
begin
# If the integration with debugger is activated, we return certain input if it
# should be dealt with by debugger
if @context.with_debugger && statement.should_be_handled_by_debugger?
return statement.code
end
@context.evaluate(statement, line_no)
if @context.echo? && !statement.suppresses_echo?
if statement.is_assignment?
if @context.echo_on_assignment?
output_value(@context.echo_on_assignment? == :truncate)
end
else
output_value
end
end
rescue SystemExit, SignalException
raise
rescue Interrupt, Exception => exc
handle_exception(exc)
@context.workspace.local_variable_set(:_, exc)
end
end
end
end
def read_input(prompt)
signal_status(:IN_INPUT) do
@context.io.prompt = prompt
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" if @context.prompting?
end
end
l
end
end
def read_input_nomultiline(prompt)
code = +''
line_offset = 0
loop do
line = read_input(prompt)
unless line
return code.empty? ? nil : code
end
code << line
return code if command?(code)
continue, opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
return code if terminated
line_offset += 1
prompt = generate_prompt(opens, continue, line_offset)
end
end
def readmultiline
with_prompt_part_cached do
prompt = generate_prompt([], false, 0)
if @context.io.respond_to?(:check_termination)
# multiline
read_input(prompt)
else
# nomultiline
read_input_nomultiline(prompt)
end
end
end
def each_top_level_statement
loop do
code = readmultiline
break unless code
yield parse_input(code), @line_no
@line_no += code.count("\n")
rescue RubyLex::TerminateLineInput
end
end
def parse_input(code)
if code.match?(/\A\n*\z/)
return Statement::EmptyInput.new
end
code = code.dup.force_encoding(@context.io.encoding)
is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables)
@context.parse_input(code, is_assignment_expression)
end
def command?(code)
parse_input(code).is_a?(Statement::Command)
end
def configure_io
if @context.io.respond_to?(:check_termination)
@context.io.check_termination do |code|
if Reline::IOGate.in_pasting?
rest = @scanner.check_termination_in_prev_line(code, local_variables: @context.local_variables)
if rest
Reline.delete_text
rest.bytes.reverse_each do |c|
Reline.ungetc(c)
end
true
else
false
end
else
next true if command?(code)
_continue, _opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
terminated
end
end
end
if @context.io.respond_to?(:dynamic_prompt)
@context.io.dynamic_prompt do |lines|
code = lines.map{ |l| l + "\n" }.join
parse_lex_result = Prism.parse_lex(code, scopes: [@context.local_variables])
line_results = IRB::NestingParser.parse_by_line(parse_lex_result)
tokens = parse_lex_result.value[1].map(&:first)
tokens_by_line = tokens.group_by {|t| t.location.start_line - 1 }
tokens_until_line = []
line_results.map.with_index do |(_prev_opens, next_opens, _min_depth), line_num_offset|
line = lines[line_num_offset]
tokens_until_line.concat(tokens_by_line[line_num_offset] || [])
continue = @scanner.should_continue?(tokens_until_line, line, line_num_offset + 1)
generate_prompt(next_opens, continue, line_num_offset)
end
end
end
if @context.io.respond_to?(:auto_indent) and @context.auto_indent_mode
@context.io.auto_indent do |lines, line_index, byte_pointer, is_newline|
next nil if lines == [nil] # Workaround for exit IRB with CTRL+d
next nil if !is_newline && lines[line_index]&.byteslice(0, byte_pointer)&.match?(/\A\s*\z/)
code = lines[0..line_index].map { |l| "#{l}\n" }.join
parse_lex_result = Prism.parse_lex(code, scopes: [@context.local_variables])
@scanner.process_indent_level(parse_lex_result, lines, line_index, is_newline)
end
end
end
def convert_invalid_byte_sequence(str, enc)
str.force_encoding(enc)
str.scrub { |c|
c.bytes.map{ |b| "\\x#{b.to_s(16).upcase}" }.join
}
end
def encode_with_invalid_byte_sequence(str, enc)
conv = Encoding::Converter.new(str.encoding, enc)
dst = String.new
begin
ret = conv.primitive_convert(str, dst)
case ret
when :invalid_byte_sequence
conv.insert_output(conv.primitive_errinfo[3].dump[1..-2])
redo
when :undefined_conversion
c = conv.primitive_errinfo[3].dup.force_encoding(conv.primitive_errinfo[1])
conv.insert_output(c.dump[1..-2])
redo
when :incomplete_input
conv.insert_output(conv.primitive_errinfo[3].dump[1..-2])
when :finished
end
break
end while nil
dst
end
def handle_exception(exc)
if 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
# To support backtrace filtering while utilizing Exception#full_message, we need to clone
# the exception to avoid modifying the original exception's backtrace.
exc = exc.clone
filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
backtrace_filter = IRB.conf[:BACKTRACE_FILTER]
if backtrace_filter
if backtrace_filter.respond_to?(:call)
filtered_backtrace = backtrace_filter.call(filtered_backtrace)
else
warn "IRB.conf[:BACKTRACE_FILTER] #{backtrace_filter} should respond to `call` method"
end
end
exc.set_backtrace(filtered_backtrace)
end
highlight = Color.colorable?
order =
if RUBY_VERSION < '3.0.0'
STDOUT.tty? ? :bottom : :top
else # '3.0.0' <= RUBY_VERSION
:top
end
message = exc.full_message(order: order, highlight: highlight)
message = convert_invalid_byte_sequence(message, exc.message.encoding)
message = encode_with_invalid_byte_sequence(message, IRB.conf[:LC_MESSAGES].encoding) unless message.encoding.to_s.casecmp?(IRB.conf[:LC_MESSAGES].encoding.to_s)
message = message.gsub(/((?:^\t.+$\n)+)/) { |m|
case order
when :top
lines = m.split("\n")
when :bottom
lines = m.split("\n").reverse
end
unless irb_bug
if lines.size > @context.back_trace_limit
omit = lines.size - @context.back_trace_limit
lines = lines[0..(@context.back_trace_limit - 1)]
lines << "\t... %d levels..." % omit
end
end
lines = lines.reverse if order == :bottom
lines.map{ |l| l + "\n" }.join
}
# The "