123456789_123456789_123456789_123456789_123456789_

Class: IRB::Irb

Relationships & Source Files
Inherits: Object
Defined in: lib/irb.rb,
lib/irb/ext/multi-irb.rb

Constant Summary

  • CONTROL_CHARACTERS_PATTERN =
    # File 'lib/irb.rb', line 924
    "\x00-\x1F"
  • PROMPT_MAIN_TRUNCATE_LENGTH =

    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

    # File 'lib/irb.rb', line 922
    32
  • PROMPT_MAIN_TRUNCATE_OMISSION =
    # File 'lib/irb.rb', line 923
    '...'

Class Method Summary

Instance Attribute Summary

  • #context readonly

    Returns the current context of this irb session.

  • #scanner rw

    The lexer used by this irb session.

Instance Method Summary

Constructor Details

.new(workspace = nil, input_method = nil) ⇒ Irb

Creates a new irb session

[ GitHub ]

  
# File 'lib/irb.rb', line 932

def initialize(workspace = nil, input_method = 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

Instance Attribute Details

#context (readonly)

Returns the current context of this irb session

[ GitHub ]

  
# File 'lib/irb.rb', line 927

attr_reader :context

#scanner (rw)

The lexer used by this irb session

[ GitHub ]

  
# File 'lib/irb.rb', line 929

attr_accessor :scanner

Instance Method Details

#build_statement(code)

[ GitHub ]

  
# File 'lib/irb.rb', line 1116

def build_statement(code)
  if code.match?(/\A\n*\z/)
    return Statement::EmptyInput.new
  end

  code.force_encoding(@context.io.encoding)
  if (command, arg = parse_command(code))
    command_class = Command.load_command(command)
    Statement::Command.new(code, command_class, arg)
  else
    is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables)
    Statement::Expression.new(code, is_assignment_expression)
  end
end

#command?(code) ⇒ Boolean

[ GitHub ]

  
# File 'lib/irb.rb', line 1150

def command?(code)
  !!parse_command(code)
end

#configure_io

[ GitHub ]

  
# File 'lib/irb.rb', line 1154

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)

        _tokens, _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|
      tokens = RubyLex.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join, local_variables: @context.local_variables)
      line_results = IRB::NestingParser.(tokens)
      tokens_until_line = []
      line_results.map.with_index do |(line_tokens, _prev_opens, next_opens, _min_depth), line_num_offset|
        line_tokens.each do |token, _s|
          # Avoid appending duplicated token. Tokens that include "n" like multiline
          # tstring_content can exist in multiple lines.
          tokens_until_line << token if token != tokens_until_line.last
        end
        continue = @scanner.should_continue?(tokens_until_line)
        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
      tokens = RubyLex.ripper_lex_without_warning(code, local_variables: @context.local_variables)
      @scanner.process_indent_level(tokens, lines, line_index, is_newline)
    end
  end
end

#convert_invalid_byte_sequence(str, enc)

[ GitHub ]

  
# File 'lib/irb.rb', line 1205

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

#debug_break

A hook point for debug command’s breakpoint after :IRB_EXIT as well as its clean-up

[ GitHub ]

  
# File 'lib/irb.rb', line 942

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

#debug_readline(binding)

