Class: IRB::Irb

Inherits:
Object show all
Defined in:
lib/irb.rb,
lib/irb/ext/multi-irb.rb

Constant Summary collapse

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

32
PROMPT_MAIN_TRUNCATE_OMISSION =
'...'.freeze
CONTROL_CHARACTERS_PATTERN =
"\x00-\x1F".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(workspace = nil, input_method = nil) ⇒ Irb

Creates a new irb session



427
428
429
430
431
432
433
# File 'lib/irb.rb', line 427

def initialize(workspace = nil, input_method = nil)
  @context = Context.new(self, workspace, input_method)
  @context.workspace.load_commands_to_main
  @signal_status = :IN_IRB
  @scanner = RubyLex.new
  @line_no = 1
end

Instance Attribute Details

#contextObject (readonly)

Returns the current context of this irb session



422
423
424
# File 'lib/irb.rb', line 422

def context
  @context
end

#scannerObject

The lexer used by this irb session



424
425
426
# File 'lib/irb.rb', line 424

def scanner
  @scanner
end

Instance Method Details

#build_statement(code) ⇒ Object



598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
# File 'lib/irb.rb', line 598

def build_statement(code)
  code.force_encoding(@context.io.encoding)
  command_or_alias, arg = code.split(/\s/, 2)
  # Transform a non-identifier alias (@, $) or keywords (next, break)
  command_name = @context.command_aliases[command_or_alias.to_sym]
  command = command_name || command_or_alias
  command_class = ExtendCommandBundle.load_command(command)

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

#configure_ioObject



619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
# File 'lib/irb.rb', line 619

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
        # Accept any single-line input for symbol aliases or commands that transform args
        next true if single_line_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|
      lines << '' if lines.empty?
      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) ⇒ Object



671
672
673
674
675
676
# File 'lib/irb.rb', line 671

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_breakObject

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



436
437
438
439
440
441
442
443
444
# File 'lib/irb.rb', line 436

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) ⇒ Object



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/irb.rb', line 446

def debug_readline(binding)
  workspace = IRB::WorkSpace.new(binding)
  context.workspace = workspace
  context.workspace.load_commands_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 = 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 reseting the counter
    context.io.reset_history_counter

    begin
      eval_input
    ensure
      context.io.save_history
    end
  else
    eval_input
  end

  if input&.include?("\n")
    @line_no += input.count("\n") - 1
  end

  input
end

#each_top_level_statementObject



585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/irb.rb', line 585

def each_top_level_statement
  loop do
    code = readmultiline
    break unless code

    if code != "\n"
      yield build_statement(code), @line_no
    end
    @line_no += code.count("\n")
  rescue RubyLex::TerminateLineInput
  end
end

#encode_with_invalid_byte_sequence(str, enc) ⇒ Object



678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/irb.rb', line 678

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_inputObject

Evaluates input for this session.



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/irb.rb', line 505

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.evaluable_code, 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

#handle_exception(exc) ⇒ Object



700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
# File 'lib/irb.rb', line 700

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
  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
      lines = lines.map { |l| @context.workspace.filter_backtrace(l) }.compact
      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 `<(?<frame>top \(required\))>'/) { "(irb):#{$~[:num]}:in `<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

#inspectObject

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



873
874
875
876
877
878
879
880
881
882
883
884
885
886
# File 'lib/irb.rb', line 873

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) ⇒ Object

:nodoc:



834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
# File 'lib/irb.rb', line 834

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

#read_input(prompt) ⇒ Object



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/irb.rb', line 537

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

#readmultilineObject



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/irb.rb', line 556

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

    # Accept any single-line input for symbol aliases or commands that transform args
    return code if single_line_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) ⇒ Object



478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'lib/irb.rb', line 478

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
    catch(:IRB_EXIT) do
      eval_input
    end
  ensure
    trap("SIGINT", prev_trap)
    conf[:AT_EXIT].each{|hook| hook.call}
    context.io.save_history if save_history
  end
end

#signal_handleObject

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



800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
# File 'lib/irb.rb', line 800

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) ⇒ Object

Evaluates the given block using the given status.



822
823
824
825
826
827
828
829
830
831
832
# File 'lib/irb.rb', line 822

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

#single_line_command?(code) ⇒ Boolean

Returns:

  • (Boolean)


614
615
616
617
# File 'lib/irb.rb', line 614

def single_line_command?(code)
  command = code.split(/\s/, 2).first
  @context.symbol_alias?(command) || @context.transform_args?(command)
end

#suspend_input_method(input_method) ⇒ Object

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.



789
790
791
792
793
794
795
796
797
# File 'lib/irb.rb', line 789

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) ⇒ Object

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.



759
760
761
762
763
764
765
766
767
768
# File 'lib/irb.rb', line 759

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) ⇒ Object

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.



775
776
777
778
779
780
781
782
# File 'lib/irb.rb', line 775

def suspend_workspace(workspace)
  @context.workspace, back_workspace = workspace, @context.workspace
  begin
    yield back_workspace
  ensure
    @context.workspace = back_workspace
  end
end