[ GitHub ]

  
# File 'lib/irb.rb', line 952

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 IRB.conf[: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

#each_top_level_statement

[ GitHub ]

  
# File 'lib/irb.rb', line 1106

def each_top_level_statement
  loop do
    code = readmultiline
    break unless code
    yield build_statement(code), @line_no
    @line_no += code.count("\n")
  rescue RubyLex::TerminateLineInput
  end
end

#encode_with_invalid_byte_sequence(str, enc)

[ GitHub ]

  
# File 'lib/irb.rb', line 1212

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

#eval_input

Evaluates input for this session.

[ GitHub ]

  
# File 'lib/irb.rb', line 1027

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

#format_prompt(format, ltype, indent, line_no) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/irb.rb', line 1465

def format_prompt(format, ltype, indent, line_no) # :nodoc:
  format.gsub(/%([0-9]+)?([a-zA-Z%])/) do
    case $2
    when "N"
      @context.irb_name
    when "m"
      main_str = @context.main.to_s rescue "!#{$!.class}"
      truncate_prompt_main(main_str)
    when "M"
      main_str = @context.main.inspect rescue "!#{$!.class}"
      truncate_prompt_main(main_str)
    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 "%"
      "%" unless $1
    end
  end
end

#generate_prompt(opens, continue, line_offset) (private)

[ GitHub ]

  
# File 'lib/irb.rb', line 1426

def generate_prompt(opens, continue, line_offset)
  ltype = @scanner.ltype_from_open_tokens(opens)
  indent = @scanner.calc_indent_level(opens)
  continue = opens.any? || continue
  line_no = @line_no + line_offset

  if ltype
    f = @context.prompt_s
  elsif continue
    f = @context.prompt_c
  else
    f = @context.prompt_i
  end
  f = "" unless f
  if @context.prompting?
    p = format_prompt(f, ltype, indent, line_no)
  else
    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 = format_prompt(prompt_i, ltype, indent, line_no)[/.*\z/].size +
        indent * 2 - p.size
      p += " " * ind if ind > 0
    end
  end
  p
end

#handle_exception(exc)

[ GitHub ]

  
# File 'lib/irb.rb', line 1234

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
    # This is mostly to make IRB work nicely with Rails console's backtrace filtering, which patches WorkSpace#filter_backtrace
    # In such use case, we want to filter the exception's backtrace before its displayed through Exception#full_message
    # And we clone the exception object in order to avoid mutating the original exception
    # TODO: introduce better API to expose exception backtrace externally
    backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
    exc = exc.clone
    exc.set_backtrace(backtrace)
  end

  if RUBY_VERSION < '3.0.0'
    if STDOUT.tty?
      message = exc.full_message(order: :bottom)
      order = :bottom
    else
      message = exc.full_message(order: :top)
      order = :top
    end
  else # '3.0.0' <= RUBY_VERSION
    message = exc.full_message(order: :top)
    order = :top
  end
  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 "<top (required)>" in "(irb)" may be the top level of IRB so imitate the main object.
  message = message.gsub(/\(irb\):(?<num>\d+):in (?<open_quote>[`'])<(?<frame>top \(required\))>'/) { "(irb):#{$~[:num]}:in #{$~[:open_quote]}<main>'" }
  puts message
  puts 'Maybe IRB bug!' if irb_bug
rescue Exception => handler_exc
  begin
    puts exc.inspect
    puts "backtraces are hidden because #{handler_exc} was raised when processing them"
  rescue Exception
    puts 'Uninspectable exception occurred'
  end
end

#inspect

Outputs the local variables to this current session, including #signal_status and #context, using Locale.

[ GitHub ]

  
# File 'lib/irb.rb', line 1409

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

#output_value(omit = false)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/irb.rb', line 1370

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..." % lines.first
        str += "\e[0m" if Color.colorable?
        multiline_p = false
      else
        str = str.gsub(/(\A.*?\n).*/m, "\\1...")
        str += "\e[0m" if Color.colorable?
      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..." % lines.first
        str += "\e[0m" if Color.colorable?
      end
    end
  end

  if multiline_p && @context.newline_before_multiline_output?
    str = "\n" + str
  end

  Pager.page_content(format(@context.return_format, str), retain_content: true)
end

#parse_command(code)

[ GitHub ]

  
# File 'lib/irb.rb', line 1131

def parse_command(code)
  command_name, arg = code.strip.split(/\s+/, 2)
  return unless code.lines.size == 1 && command_name

  arg ||= ''
  command = command_name.to_sym
  # Command aliases are always command. example: $, @
  if (alias_name = @context.command_aliases[command])
    return [alias_name, arg]
  end

  # Check visibility
  public_method = !!Kernel.instance_method(:public_method).bind_call(@context.main, command) rescue false
  private_method = !public_method && !!Kernel.instance_method(:method).bind_call(@context.main, command) rescue false
  if Command.execute_as_command?(command, public_method: public_method, private_method: private_method)
    [command, arg]
  end
end

#read_input(prompt)

[ GitHub ]

  
# File 'lib/irb.rb', line 1060

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

#readmultiline

[ GitHub ]

  
# File 'lib/irb.rb', line 1079

def readmultiline
  prompt = generate_prompt([], false, 0)

  # multiline
  return read_input(prompt) if @context.io.respond_to?(:check_termination)

  # nomultiline
  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)

    tokens, opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
    return code if terminated

    line_offset += 1
    continue = @scanner.should_continue?(tokens)
    prompt = generate_prompt(opens, continue, line_offset)
  end
end

#run(conf = IRB.conf)

[ GitHub ]

  
# File 'lib/irb.rb', line 992

def run(conf = IRB.conf)
  in_nested_session = !!conf[:MAIN_CONTEXT]
  conf[:IRB_RC].call(context) if conf[:IRB_RC]
  conf[:MAIN_CONTEXT] = context

  save_history = !in_nested_session && conf[:SAVE_HISTORY] && context.io.support_history_saving?

  if save_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
    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

#signal_handle

Handler for the signal SIGINT, see Kernel.trap for more information.

See additional method definition at file lib/irb.rb line 1336.

[ GitHub ]

  
# File 'lib/irb/ext/multi-irb.rb', line 231

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

#signal_status(status)

Evaluates the given block using the given status.

[ GitHub ]

  
# File 'lib/irb.rb', line 1358

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

#suspend_input_method(input_method)

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.

[ GitHub ]

  
# File 'lib/irb.rb', line 1325

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

#suspend_name(path = nil, name = nil)

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.

[ GitHub ]

  
# File 'lib/irb.rb', line 1298

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

#suspend_workspace(workspace)

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.

[ GitHub ]

  
# File 'lib/irb.rb', line 1313

def suspend_workspace(workspace)
  current_workspace = @context.workspace
  @context.replace_workspace(workspace)
  yield
ensure
  @context.replace_workspace current_workspace
end

#truncate_prompt_main(str) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/irb.rb', line 1456

def truncate_prompt_main(str) # :nodoc:
  str = str.tr(CONTROL_CHARACTERS_PATTERN, ' ')
  if str.size <= PROMPT_MAIN_TRUNCATE_LENGTH
    str
  else
    str[0, PROMPT_MAIN_TRUNCATE_LENGTH - PROMPT_MAIN_TRUNCATE_OMISSION.size] + PROMPT_MAIN_TRUNCATE_OMISSION
  end
end