From f5801e2bf404cbb0f673a6ebb040b0ba6cd01b77 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Fri, 16 Feb 2024 16:47:32 +0000 Subject: [ruby/irb] Standardize command related names (https://github.com/ruby/irb/pull/873) * Replace ExtendCommand with Command and standardize command related names 1. Rename lib/irb/extend-command.rb to lib/irb/command.rb 2. Rename lib/irb/cmd/*.rb to lib/irb/command/*.rb 3. Rename test/irb/test_cmd.rb to test/irb/test_command.rb 4. Rename ExtendCommand to Command * Alias ExtendCommand to Command and deprecate it * Rename Command::Nop to Command::Base * Not deprecate old constants just yet * Add lib/irb/cmd/nop.rb back https://github.com/ruby/irb/commit/462c1284af --- lib/irb.rb | 2 +- lib/irb/cmd/backtrace.rb | 21 - lib/irb/cmd/break.rb | 21 - lib/irb/cmd/catch.rb | 21 - lib/irb/cmd/chws.rb | 36 -- lib/irb/cmd/continue.rb | 17 - lib/irb/cmd/debug.rb | 80 ---- lib/irb/cmd/delete.rb | 17 - lib/irb/cmd/edit.rb | 54 --- lib/irb/cmd/exit.rb | 22 - lib/irb/cmd/finish.rb | 17 - lib/irb/cmd/force_exit.rb | 22 - lib/irb/cmd/help.rb | 12 - lib/irb/cmd/history.rb | 47 -- lib/irb/cmd/info.rb | 21 - lib/irb/cmd/irb_info.rb | 34 -- lib/irb/cmd/load.rb | 76 ---- lib/irb/cmd/ls.rb | 139 ------ lib/irb/cmd/measure.rb | 45 -- lib/irb/cmd/next.rb | 17 - lib/irb/cmd/nop.rb | 55 +-- lib/irb/cmd/pushws.rb | 45 -- lib/irb/cmd/show_cmds.rb | 59 --- lib/irb/cmd/show_doc.rb | 48 -- lib/irb/cmd/show_source.rb | 68 --- lib/irb/cmd/step.rb | 17 - lib/irb/cmd/subirb.rb | 109 ----- lib/irb/cmd/whereami.rb | 25 -- lib/irb/command.rb | 363 +++++++++++++++ lib/irb/command/backtrace.rb | 21 + lib/irb/command/base.rb | 55 +++ lib/irb/command/break.rb | 21 + lib/irb/command/catch.rb | 21 + lib/irb/command/chws.rb | 34 ++ lib/irb/command/continue.rb | 17 + lib/irb/command/debug.rb | 79 ++++ lib/irb/command/delete.rb | 17 + lib/irb/command/edit.rb | 54 +++ lib/irb/command/exit.rb | 20 + lib/irb/command/finish.rb | 17 + lib/irb/command/force_exit.rb | 20 + lib/irb/command/help.rb | 12 + lib/irb/command/history.rb | 47 ++ lib/irb/command/info.rb | 21 + lib/irb/command/irb_info.rb | 32 ++ lib/irb/command/load.rb | 74 ++++ lib/irb/command/ls.rb | 139 ++++++ lib/irb/command/measure.rb | 43 ++ lib/irb/command/next.rb | 17 + lib/irb/command/pushws.rb | 44 ++ lib/irb/command/show_cmds.rb | 59 +++ lib/irb/command/show_doc.rb | 46 ++ lib/irb/command/show_source.rb | 67 +++ lib/irb/command/step.rb | 17 + lib/irb/command/subirb.rb | 107 +++++ lib/irb/command/whereami.rb | 23 + lib/irb/ext/use-loader.rb | 4 +- lib/irb/extend-command.rb | 358 --------------- lib/irb/statement.rb | 6 +- test/irb/test_cmd.rb | 981 ----------------------------------------- test/irb/test_command.rb | 980 ++++++++++++++++++++++++++++++++++++++++ 61 files changed, 2476 insertions(+), 2487 deletions(-) delete mode 100644 lib/irb/cmd/backtrace.rb delete mode 100644 lib/irb/cmd/break.rb delete mode 100644 lib/irb/cmd/catch.rb delete mode 100644 lib/irb/cmd/chws.rb delete mode 100644 lib/irb/cmd/continue.rb delete mode 100644 lib/irb/cmd/debug.rb delete mode 100644 lib/irb/cmd/delete.rb delete mode 100644 lib/irb/cmd/edit.rb delete mode 100644 lib/irb/cmd/exit.rb delete mode 100644 lib/irb/cmd/finish.rb delete mode 100644 lib/irb/cmd/force_exit.rb delete mode 100644 lib/irb/cmd/help.rb delete mode 100644 lib/irb/cmd/history.rb delete mode 100644 lib/irb/cmd/info.rb delete mode 100644 lib/irb/cmd/irb_info.rb delete mode 100644 lib/irb/cmd/load.rb delete mode 100644 lib/irb/cmd/ls.rb delete mode 100644 lib/irb/cmd/measure.rb delete mode 100644 lib/irb/cmd/next.rb delete mode 100644 lib/irb/cmd/pushws.rb delete mode 100644 lib/irb/cmd/show_cmds.rb delete mode 100644 lib/irb/cmd/show_doc.rb delete mode 100644 lib/irb/cmd/show_source.rb delete mode 100644 lib/irb/cmd/step.rb delete mode 100644 lib/irb/cmd/subirb.rb delete mode 100644 lib/irb/cmd/whereami.rb create mode 100644 lib/irb/command.rb create mode 100644 lib/irb/command/backtrace.rb create mode 100644 lib/irb/command/base.rb create mode 100644 lib/irb/command/break.rb create mode 100644 lib/irb/command/catch.rb create mode 100644 lib/irb/command/chws.rb create mode 100644 lib/irb/command/continue.rb create mode 100644 lib/irb/command/debug.rb create mode 100644 lib/irb/command/delete.rb create mode 100644 lib/irb/command/edit.rb create mode 100644 lib/irb/command/exit.rb create mode 100644 lib/irb/command/finish.rb create mode 100644 lib/irb/command/force_exit.rb create mode 100644 lib/irb/command/help.rb create mode 100644 lib/irb/command/history.rb create mode 100644 lib/irb/command/info.rb create mode 100644 lib/irb/command/irb_info.rb create mode 100644 lib/irb/command/load.rb create mode 100644 lib/irb/command/ls.rb create mode 100644 lib/irb/command/measure.rb create mode 100644 lib/irb/command/next.rb create mode 100644 lib/irb/command/pushws.rb create mode 100644 lib/irb/command/show_cmds.rb create mode 100644 lib/irb/command/show_doc.rb create mode 100644 lib/irb/command/show_source.rb create mode 100644 lib/irb/command/step.rb create mode 100644 lib/irb/command/subirb.rb create mode 100644 lib/irb/command/whereami.rb delete mode 100644 lib/irb/extend-command.rb delete mode 100644 test/irb/test_cmd.rb create mode 100644 test/irb/test_command.rb diff --git a/lib/irb.rb b/lib/irb.rb index fd06626e9b..73d96947ea 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -9,7 +9,7 @@ require "reline" require_relative "irb/init" require_relative "irb/context" -require_relative "irb/extend-command" +require_relative "irb/command" require_relative "irb/ruby-lex" require_relative "irb/statement" diff --git a/lib/irb/cmd/backtrace.rb b/lib/irb/cmd/backtrace.rb deleted file mode 100644 index f632894618..0000000000 --- a/lib/irb/cmd/backtrace.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Backtrace < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(*args) - super(pre_cmds: ["backtrace", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/break.rb b/lib/irb/cmd/break.rb deleted file mode 100644 index df259a90ca..0000000000 --- a/lib/irb/cmd/break.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Break < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(args = nil) - super(pre_cmds: "break #{args}") - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/catch.rb b/lib/irb/cmd/catch.rb deleted file mode 100644 index 40b62c7533..0000000000 --- a/lib/irb/cmd/catch.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Catch < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(*args) - super(pre_cmds: ["catch", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/chws.rb b/lib/irb/cmd/chws.rb deleted file mode 100644 index 31045f9bbb..0000000000 --- a/lib/irb/cmd/chws.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: false -# -# change-ws.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -require_relative "nop" -require_relative "../ext/change-ws" - -module IRB - # :stopdoc: - - module ExtendCommand - - class CurrentWorkingWorkspace < Nop - category "Workspace" - description "Show the current workspace." - - def execute(*obj) - irb_context.main - end - end - - class ChangeWorkspace < Nop - category "Workspace" - description "Change the current workspace to an object." - - def execute(*obj) - irb_context.change_workspace(*obj) - irb_context.main - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/continue.rb b/lib/irb/cmd/continue.rb deleted file mode 100644 index 9136177eef..0000000000 --- a/lib/irb/cmd/continue.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Continue < DebugCommand - def execute(*args) - super(do_cmds: ["continue", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/debug.rb b/lib/irb/cmd/debug.rb deleted file mode 100644 index e236084ca4..0000000000 --- a/lib/irb/cmd/debug.rb +++ /dev/null @@ -1,80 +0,0 @@ -require_relative "nop" -require_relative "../debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Debug < Nop - category "Debugging" - description "Start the debugger of debug.gem." - - BINDING_IRB_FRAME_REGEXPS = [ - '', - binding.method(:irb).source_location.first, - ].map { |file| /\A#{Regexp.escape(file)}:\d+:in `irb'\z/ } - - def execute(pre_cmds: nil, do_cmds: nil) - if irb_context.with_debugger - # If IRB is already running with a debug session, throw the command and IRB.debug_readline will pass it to the debugger. - if cmd = pre_cmds || do_cmds - throw :IRB_EXIT, cmd - else - puts "IRB is already running with a debug session." - return - end - else - # If IRB is not running with a debug session yet, then: - # 1. Check if the debugging command is run from a `binding.irb` call. - # 2. If so, try setting up the debug gem. - # 3. Insert a debug breakpoint at `Irb#debug_break` with the intended command. - # 4. Exit the current Irb#run call via `throw :IRB_EXIT`. - # 5. `Irb#debug_break` will be called and trigger the breakpoint, which will run the intended command. - unless binding_irb? - puts "Debugging commands are only available when IRB is started with binding.irb" - return - end - - if IRB.respond_to?(:JobManager) - warn "Can't start the debugger when IRB is running in a multi-IRB session." - return - end - - unless IRB::Debug.setup(irb_context.irb) - puts <<~MSG - You need to install the debug gem before using this command. - If you use `bundle exec`, please add `gem "debug"` into your Gemfile. - MSG - return - end - - IRB::Debug.insert_debug_break(pre_cmds: pre_cmds, do_cmds: do_cmds) - - # exit current Irb#run call - throw :IRB_EXIT - end - end - - private - - def binding_irb? - caller.any? do |frame| - BINDING_IRB_FRAME_REGEXPS.any? do |regexp| - frame.match?(regexp) - end - end - end - end - - class DebugCommand < Debug - def self.category - "Debugging" - end - - def self.description - command_name = self.name.split("::").last.downcase - "Start the debugger of debug.gem and run its `#{command_name}` command." - end - end - end -end diff --git a/lib/irb/cmd/delete.rb b/lib/irb/cmd/delete.rb deleted file mode 100644 index aeb26d2572..0000000000 --- a/lib/irb/cmd/delete.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Delete < DebugCommand - def execute(*args) - super(pre_cmds: ["delete", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/edit.rb b/lib/irb/cmd/edit.rb deleted file mode 100644 index dae65f3c39..0000000000 --- a/lib/irb/cmd/edit.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'shellwords' -require_relative "nop" -require_relative "../source_finder" - -module IRB - # :stopdoc: - - module ExtendCommand - class Edit < Nop - category "Misc" - description 'Open a file with the editor command defined with `ENV["VISUAL"]` or `ENV["EDITOR"]`.' - - class << self - def transform_args(args) - # Return a string literal as is for backward compatibility - if args.nil? || args.empty? || string_literal?(args) - args - else # Otherwise, consider the input as a String for convenience - args.strip.dump - end - end - end - - def execute(*args) - path = args.first - - if path.nil? - path = @irb_context.irb_path - elsif !File.exist?(path) - source = SourceFinder.new(@irb_context).find_source(path) - - if source&.file_exist? && !source.binary_file? - path = source.file - end - end - - unless File.exist?(path) - puts "Can not find file: #{path}" - return - end - - if editor = (ENV['VISUAL'] || ENV['EDITOR']) - puts "command: '#{editor}'" - puts " path: #{path}" - system(*Shellwords.split(editor), path) - else - puts "Can not find editor setting: ENV['VISUAL'] or ENV['EDITOR']" - end - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/exit.rb b/lib/irb/cmd/exit.rb deleted file mode 100644 index 415e555335..0000000000 --- a/lib/irb/cmd/exit.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" - -module IRB - # :stopdoc: - - module ExtendCommand - class Exit < Nop - category "IRB" - description "Exit the current irb session." - - def execute(*) - IRB.irb_exit - rescue UncaughtThrowError - Kernel.exit - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/finish.rb b/lib/irb/cmd/finish.rb deleted file mode 100644 index 29f100feb5..0000000000 --- a/lib/irb/cmd/finish.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Finish < DebugCommand - def execute(*args) - super(do_cmds: ["finish", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/force_exit.rb b/lib/irb/cmd/force_exit.rb deleted file mode 100644 index 7e9b308de0..0000000000 --- a/lib/irb/cmd/force_exit.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" - -module IRB - # :stopdoc: - - module ExtendCommand - class ForceExit < Nop - category "IRB" - description "Exit the current process." - - def execute(*) - throw :IRB_EXIT, true - rescue UncaughtThrowError - Kernel.exit! - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/help.rb b/lib/irb/cmd/help.rb deleted file mode 100644 index 7f894688cc..0000000000 --- a/lib/irb/cmd/help.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require_relative "show_cmds" - -module IRB - module ExtendCommand - class Help < ShowCmds - category "IRB" - description "List all available commands and their description." - end - end -end diff --git a/lib/irb/cmd/history.rb b/lib/irb/cmd/history.rb deleted file mode 100644 index 5b712fa44d..0000000000 --- a/lib/irb/cmd/history.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require "stringio" -require_relative "nop" -require_relative "../pager" - -module IRB - # :stopdoc: - - module ExtendCommand - class History < Nop - category "IRB" - description "Shows the input history. `-g [query]` or `-G [query]` allows you to filter the output." - - def self.transform_args(args) - match = args&.match(/(-g|-G)\s+(?.+)\s*\n\z/) - return unless match - - "grep: #{Regexp.new(match[:grep]).inspect}" - end - - def execute(grep: nil) - formatted_inputs = irb_context.io.class::HISTORY.each_with_index.reverse_each.filter_map do |input, index| - next if grep && !input.match?(grep) - - header = "#{index}: " - - first_line, *other_lines = input.split("\n") - first_line = "#{header}#{first_line}" - - truncated_lines = other_lines.slice!(1..) # Show 1 additional line (2 total) - other_lines << "..." if truncated_lines&.any? - - other_lines.map! do |line| - " " * header.length + line - end - - [first_line, *other_lines].join("\n") + "\n" - end - - Pager.page_content(formatted_inputs.join) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/info.rb b/lib/irb/cmd/info.rb deleted file mode 100644 index 2c0a32b34f..0000000000 --- a/lib/irb/cmd/info.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Info < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(*args) - super(pre_cmds: ["info", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/irb_info.rb b/lib/irb/cmd/irb_info.rb deleted file mode 100644 index 5b905a09bd..0000000000 --- a/lib/irb/cmd/irb_info.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: false - -require_relative "nop" - -module IRB - # :stopdoc: - - module ExtendCommand - class IrbInfo < Nop - category "IRB" - description "Show information about IRB." - - def execute - str = "Ruby version: #{RUBY_VERSION}\n" - str += "IRB version: #{IRB.version}\n" - str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n" - str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n" - str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file) - str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n" - str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty? - str += "LC_ALL env: #{ENV["LC_ALL"]}\n" if ENV["LC_ALL"] && !ENV["LC_ALL"].empty? - str += "East Asian Ambiguous Width: #{Reline.ambiguous_width.inspect}\n" - if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/ - codepage = `chcp`.b.sub(/.*: (\d+)\n/, '\1') - str += "Code page: #{codepage}\n" - end - puts str - nil - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/load.rb b/lib/irb/cmd/load.rb deleted file mode 100644 index a3e797a7e0..0000000000 --- a/lib/irb/cmd/load.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: false -# -# load.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -require_relative "nop" -require_relative "../ext/loader" - -module IRB - # :stopdoc: - - module ExtendCommand - class LoaderCommand < Nop - include IrbLoader - - def raise_cmd_argument_error - raise CommandArgumentError.new("Please specify the file name.") - end - end - - class Load < LoaderCommand - category "IRB" - description "Load a Ruby file." - - def execute(file_name = nil, priv = nil) - raise_cmd_argument_error unless file_name - irb_load(file_name, priv) - end - end - - class Require < LoaderCommand - category "IRB" - description "Require a Ruby file." - def execute(file_name = nil) - raise_cmd_argument_error unless file_name - - rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?") - return false if $".find{|f| f =~ rex} - - case file_name - when /\.rb$/ - begin - if irb_load(file_name) - $".push file_name - return true - end - rescue LoadError - end - when /\.(so|o|sl)$/ - return ruby_require(file_name) - end - - begin - irb_load(f = file_name + ".rb") - $".push f - return true - rescue LoadError - return ruby_require(file_name) - end - end - end - - class Source < LoaderCommand - category "IRB" - description "Loads a given file in the current session." - - def execute(file_name = nil) - raise_cmd_argument_error unless file_name - - source_file(file_name) - end - end - end - # :startdoc: -end diff --git a/lib/irb/cmd/ls.rb b/lib/irb/cmd/ls.rb deleted file mode 100644 index 791b1c1b21..0000000000 --- a/lib/irb/cmd/ls.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -require "reline" -require "stringio" -require_relative "nop" -require_relative "../pager" -require_relative "../color" - -module IRB - # :stopdoc: - - module ExtendCommand - class Ls < Nop - category "Context" - description "Show methods, constants, and variables. `-g [query]` or `-G [query]` allows you to filter out the output." - - def self.transform_args(args) - if match = args&.match(/\A(?.+\s|)(-g|-G)\s+(?[^\s]+)\s*\n\z/) - args = match[:args] - "#{args}#{',' unless args.chomp.empty?} grep: /#{match[:grep]}/" - else - args - end - end - - def execute(*arg, grep: nil) - o = Output.new(grep: grep) - - obj = arg.empty? ? irb_context.workspace.main : arg.first - locals = arg.empty? ? irb_context.workspace.binding.local_variables : [] - klass = (obj.class == Class || obj.class == Module ? obj : obj.class) - - o.dump("constants", obj.constants) if obj.respond_to?(:constants) - dump_methods(o, klass, obj) - o.dump("instance variables", obj.instance_variables) - o.dump("class variables", klass.class_variables) - o.dump("locals", locals) - o.print_result - end - - def dump_methods(o, klass, obj) - singleton_class = begin obj.singleton_class; rescue TypeError; nil end - dumped_mods = Array.new - ancestors = klass.ancestors - ancestors = ancestors.reject { |c| c >= Object } if klass < Object - singleton_ancestors = (singleton_class&.ancestors || []).reject { |c| c >= Class } - - # singleton_class' ancestors should be at the front - maps = class_method_map(singleton_ancestors, dumped_mods) + class_method_map(ancestors, dumped_mods) - maps.each do |mod, methods| - name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods" - o.dump(name, methods) - end - end - - def class_method_map(classes, dumped_mods) - dumped_methods = Array.new - classes.map do |mod| - next if dumped_mods.include? mod - - dumped_mods << mod - - methods = mod.public_instance_methods(false).select do |method| - if dumped_methods.include? method - false - else - dumped_methods << method - true - end - end - - [mod, methods] - end.compact - end - - class Output - MARGIN = " " - - def initialize(grep: nil) - @grep = grep - @line_width = screen_width - MARGIN.length # right padding - @io = StringIO.new - end - - def print_result - Pager.page_content(@io.string) - end - - def dump(name, strs) - strs = strs.grep(@grep) if @grep - strs = strs.sort - return if strs.empty? - - # Attempt a single line - @io.print "#{Color.colorize(name, [:BOLD, :BLUE])}: " - if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length) - @io.puts strs.join(MARGIN) - return - end - @io.puts - - # Dump with the largest # of columns that fits on a line - cols = strs.size - until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1 - cols -= 1 - end - widths = col_widths(strs, cols: cols) - strs.each_slice(cols) do |ss| - @io.puts ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join - end - end - - private - - def fits_on_line?(strs, cols:, offset: 0) - width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1) - width <= @line_width - offset - end - - def col_widths(strs, cols:) - cols.times.map do |col| - (col...strs.size).step(cols).map do |i| - strs[i].length - end.max - end - end - - def screen_width - Reline.get_screen_size.last - rescue Errno::EINVAL # in `winsize': Invalid argument - - 80 - end - end - private_constant :Output - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/measure.rb b/lib/irb/cmd/measure.rb deleted file mode 100644 index 4e1125a0a6..0000000000 --- a/lib/irb/cmd/measure.rb +++ /dev/null @@ -1,45 +0,0 @@ -require_relative "nop" - -module IRB - # :stopdoc: - - module ExtendCommand - class Measure < Nop - category "Misc" - description "`measure` enables the mode to measure processing time. `measure :off` disables it." - - def initialize(*args) - super(*args) - end - - def execute(type = nil, arg = nil) - # Please check IRB.init_config in lib/irb/init.rb that sets - # IRB.conf[:MEASURE_PROC] to register default "measure" methods, - # "measure :time" (abbreviated as "measure") and "measure :stackprof". - - if block_given? - warn 'Configure IRB.conf[:MEASURE_PROC] to add custom measure methods.' - return - end - - case type - when :off - IRB.unset_measure_callback(arg) - when :list - IRB.conf[:MEASURE_CALLBACKS].each do |type_name, _, arg_val| - puts "- #{type_name}" + (arg_val ? "(#{arg_val.inspect})" : '') - end - when :on - added = IRB.set_measure_callback(arg) - puts "#{added[0]} is added." if added - else - added = IRB.set_measure_callback(type, arg) - puts "#{added[0]} is added." if added - end - nil - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/next.rb b/lib/irb/cmd/next.rb deleted file mode 100644 index d29c82e7fc..0000000000 --- a/lib/irb/cmd/next.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Next < DebugCommand - def execute(*args) - super(do_cmds: ["next", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb index 7fb197c51f..49f89bac95 100644 --- a/lib/irb/cmd/nop.rb +++ b/lib/irb/cmd/nop.rb @@ -1,53 +1,4 @@ -# frozen_string_literal: false -# -# nop.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# +# frozen_string_literal: true -module IRB - # :stopdoc: - - module ExtendCommand - class CommandArgumentError < StandardError; end - - class Nop - class << self - def category(category = nil) - @category = category if category - @category - end - - def description(description = nil) - @description = description if description - @description - end - - private - - def string_literal?(args) - sexp = Ripper.sexp(args) - sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal - end - end - - def self.execute(irb_context, *opts, **kwargs, &block) - command = new(irb_context) - command.execute(*opts, **kwargs, &block) - rescue CommandArgumentError => e - puts e.message - end - - def initialize(irb_context) - @irb_context = irb_context - end - - attr_reader :irb_context - - def execute(*opts) - #nop - end - end - end - - # :startdoc: -end +# This file is just a placeholder for backward-compatibility. +# Please require 'irb' and inheirt your command from `IRB::Command::Base` instead. diff --git a/lib/irb/cmd/pushws.rb b/lib/irb/cmd/pushws.rb deleted file mode 100644 index 59996ceb0c..0000000000 --- a/lib/irb/cmd/pushws.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: false -# -# change-ws.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -require_relative "nop" -require_relative "../ext/workspaces" - -module IRB - # :stopdoc: - - module ExtendCommand - class Workspaces < Nop - category "Workspace" - description "Show workspaces." - - def execute(*obj) - irb_context.workspaces.collect{|ws| ws.main} - end - end - - class PushWorkspace < Workspaces - category "Workspace" - description "Push an object to the workspace stack." - - def execute(*obj) - irb_context.push_workspace(*obj) - super - end - end - - class PopWorkspace < Workspaces - category "Workspace" - description "Pop a workspace from the workspace stack." - - def execute(*obj) - irb_context.pop_workspace(*obj) - super - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/show_cmds.rb b/lib/irb/cmd/show_cmds.rb deleted file mode 100644 index a8d899e4ac..0000000000 --- a/lib/irb/cmd/show_cmds.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require "stringio" -require_relative "nop" -require_relative "../pager" - -module IRB - # :stopdoc: - - module ExtendCommand - class ShowCmds < Nop - category "IRB" - description "List all available commands and their description." - - def execute(*args) - commands_info = IRB::ExtendCommandBundle.all_commands_info - commands_grouped_by_categories = commands_info.group_by { |cmd| cmd[:category] } - - user_aliases = irb_context.instance_variable_get(:@user_aliases) - - commands_grouped_by_categories["Aliases"] = user_aliases.map do |alias_name, target| - { display_name: alias_name, description: "Alias for `#{target}`" } - end - - if irb_context.with_debugger - # Remove the original "Debugging" category - commands_grouped_by_categories.delete("Debugging") - # Remove the `help` command as it's delegated to the debugger - commands_grouped_by_categories["Context"].delete_if { |cmd| cmd[:display_name] == :help } - # Add an empty "Debugging (from debug.gem)" category at the end - commands_grouped_by_categories["Debugging (from debug.gem)"] = [] - end - - longest_cmd_name_length = commands_info.map { |c| c[:display_name].length }.max - - output = StringIO.new - - commands_grouped_by_categories.each do |category, cmds| - output.puts Color.colorize(category, [:BOLD]) - - cmds.each do |cmd| - output.puts " #{cmd[:display_name].to_s.ljust(longest_cmd_name_length)} #{cmd[:description]}" - end - - output.puts - end - - # Append the debugger help at the end - if irb_context.with_debugger - output.puts DEBUGGER__.help - end - - Pager.page_content(output.string) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/show_doc.rb b/lib/irb/cmd/show_doc.rb deleted file mode 100644 index 99dd9ab95a..0000000000 --- a/lib/irb/cmd/show_doc.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" - -module IRB - module ExtendCommand - class ShowDoc < Nop - class << self - def transform_args(args) - # Return a string literal as is for backward compatibility - if args.empty? || string_literal?(args) - args - else # Otherwise, consider the input as a String for convenience - args.strip.dump - end - end - end - - category "Context" - description "Enter the mode to look up RI documents." - - def execute(*names) - require 'rdoc/ri/driver' - - unless ShowDoc.const_defined?(:Ri) - opts = RDoc::RI::Driver.process_args([]) - ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts)) - end - - if names.empty? - Ri.interactive - else - names.each do |name| - begin - Ri.display_name(name.to_s) - rescue RDoc::RI::Error - puts $!.message - end - end - end - - nil - rescue LoadError, SystemExit - warn "Can't display document because `rdoc` is not installed." - end - end - end -end diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb deleted file mode 100644 index cd07de3e90..0000000000 --- a/lib/irb/cmd/show_source.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" -require_relative "../source_finder" -require_relative "../pager" -require_relative "../color" - -module IRB - module ExtendCommand - class ShowSource < Nop - category "Context" - description "Show the source code of a given method or constant." - - class << self - def transform_args(args) - # Return a string literal as is for backward compatibility - if args.empty? || string_literal?(args) - args - else # Otherwise, consider the input as a String for convenience - args.strip.dump - end - end - end - - def execute(str = nil) - unless str.is_a?(String) - puts "Error: Expected a string but got #{str.inspect}" - return - end - - str, esses = str.split(" -") - super_level = esses ? esses.count("s") : 0 - source = SourceFinder.new(@irb_context).find_source(str, super_level) - - if source - show_source(source) - elsif super_level > 0 - puts "Error: Couldn't locate a super definition for #{str}" - else - puts "Error: Couldn't locate a definition for #{str}" - end - nil - end - - private - - def show_source(source) - if source.binary_file? - content = "\n#{bold('Defined in binary file')}: #{source.file}\n\n" - else - code = source.colorized_content || 'Source not available' - content = <<~CONTENT - - #{bold("From")}: #{source.file}:#{source.line} - - #{code.chomp} - - CONTENT - end - Pager.page_content(content) - end - - def bold(str) - Color.colorize(str, [:BOLD]) - end - end - end -end diff --git a/lib/irb/cmd/step.rb b/lib/irb/cmd/step.rb deleted file mode 100644 index 2bc74a9d79..0000000000 --- a/lib/irb/cmd/step.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Step < DebugCommand - def execute(*args) - super(do_cmds: ["step", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/cmd/subirb.rb deleted file mode 100644 index 5ffd646416..0000000000 --- a/lib/irb/cmd/subirb.rb +++ /dev/null @@ -1,109 +0,0 @@ -# frozen_string_literal: false -# -# multi.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -require_relative "nop" - -module IRB - # :stopdoc: - - module ExtendCommand - class MultiIRBCommand < Nop - def execute(*args) - extend_irb_context - end - - private - - def print_deprecated_warning - warn <<~MSG - Multi-irb commands are deprecated and will be removed in IRB 2.0.0. Please use workspace commands instead. - If you have any use case for multi-irb, please leave a comment at https://github.com/ruby/irb/issues/653 - MSG - end - - def extend_irb_context - # this extension patches IRB context like IRB.CurrentContext - require_relative "../ext/multi-irb" - end - - def print_debugger_warning - warn "Multi-IRB commands are not available when the debugger is enabled." - end - end - - class IrbCommand < MultiIRBCommand - category "Multi-irb (DEPRECATED)" - description "Start a child IRB." - - def execute(*obj) - print_deprecated_warning - - if irb_context.with_debugger - print_debugger_warning - return - end - - super - IRB.irb(nil, *obj) - end - end - - class Jobs < MultiIRBCommand - category "Multi-irb (DEPRECATED)" - description "List of current sessions." - - def execute - print_deprecated_warning - - if irb_context.with_debugger - print_debugger_warning - return - end - - super - IRB.JobManager - end - end - - class Foreground < MultiIRBCommand - category "Multi-irb (DEPRECATED)" - description "Switches to the session of the given number." - - def execute(key = nil) - print_deprecated_warning - - if irb_context.with_debugger - print_debugger_warning - return - end - - super - - raise CommandArgumentError.new("Please specify the id of target IRB job (listed in the `jobs` command).") unless key - IRB.JobManager.switch(key) - end - end - - class Kill < MultiIRBCommand - category "Multi-irb (DEPRECATED)" - description "Kills the session with the given number." - - def execute(*keys) - print_deprecated_warning - - if irb_context.with_debugger - print_debugger_warning - return - end - - super - IRB.JobManager.kill(*keys) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/whereami.rb b/lib/irb/cmd/whereami.rb deleted file mode 100644 index 8f56ba073d..0000000000 --- a/lib/irb/cmd/whereami.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" - -module IRB - # :stopdoc: - - module ExtendCommand - class Whereami < Nop - category "Context" - description "Show the source code around binding.irb again." - - def execute(*) - code = irb_context.workspace.code_around_binding - if code - puts code - else - puts "The current context doesn't have code." - end - end - end - end - - # :startdoc: -end diff --git a/lib/irb/command.rb b/lib/irb/command.rb new file mode 100644 index 0000000000..cab571cfe3 --- /dev/null +++ b/lib/irb/command.rb @@ -0,0 +1,363 @@ +# frozen_string_literal: false +# +# irb/command.rb - irb command +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +require_relative "command/base" + +module IRB # :nodoc: + module Command; end + ExtendCommand = Command + + # Installs the default irb extensions command bundle. + module ExtendCommandBundle + EXCB = ExtendCommandBundle # :nodoc: + + # See #install_alias_method. + NO_OVERRIDE = 0 + # See #install_alias_method. + OVERRIDE_PRIVATE_ONLY = 0x01 + # See #install_alias_method. + OVERRIDE_ALL = 0x02 + + # Displays current configuration. + # + # Modifying the configuration is achieved by sending a message to IRB.conf. + def irb_context + IRB.CurrentContext + end + + @ALIASES = [ + [:context, :irb_context, NO_OVERRIDE], + [:conf, :irb_context, NO_OVERRIDE], + ] + + + @EXTEND_COMMANDS = [ + [ + :irb_exit, :Exit, "command/exit", + [:exit, OVERRIDE_PRIVATE_ONLY], + [:quit, OVERRIDE_PRIVATE_ONLY], + [:irb_quit, OVERRIDE_PRIVATE_ONLY], + ], + [ + :irb_exit!, :ForceExit, "command/force_exit", + [:exit!, OVERRIDE_PRIVATE_ONLY], + ], + + [ + :irb_current_working_workspace, :CurrentWorkingWorkspace, "command/chws", + [:cwws, NO_OVERRIDE], + [:pwws, NO_OVERRIDE], + [:irb_print_working_workspace, OVERRIDE_ALL], + [:irb_cwws, OVERRIDE_ALL], + [:irb_pwws, OVERRIDE_ALL], + [:irb_current_working_binding, OVERRIDE_ALL], + [:irb_print_working_binding, OVERRIDE_ALL], + [:irb_cwb, OVERRIDE_ALL], + [:irb_pwb, OVERRIDE_ALL], + ], + [ + :irb_change_workspace, :ChangeWorkspace, "command/chws", + [:chws, NO_OVERRIDE], + [:cws, NO_OVERRIDE], + [:irb_chws, OVERRIDE_ALL], + [:irb_cws, OVERRIDE_ALL], + [:irb_change_binding, OVERRIDE_ALL], + [:irb_cb, OVERRIDE_ALL], + [:cb, NO_OVERRIDE], + ], + + [ + :irb_workspaces, :Workspaces, "command/pushws", + [:workspaces, NO_OVERRIDE], + [:irb_bindings, OVERRIDE_ALL], + [:bindings, NO_OVERRIDE], + ], + [ + :irb_push_workspace, :PushWorkspace, "command/pushws", + [:pushws, NO_OVERRIDE], + [:irb_pushws, OVERRIDE_ALL], + [:irb_push_binding, OVERRIDE_ALL], + [:irb_pushb, OVERRIDE_ALL], + [:pushb, NO_OVERRIDE], + ], + [ + :irb_pop_workspace, :PopWorkspace, "command/pushws", + [:popws, NO_OVERRIDE], + [:irb_popws, OVERRIDE_ALL], + [:irb_pop_binding, OVERRIDE_ALL], + [:irb_popb, OVERRIDE_ALL], + [:popb, NO_OVERRIDE], + ], + + [ + :irb_load, :Load, "command/load"], + [ + :irb_require, :Require, "command/load"], + [ + :irb_source, :Source, "command/load", + [:source, NO_OVERRIDE], + ], + + [ + :irb, :IrbCommand, "command/subirb"], + [ + :irb_jobs, :Jobs, "command/subirb", + [:jobs, NO_OVERRIDE], + ], + [ + :irb_fg, :Foreground, "command/subirb", + [:fg, NO_OVERRIDE], + ], + [ + :irb_kill, :Kill, "command/subirb", + [:kill, OVERRIDE_PRIVATE_ONLY], + ], + + [ + :irb_debug, :Debug, "command/debug", + [:debug, NO_OVERRIDE], + ], + [ + :irb_edit, :Edit, "command/edit", + [:edit, NO_OVERRIDE], + ], + [ + :irb_break, :Break, "command/break", + ], + [ + :irb_catch, :Catch, "command/catch", + ], + [ + :irb_next, :Next, "command/next" + ], + [ + :irb_delete, :Delete, "command/delete", + [:delete, NO_OVERRIDE], + ], + [ + :irb_step, :Step, "command/step", + [:step, NO_OVERRIDE], + ], + [ + :irb_continue, :Continue, "command/continue", + [:continue, NO_OVERRIDE], + ], + [ + :irb_finish, :Finish, "command/finish", + [:finish, NO_OVERRIDE], + ], + [ + :irb_backtrace, :Backtrace, "command/backtrace", + [:backtrace, NO_OVERRIDE], + [:bt, NO_OVERRIDE], + ], + [ + :irb_debug_info, :Info, "command/info", + [:info, NO_OVERRIDE], + ], + + [ + :irb_help, :Help, "command/help", + [:help, NO_OVERRIDE], + ], + + [ + :irb_show_doc, :ShowDoc, "command/show_doc", + [:show_doc, NO_OVERRIDE], + ], + + [ + :irb_info, :IrbInfo, "command/irb_info" + ], + + [ + :irb_ls, :Ls, "command/ls", + [:ls, NO_OVERRIDE], + ], + + [ + :irb_measure, :Measure, "command/measure", + [:measure, NO_OVERRIDE], + ], + + [ + :irb_show_source, :ShowSource, "command/show_source", + [:show_source, NO_OVERRIDE], + ], + + [ + :irb_whereami, :Whereami, "command/whereami", + [:whereami, NO_OVERRIDE], + ], + [ + :irb_show_cmds, :ShowCmds, "command/show_cmds", + [:show_cmds, NO_OVERRIDE], + ], + + [ + :irb_history, :History, "command/history", + [:history, NO_OVERRIDE], + [:hist, NO_OVERRIDE], + ] + ] + + + @@commands = [] + + def self.all_commands_info + return @@commands unless @@commands.empty? + user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result| + result[target] ||= [] + result[target] << alias_name + end + + @EXTEND_COMMANDS.each do |cmd_name, cmd_class, load_file, *aliases| + if !defined?(Command) || !Command.const_defined?(cmd_class, false) + require_relative load_file + end + + klass = Command.const_get(cmd_class, false) + aliases = aliases.map { |a| a.first } + + if additional_aliases = user_aliases[cmd_name] + aliases += additional_aliases + end + + display_name = aliases.shift || cmd_name + @@commands << { display_name: display_name, description: klass.description, category: klass.category } + end + + @@commands + end + + # Convert a command name to its implementation class if such command exists + def self.load_command(command) + command = command.to_sym + @EXTEND_COMMANDS.each do |cmd_name, cmd_class, load_file, *aliases| + next if cmd_name != command && aliases.all? { |alias_name, _| alias_name != command } + + if !defined?(Command) || !Command.const_defined?(cmd_class, false) + require_relative load_file + end + return Command.const_get(cmd_class, false) + end + nil + end + + # Installs the default irb commands. + def self.install_extend_commands + for args in @EXTEND_COMMANDS + def_extend_command(*args) + end + end + + # Evaluate the given +cmd_name+ on the given +cmd_class+ Class. + # + # Will also define any given +aliases+ for the method. + # + # The optional +load_file+ parameter will be required within the method + # definition. + def self.def_extend_command(cmd_name, cmd_class, load_file, *aliases) + case cmd_class + when Symbol + cmd_class = cmd_class.id2name + when String + when Class + cmd_class = cmd_class.name + end + + line = __LINE__; eval %[ + def #{cmd_name}(*opts, **kwargs, &b) + Kernel.require_relative "#{load_file}" + ::IRB::Command::#{cmd_class}.execute(irb_context, *opts, **kwargs, &b) + end + ], nil, __FILE__, line + + for ali, flag in aliases + @ALIASES.push [ali, cmd_name, flag] + end + end + + # Installs alias methods for the default irb commands, see + # ::install_extend_commands. + def install_alias_method(to, from, override = NO_OVERRIDE) + to = to.id2name unless to.kind_of?(String) + from = from.id2name unless from.kind_of?(String) + + if override == OVERRIDE_ALL or + (override == OVERRIDE_PRIVATE_ONLY) && !respond_to?(to) or + (override == NO_OVERRIDE) && !respond_to?(to, true) + target = self + (class << self; self; end).instance_eval{ + if target.respond_to?(to, true) && + !target.respond_to?(EXCB.irb_original_method_name(to), true) + alias_method(EXCB.irb_original_method_name(to), to) + end + alias_method to, from + } + else + Kernel.warn "irb: warn: can't alias #{to} from #{from}.\n" + end + end + + def self.irb_original_method_name(method_name) # :nodoc: + "irb_" + method_name + "_org" + end + + # Installs alias methods for the default irb commands on the given object + # using #install_alias_method. + def self.extend_object(obj) + unless (class << obj; ancestors; end).include?(EXCB) + super + for ali, com, flg in @ALIASES + obj.install_alias_method(ali, com, flg) + end + end + end + + install_extend_commands + end + + # Extends methods for the Context module + module ContextExtender + CE = ContextExtender # :nodoc: + + @EXTEND_COMMANDS = [ + [:eval_history=, "ext/eval_history.rb"], + [:use_loader=, "ext/use-loader.rb"], + ] + + # Installs the default context extensions as irb commands: + # + # Context#eval_history=:: +irb/ext/history.rb+ + # Context#use_tracer=:: +irb/ext/tracer.rb+ + # Context#use_loader=:: +irb/ext/use-loader.rb+ + def self.install_extend_commands + for args in @EXTEND_COMMANDS + def_extend_command(*args) + end + end + + # Evaluate the given +command+ from the given +load_file+ on the Context + # module. + # + # Will also define any given +aliases+ for the method. + def self.def_extend_command(cmd_name, load_file, *aliases) + line = __LINE__; Context.module_eval %[ + def #{cmd_name}(*opts, &b) + Context.module_eval {remove_method(:#{cmd_name})} + require_relative "#{load_file}" + __send__ :#{cmd_name}, *opts, &b + end + for ali in aliases + alias_method ali, cmd_name + end + ], __FILE__, line + end + + CE.install_extend_commands + end +end diff --git a/lib/irb/command/backtrace.rb b/lib/irb/command/backtrace.rb new file mode 100644 index 0000000000..47e5e60724 --- /dev/null +++ b/lib/irb/command/backtrace.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Backtrace < DebugCommand + def self.transform_args(args) + args&.dump + end + + def execute(*args) + super(pre_cmds: ["backtrace", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb new file mode 100644 index 0000000000..87d2fea356 --- /dev/null +++ b/lib/irb/command/base.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: false +# +# nop.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +module IRB + # :stopdoc: + + module Command + class CommandArgumentError < StandardError; end + + class Base + class << self + def category(category = nil) + @category = category if category + @category + end + + def description(description = nil) + @description = description if description + @description + end + + private + + def string_literal?(args) + sexp = Ripper.sexp(args) + sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal + end + end + + def self.execute(irb_context, *opts, **kwargs, &block) + command = new(irb_context) + command.execute(*opts, **kwargs, &block) + rescue CommandArgumentError => e + puts e.message + end + + def initialize(irb_context) + @irb_context = irb_context + end + + attr_reader :irb_context + + def execute(*opts) + #nop + end + end + + Nop = Base + end + + # :startdoc: +end diff --git a/lib/irb/command/break.rb b/lib/irb/command/break.rb new file mode 100644 index 0000000000..fa200f2d78 --- /dev/null +++ b/lib/irb/command/break.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Break < DebugCommand + def self.transform_args(args) + args&.dump + end + + def execute(args = nil) + super(pre_cmds: "break #{args}") + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/catch.rb b/lib/irb/command/catch.rb new file mode 100644 index 0000000000..6b2edff5e5 --- /dev/null +++ b/lib/irb/command/catch.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Catch < DebugCommand + def self.transform_args(args) + args&.dump + end + + def execute(*args) + super(pre_cmds: ["catch", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/chws.rb b/lib/irb/command/chws.rb new file mode 100644 index 0000000000..341d516155 --- /dev/null +++ b/lib/irb/command/chws.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: false +# +# change-ws.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# +require_relative "../ext/change-ws" + +module IRB + # :stopdoc: + + module Command + + class CurrentWorkingWorkspace < Base + category "Workspace" + description "Show the current workspace." + + def execute(*obj) + irb_context.main + end + end + + class ChangeWorkspace < Base + category "Workspace" + description "Change the current workspace to an object." + + def execute(*obj) + irb_context.change_workspace(*obj) + irb_context.main + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/continue.rb b/lib/irb/command/continue.rb new file mode 100644 index 0000000000..8b6ffc860c --- /dev/null +++ b/lib/irb/command/continue.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Continue < DebugCommand + def execute(*args) + super(do_cmds: ["continue", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/debug.rb b/lib/irb/command/debug.rb new file mode 100644 index 0000000000..9a2c641251 --- /dev/null +++ b/lib/irb/command/debug.rb @@ -0,0 +1,79 @@ +require_relative "../debug" + +module IRB + # :stopdoc: + + module Command + class Debug < Base + category "Debugging" + description "Start the debugger of debug.gem." + + BINDING_IRB_FRAME_REGEXPS = [ + '', + binding.method(:irb).source_location.first, + ].map { |file| /\A#{Regexp.escape(file)}:\d+:in `irb'\z/ } + + def execute(pre_cmds: nil, do_cmds: nil) + if irb_context.with_debugger + # If IRB is already running with a debug session, throw the command and IRB.debug_readline will pass it to the debugger. + if cmd = pre_cmds || do_cmds + throw :IRB_EXIT, cmd + else + puts "IRB is already running with a debug session." + return + end + else + # If IRB is not running with a debug session yet, then: + # 1. Check if the debugging command is run from a `binding.irb` call. + # 2. If so, try setting up the debug gem. + # 3. Insert a debug breakpoint at `Irb#debug_break` with the intended command. + # 4. Exit the current Irb#run call via `throw :IRB_EXIT`. + # 5. `Irb#debug_break` will be called and trigger the breakpoint, which will run the intended command. + unless binding_irb? + puts "Debugging commands are only available when IRB is started with binding.irb" + return + end + + if IRB.respond_to?(:JobManager) + warn "Can't start the debugger when IRB is running in a multi-IRB session." + return + end + + unless IRB::Debug.setup(irb_context.irb) + puts <<~MSG + You need to install the debug gem before using this command. + If you use `bundle exec`, please add `gem "debug"` into your Gemfile. + MSG + return + end + + IRB::Debug.insert_debug_break(pre_cmds: pre_cmds, do_cmds: do_cmds) + + # exit current Irb#run call + throw :IRB_EXIT + end + end + + private + + def binding_irb? + caller.any? do |frame| + BINDING_IRB_FRAME_REGEXPS.any? do |regexp| + frame.match?(regexp) + end + end + end + end + + class DebugCommand < Debug + def self.category + "Debugging" + end + + def self.description + command_name = self.name.split("::").last.downcase + "Start the debugger of debug.gem and run its `#{command_name}` command." + end + end + end +end diff --git a/lib/irb/command/delete.rb b/lib/irb/command/delete.rb new file mode 100644 index 0000000000..a36b4577b0 --- /dev/null +++ b/lib/irb/command/delete.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Delete < DebugCommand + def execute(*args) + super(pre_cmds: ["delete", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/edit.rb b/lib/irb/command/edit.rb new file mode 100644 index 0000000000..1a8ded6bcf --- /dev/null +++ b/lib/irb/command/edit.rb @@ -0,0 +1,54 @@ +require 'shellwords' + +require_relative "../source_finder" + +module IRB + # :stopdoc: + + module Command + class Edit < Base + category "Misc" + description 'Open a file with the editor command defined with `ENV["VISUAL"]` or `ENV["EDITOR"]`.' + + class << self + def transform_args(args) + # Return a string literal as is for backward compatibility + if args.nil? || args.empty? || string_literal?(args) + args + else # Otherwise, consider the input as a String for convenience + args.strip.dump + end + end + end + + def execute(*args) + path = args.first + + if path.nil? + path = @irb_context.irb_path + elsif !File.exist?(path) + source = SourceFinder.new(@irb_context).find_source(path) + + if source&.file_exist? && !source.binary_file? + path = source.file + end + end + + unless File.exist?(path) + puts "Can not find file: #{path}" + return + end + + if editor = (ENV['VISUAL'] || ENV['EDITOR']) + puts "command: '#{editor}'" + puts " path: #{path}" + system(*Shellwords.split(editor), path) + else + puts "Can not find editor setting: ENV['VISUAL'] or ENV['EDITOR']" + end + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/exit.rb b/lib/irb/command/exit.rb new file mode 100644 index 0000000000..3109ec16e3 --- /dev/null +++ b/lib/irb/command/exit.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module IRB + # :stopdoc: + + module Command + class Exit < Base + category "IRB" + description "Exit the current irb session." + + def execute(*) + IRB.irb_exit + rescue UncaughtThrowError + Kernel.exit + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/finish.rb b/lib/irb/command/finish.rb new file mode 100644 index 0000000000..05501819e2 --- /dev/null +++ b/lib/irb/command/finish.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Finish < DebugCommand + def execute(*args) + super(do_cmds: ["finish", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/force_exit.rb b/lib/irb/command/force_exit.rb new file mode 100644 index 0000000000..c2c5542e24 --- /dev/null +++ b/lib/irb/command/force_exit.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module IRB + # :stopdoc: + + module Command + class ForceExit < Base + category "IRB" + description "Exit the current process." + + def execute(*) + throw :IRB_EXIT, true + rescue UncaughtThrowError + Kernel.exit! + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/help.rb b/lib/irb/command/help.rb new file mode 100644 index 0000000000..67cc31a0bf --- /dev/null +++ b/lib/irb/command/help.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative "show_cmds" + +module IRB + module Command + class Help < ShowCmds + category "IRB" + description "List all available commands and their description." + end + end +end diff --git a/lib/irb/command/history.rb b/lib/irb/command/history.rb new file mode 100644 index 0000000000..a47a8795dd --- /dev/null +++ b/lib/irb/command/history.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "stringio" + +require_relative "../pager" + +module IRB + # :stopdoc: + + module Command + class History < Base + category "IRB" + description "Shows the input history. `-g [query]` or `-G [query]` allows you to filter the output." + + def self.transform_args(args) + match = args&.match(/(-g|-G)\s+(?.+)\s*\n\z/) + return unless match + + "grep: #{Regexp.new(match[:grep]).inspect}" + end + + def execute(grep: nil) + formatted_inputs = irb_context.io.class::HISTORY.each_with_index.reverse_each.filter_map do |input, index| + next if grep && !input.match?(grep) + + header = "#{index}: " + + first_line, *other_lines = input.split("\n") + first_line = "#{header}#{first_line}" + + truncated_lines = other_lines.slice!(1..) # Show 1 additional line (2 total) + other_lines << "..." if truncated_lines&.any? + + other_lines.map! do |line| + " " * header.length + line + end + + [first_line, *other_lines].join("\n") + "\n" + end + + Pager.page_content(formatted_inputs.join) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/info.rb b/lib/irb/command/info.rb new file mode 100644 index 0000000000..a67be3eb85 --- /dev/null +++ b/lib/irb/command/info.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Info < DebugCommand + def self.transform_args(args) + args&.dump + end + + def execute(*args) + super(pre_cmds: ["info", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/irb_info.rb b/lib/irb/command/irb_info.rb new file mode 100644 index 0000000000..7fd3e2104a --- /dev/null +++ b/lib/irb/command/irb_info.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: false + +module IRB + # :stopdoc: + + module Command + class IrbInfo < Base + category "IRB" + description "Show information about IRB." + + def execute + str = "Ruby version: #{RUBY_VERSION}\n" + str += "IRB version: #{IRB.version}\n" + str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n" + str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n" + str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file) + str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n" + str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty? + str += "LC_ALL env: #{ENV["LC_ALL"]}\n" if ENV["LC_ALL"] && !ENV["LC_ALL"].empty? + str += "East Asian Ambiguous Width: #{Reline.ambiguous_width.inspect}\n" + if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/ + codepage = `chcp`.b.sub(/.*: (\d+)\n/, '\1') + str += "Code page: #{codepage}\n" + end + puts str + nil + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/load.rb b/lib/irb/command/load.rb new file mode 100644 index 0000000000..0558bc83b0 --- /dev/null +++ b/lib/irb/command/load.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: false +# +# load.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# +require_relative "../ext/loader" + +module IRB + # :stopdoc: + + module Command + class LoaderCommand < Base + include IrbLoader + + def raise_cmd_argument_error + raise CommandArgumentError.new("Please specify the file name.") + end + end + + class Load < LoaderCommand + category "IRB" + description "Load a Ruby file." + + def execute(file_name = nil, priv = nil) + raise_cmd_argument_error unless file_name + irb_load(file_name, priv) + end + end + + class Require < LoaderCommand + category "IRB" + description "Require a Ruby file." + def execute(file_name = nil) + raise_cmd_argument_error unless file_name + + rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?") + return false if $".find{|f| f =~ rex} + + case file_name + when /\.rb$/ + begin + if irb_load(file_name) + $".push file_name + return true + end + rescue LoadError + end + when /\.(so|o|sl)$/ + return ruby_require(file_name) + end + + begin + irb_load(f = file_name + ".rb") + $".push f + return true + rescue LoadError + return ruby_require(file_name) + end + end + end + + class Source < LoaderCommand + category "IRB" + description "Loads a given file in the current session." + + def execute(file_name = nil) + raise_cmd_argument_error unless file_name + + source_file(file_name) + end + end + end + # :startdoc: +end diff --git a/lib/irb/command/ls.rb b/lib/irb/command/ls.rb new file mode 100644 index 0000000000..bbe4a1ee98 --- /dev/null +++ b/lib/irb/command/ls.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require "reline" +require "stringio" + +require_relative "../pager" +require_relative "../color" + +module IRB + # :stopdoc: + + module Command + class Ls < Base + category "Context" + description "Show methods, constants, and variables. `-g [query]` or `-G [query]` allows you to filter out the output." + + def self.transform_args(args) + if match = args&.match(/\A(?.+\s|)(-g|-G)\s+(?[^\s]+)\s*\n\z/) + args = match[:args] + "#{args}#{',' unless args.chomp.empty?} grep: /#{match[:grep]}/" + else + args + end + end + + def execute(*arg, grep: nil) + o = Output.new(grep: grep) + + obj = arg.empty? ? irb_context.workspace.main : arg.first + locals = arg.empty? ? irb_context.workspace.binding.local_variables : [] + klass = (obj.class == Class || obj.class == Module ? obj : obj.class) + + o.dump("constants", obj.constants) if obj.respond_to?(:constants) + dump_methods(o, klass, obj) + o.dump("instance variables", obj.instance_variables) + o.dump("class variables", klass.class_variables) + o.dump("locals", locals) + o.print_result + end + + def dump_methods(o, klass, obj) + singleton_class = begin obj.singleton_class; rescue TypeError; nil end + dumped_mods = Array.new + ancestors = klass.ancestors + ancestors = ancestors.reject { |c| c >= Object } if klass < Object + singleton_ancestors = (singleton_class&.ancestors || []).reject { |c| c >= Class } + + # singleton_class' ancestors should be at the front + maps = class_method_map(singleton_ancestors, dumped_mods) + class_method_map(ancestors, dumped_mods) + maps.each do |mod, methods| + name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods" + o.dump(name, methods) + end + end + + def class_method_map(classes, dumped_mods) + dumped_methods = Array.new + classes.map do |mod| + next if dumped_mods.include? mod + + dumped_mods << mod + + methods = mod.public_instance_methods(false).select do |method| + if dumped_methods.include? method + false + else + dumped_methods << method + true + end + end + + [mod, methods] + end.compact + end + + class Output + MARGIN = " " + + def initialize(grep: nil) + @grep = grep + @line_width = screen_width - MARGIN.length # right padding + @io = StringIO.new + end + + def print_result + Pager.page_content(@io.string) + end + + def dump(name, strs) + strs = strs.grep(@grep) if @grep + strs = strs.sort + return if strs.empty? + + # Attempt a single line + @io.print "#{Color.colorize(name, [:BOLD, :BLUE])}: " + if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length) + @io.puts strs.join(MARGIN) + return + end + @io.puts + + # Dump with the largest # of columns that fits on a line + cols = strs.size + until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1 + cols -= 1 + end + widths = col_widths(strs, cols: cols) + strs.each_slice(cols) do |ss| + @io.puts ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join + end + end + + private + + def fits_on_line?(strs, cols:, offset: 0) + width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1) + width <= @line_width - offset + end + + def col_widths(strs, cols:) + cols.times.map do |col| + (col...strs.size).step(cols).map do |i| + strs[i].length + end.max + end + end + + def screen_width + Reline.get_screen_size.last + rescue Errno::EINVAL # in `winsize': Invalid argument - + 80 + end + end + private_constant :Output + end + end + + # :startdoc: +end diff --git a/lib/irb/command/measure.rb b/lib/irb/command/measure.rb new file mode 100644 index 0000000000..ee7927b010 --- /dev/null +++ b/lib/irb/command/measure.rb @@ -0,0 +1,43 @@ +module IRB + # :stopdoc: + + module Command + class Measure < Base + category "Misc" + description "`measure` enables the mode to measure processing time. `measure :off` disables it." + + def initialize(*args) + super(*args) + end + + def execute(type = nil, arg = nil) + # Please check IRB.init_config in lib/irb/init.rb that sets + # IRB.conf[:MEASURE_PROC] to register default "measure" methods, + # "measure :time" (abbreviated as "measure") and "measure :stackprof". + + if block_given? + warn 'Configure IRB.conf[:MEASURE_PROC] to add custom measure methods.' + return + end + + case type + when :off + IRB.unset_measure_callback(arg) + when :list + IRB.conf[:MEASURE_CALLBACKS].each do |type_name, _, arg_val| + puts "- #{type_name}" + (arg_val ? "(#{arg_val.inspect})" : '') + end + when :on + added = IRB.set_measure_callback(arg) + puts "#{added[0]} is added." if added + else + added = IRB.set_measure_callback(type, arg) + puts "#{added[0]} is added." if added + end + nil + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/next.rb b/lib/irb/command/next.rb new file mode 100644 index 0000000000..6487c9d24c --- /dev/null +++ b/lib/irb/command/next.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Next < DebugCommand + def execute(*args) + super(do_cmds: ["next", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/pushws.rb b/lib/irb/command/pushws.rb new file mode 100644 index 0000000000..a6fcd6a165 --- /dev/null +++ b/lib/irb/command/pushws.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: false +# +# change-ws.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +require_relative "../ext/workspaces" + +module IRB + # :stopdoc: + + module Command + class Workspaces < Base + category "Workspace" + description "Show workspaces." + + def execute(*obj) + irb_context.workspaces.collect{|ws| ws.main} + end + end + + class PushWorkspace < Workspaces + category "Workspace" + description "Push an object to the workspace stack." + + def execute(*obj) + irb_context.push_workspace(*obj) + super + end + end + + class PopWorkspace < Workspaces + category "Workspace" + description "Pop a workspace from the workspace stack." + + def execute(*obj) + irb_context.pop_workspace(*obj) + super + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/show_cmds.rb b/lib/irb/command/show_cmds.rb new file mode 100644 index 0000000000..940ed490d3 --- /dev/null +++ b/lib/irb/command/show_cmds.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "stringio" + +require_relative "../pager" + +module IRB + # :stopdoc: + + module Command + class ShowCmds < Base + category "IRB" + description "List all available commands and their description." + + def execute(*args) + commands_info = IRB::ExtendCommandBundle.all_commands_info + commands_grouped_by_categories = commands_info.group_by { |cmd| cmd[:category] } + + user_aliases = irb_context.instance_variable_get(:@user_aliases) + + commands_grouped_by_categories["Aliases"] = user_aliases.map do |alias_name, target| + { display_name: alias_name, description: "Alias for `#{target}`" } + end + + if irb_context.with_debugger + # Remove the original "Debugging" category + commands_grouped_by_categories.delete("Debugging") + # Remove the `help` command as it's delegated to the debugger + commands_grouped_by_categories["Context"].delete_if { |cmd| cmd[:display_name] == :help } + # Add an empty "Debugging (from debug.gem)" category at the end + commands_grouped_by_categories["Debugging (from debug.gem)"] = [] + end + + longest_cmd_name_length = commands_info.map { |c| c[:display_name].length }.max + + output = StringIO.new + + commands_grouped_by_categories.each do |category, cmds| + output.puts Color.colorize(category, [:BOLD]) + + cmds.each do |cmd| + output.puts " #{cmd[:display_name].to_s.ljust(longest_cmd_name_length)} #{cmd[:description]}" + end + + output.puts + end + + # Append the debugger help at the end + if irb_context.with_debugger + output.puts DEBUGGER__.help + end + + Pager.page_content(output.string) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/show_doc.rb b/lib/irb/command/show_doc.rb new file mode 100644 index 0000000000..dca10ec4be --- /dev/null +++ b/lib/irb/command/show_doc.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module IRB + module Command + class ShowDoc < Base + class << self + def transform_args(args) + # Return a string literal as is for backward compatibility + if args.empty? || string_literal?(args) + args + else # Otherwise, consider the input as a String for convenience + args.strip.dump + end + end + end + + category "Context" + description "Enter the mode to look up RI documents." + + def execute(*names) + require 'rdoc/ri/driver' + + unless ShowDoc.const_defined?(:Ri) + opts = RDoc::RI::Driver.process_args([]) + ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts)) + end + + if names.empty? + Ri.interactive + else + names.each do |name| + begin + Ri.display_name(name.to_s) + rescue RDoc::RI::Error + puts $!.message + end + end + end + + nil + rescue LoadError, SystemExit + warn "Can't display document because `rdoc` is not installed." + end + end + end +end diff --git a/lib/irb/command/show_source.rb b/lib/irb/command/show_source.rb new file mode 100644 index 0000000000..cc783e7532 --- /dev/null +++ b/lib/irb/command/show_source.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require_relative "../source_finder" +require_relative "../pager" +require_relative "../color" + +module IRB + module Command + class ShowSource < Base + category "Context" + description "Show the source code of a given method or constant." + + class << self + def transform_args(args) + # Return a string literal as is for backward compatibility + if args.empty? || string_literal?(args) + args + else # Otherwise, consider the input as a String for convenience + args.strip.dump + end + end + end + + def execute(str = nil) + unless str.is_a?(String) + puts "Error: Expected a string but got #{str.inspect}" + return + end + + str, esses = str.split(" -") + super_level = esses ? esses.count("s") : 0 + source = SourceFinder.new(@irb_context).find_source(str, super_level) + + if source + show_source(source) + elsif super_level > 0 + puts "Error: Couldn't locate a super definition for #{str}" + else + puts "Error: Couldn't locate a definition for #{str}" + end + nil + end + + private + + def show_source(source) + if source.binary_file? + content = "\n#{bold('Defined in binary file')}: #{source.file}\n\n" + else + code = source.colorized_content || 'Source not available' + content = <<~CONTENT + + #{bold("From")}: #{source.file}:#{source.line} + + #{code.chomp} + + CONTENT + end + Pager.page_content(content) + end + + def bold(str) + Color.colorize(str, [:BOLD]) + end + end + end +end diff --git a/lib/irb/command/step.rb b/lib/irb/command/step.rb new file mode 100644 index 0000000000..cce7d2b0f8 --- /dev/null +++ b/lib/irb/command/step.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Step < DebugCommand + def execute(*args) + super(do_cmds: ["step", *args].join(" ")) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/subirb.rb b/lib/irb/command/subirb.rb new file mode 100644 index 0000000000..0a75706f5f --- /dev/null +++ b/lib/irb/command/subirb.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: false +# +# multi.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +module IRB + # :stopdoc: + + module Command + class MultiIRBCommand < Base + def execute(*args) + extend_irb_context + end + + private + + def print_deprecated_warning + warn <<~MSG + Multi-irb commands are deprecated and will be removed in IRB 2.0.0. Please use workspace commands instead. + If you have any use case for multi-irb, please leave a comment at https://github.com/ruby/irb/issues/653 + MSG + end + + def extend_irb_context + # this extension patches IRB context like IRB.CurrentContext + require_relative "../ext/multi-irb" + end + + def print_debugger_warning + warn "Multi-IRB commands are not available when the debugger is enabled." + end + end + + class IrbCommand < MultiIRBCommand + category "Multi-irb (DEPRECATED)" + description "Start a child IRB." + + def execute(*obj) + print_deprecated_warning + + if irb_context.with_debugger + print_debugger_warning + return + end + + super + IRB.irb(nil, *obj) + end + end + + class Jobs < MultiIRBCommand + category "Multi-irb (DEPRECATED)" + description "List of current sessions." + + def execute + print_deprecated_warning + + if irb_context.with_debugger + print_debugger_warning + return + end + + super + IRB.JobManager + end + end + + class Foreground < MultiIRBCommand + category "Multi-irb (DEPRECATED)" + description "Switches to the session of the given number." + + def execute(key = nil) + print_deprecated_warning + + if irb_context.with_debugger + print_debugger_warning + return + end + + super + + raise CommandArgumentError.new("Please specify the id of target IRB job (listed in the `jobs` command).") unless key + IRB.JobManager.switch(key) + end + end + + class Kill < MultiIRBCommand + category "Multi-irb (DEPRECATED)" + description "Kills the session with the given number." + + def execute(*keys) + print_deprecated_warning + + if irb_context.with_debugger + print_debugger_warning + return + end + + super + IRB.JobManager.kill(*keys) + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/whereami.rb b/lib/irb/command/whereami.rb new file mode 100644 index 0000000000..d6658d7043 --- /dev/null +++ b/lib/irb/command/whereami.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module IRB + # :stopdoc: + + module Command + class Whereami < Base + category "Context" + description "Show the source code around binding.irb again." + + def execute(*) + code = irb_context.workspace.code_around_binding + if code + puts code + else + puts "The current context doesn't have code." + end + end + end + end + + # :startdoc: +end diff --git a/lib/irb/ext/use-loader.rb b/lib/irb/ext/use-loader.rb index d0b8c2d4f4..c5ecbe2847 100644 --- a/lib/irb/ext/use-loader.rb +++ b/lib/irb/ext/use-loader.rb @@ -17,12 +17,12 @@ module IRB remove_method :irb_load if method_defined?(:irb_load) # Loads the given file similarly to Kernel#load, see IrbLoader#irb_load def irb_load(*opts, &b) - ExtendCommand::Load.execute(irb_context, *opts, &b) + Command::Load.execute(irb_context, *opts, &b) end remove_method :irb_require if method_defined?(:irb_require) # Loads the given file similarly to Kernel#require def irb_require(*opts, &b) - ExtendCommand::Require.execute(irb_context, *opts, &b) + Command::Require.execute(irb_context, *opts, &b) end end diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb deleted file mode 100644 index d303bf76da..0000000000 --- a/lib/irb/extend-command.rb +++ /dev/null @@ -1,358 +0,0 @@ -# frozen_string_literal: false -# -# irb/extend-command.rb - irb extend command -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -module IRB # :nodoc: - # Installs the default irb extensions command bundle. - module ExtendCommandBundle - EXCB = ExtendCommandBundle # :nodoc: - - # See #install_alias_method. - NO_OVERRIDE = 0 - # See #install_alias_method. - OVERRIDE_PRIVATE_ONLY = 0x01 - # See #install_alias_method. - OVERRIDE_ALL = 0x02 - - # Displays current configuration. - # - # Modifying the configuration is achieved by sending a message to IRB.conf. - def irb_context - IRB.CurrentContext - end - - @ALIASES = [ - [:context, :irb_context, NO_OVERRIDE], - [:conf, :irb_context, NO_OVERRIDE], - ] - - - @EXTEND_COMMANDS = [ - [ - :irb_exit, :Exit, "cmd/exit", - [:exit, OVERRIDE_PRIVATE_ONLY], - [:quit, OVERRIDE_PRIVATE_ONLY], - [:irb_quit, OVERRIDE_PRIVATE_ONLY], - ], - [ - :irb_exit!, :ForceExit, "cmd/force_exit", - [:exit!, OVERRIDE_PRIVATE_ONLY], - ], - - [ - :irb_current_working_workspace, :CurrentWorkingWorkspace, "cmd/chws", - [:cwws, NO_OVERRIDE], - [:pwws, NO_OVERRIDE], - [:irb_print_working_workspace, OVERRIDE_ALL], - [:irb_cwws, OVERRIDE_ALL], - [:irb_pwws, OVERRIDE_ALL], - [:irb_current_working_binding, OVERRIDE_ALL], - [:irb_print_working_binding, OVERRIDE_ALL], - [:irb_cwb, OVERRIDE_ALL], - [:irb_pwb, OVERRIDE_ALL], - ], - [ - :irb_change_workspace, :ChangeWorkspace, "cmd/chws", - [:chws, NO_OVERRIDE], - [:cws, NO_OVERRIDE], - [:irb_chws, OVERRIDE_ALL], - [:irb_cws, OVERRIDE_ALL], - [:irb_change_binding, OVERRIDE_ALL], - [:irb_cb, OVERRIDE_ALL], - [:cb, NO_OVERRIDE], - ], - - [ - :irb_workspaces, :Workspaces, "cmd/pushws", - [:workspaces, NO_OVERRIDE], - [:irb_bindings, OVERRIDE_ALL], - [:bindings, NO_OVERRIDE], - ], - [ - :irb_push_workspace, :PushWorkspace, "cmd/pushws", - [:pushws, NO_OVERRIDE], - [:irb_pushws, OVERRIDE_ALL], - [:irb_push_binding, OVERRIDE_ALL], - [:irb_pushb, OVERRIDE_ALL], - [:pushb, NO_OVERRIDE], - ], - [ - :irb_pop_workspace, :PopWorkspace, "cmd/pushws", - [:popws, NO_OVERRIDE], - [:irb_popws, OVERRIDE_ALL], - [:irb_pop_binding, OVERRIDE_ALL], - [:irb_popb, OVERRIDE_ALL], - [:popb, NO_OVERRIDE], - ], - - [ - :irb_load, :Load, "cmd/load"], - [ - :irb_require, :Require, "cmd/load"], - [ - :irb_source, :Source, "cmd/load", - [:source, NO_OVERRIDE], - ], - - [ - :irb, :IrbCommand, "cmd/subirb"], - [ - :irb_jobs, :Jobs, "cmd/subirb", - [:jobs, NO_OVERRIDE], - ], - [ - :irb_fg, :Foreground, "cmd/subirb", - [:fg, NO_OVERRIDE], - ], - [ - :irb_kill, :Kill, "cmd/subirb", - [:kill, OVERRIDE_PRIVATE_ONLY], - ], - - [ - :irb_debug, :Debug, "cmd/debug", - [:debug, NO_OVERRIDE], - ], - [ - :irb_edit, :Edit, "cmd/edit", - [:edit, NO_OVERRIDE], - ], - [ - :irb_break, :Break, "cmd/break", - ], - [ - :irb_catch, :Catch, "cmd/catch", - ], - [ - :irb_next, :Next, "cmd/next" - ], - [ - :irb_delete, :Delete, "cmd/delete", - [:delete, NO_OVERRIDE], - ], - [ - :irb_step, :Step, "cmd/step", - [:step, NO_OVERRIDE], - ], - [ - :irb_continue, :Continue, "cmd/continue", - [:continue, NO_OVERRIDE], - ], - [ - :irb_finish, :Finish, "cmd/finish", - [:finish, NO_OVERRIDE], - ], - [ - :irb_backtrace, :Backtrace, "cmd/backtrace", - [:backtrace, NO_OVERRIDE], - [:bt, NO_OVERRIDE], - ], - [ - :irb_debug_info, :Info, "cmd/info", - [:info, NO_OVERRIDE], - ], - - [ - :irb_help, :Help, "cmd/help", - [:help, NO_OVERRIDE], - ], - - [ - :irb_show_doc, :ShowDoc, "cmd/show_doc", - [:show_doc, NO_OVERRIDE], - ], - - [ - :irb_info, :IrbInfo, "cmd/irb_info" - ], - - [ - :irb_ls, :Ls, "cmd/ls", - [:ls, NO_OVERRIDE], - ], - - [ - :irb_measure, :Measure, "cmd/measure", - [:measure, NO_OVERRIDE], - ], - - [ - :irb_show_source, :ShowSource, "cmd/show_source", - [:show_source, NO_OVERRIDE], - ], - - [ - :irb_whereami, :Whereami, "cmd/whereami", - [:whereami, NO_OVERRIDE], - ], - [ - :irb_show_cmds, :ShowCmds, "cmd/show_cmds", - [:show_cmds, NO_OVERRIDE], - ], - - [ - :irb_history, :History, "cmd/history", - [:history, NO_OVERRIDE], - [:hist, NO_OVERRIDE], - ] - ] - - - @@commands = [] - - def self.all_commands_info - return @@commands unless @@commands.empty? - user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result| - result[target] ||= [] - result[target] << alias_name - end - - @EXTEND_COMMANDS.each do |cmd_name, cmd_class, load_file, *aliases| - if !defined?(ExtendCommand) || !ExtendCommand.const_defined?(cmd_class, false) - require_relative load_file - end - - klass = ExtendCommand.const_get(cmd_class, false) - aliases = aliases.map { |a| a.first } - - if additional_aliases = user_aliases[cmd_name] - aliases += additional_aliases - end - - display_name = aliases.shift || cmd_name - @@commands << { display_name: display_name, description: klass.description, category: klass.category } - end - - @@commands - end - - # Convert a command name to its implementation class if such command exists - def self.load_command(command) - command = command.to_sym - @EXTEND_COMMANDS.each do |cmd_name, cmd_class, load_file, *aliases| - next if cmd_name != command && aliases.all? { |alias_name, _| alias_name != command } - - if !defined?(ExtendCommand) || !ExtendCommand.const_defined?(cmd_class, false) - require_relative load_file - end - return ExtendCommand.const_get(cmd_class, false) - end - nil - end - - # Installs the default irb commands. - def self.install_extend_commands - for args in @EXTEND_COMMANDS - def_extend_command(*args) - end - end - - # Evaluate the given +cmd_name+ on the given +cmd_class+ Class. - # - # Will also define any given +aliases+ for the method. - # - # The optional +load_file+ parameter will be required within the method - # definition. - def self.def_extend_command(cmd_name, cmd_class, load_file, *aliases) - case cmd_class - when Symbol - cmd_class = cmd_class.id2name - when String - when Class - cmd_class = cmd_class.name - end - - line = __LINE__; eval %[ - def #{cmd_name}(*opts, **kwargs, &b) - Kernel.require_relative "#{load_file}" - ::IRB::ExtendCommand::#{cmd_class}.execute(irb_context, *opts, **kwargs, &b) - end - ], nil, __FILE__, line - - for ali, flag in aliases - @ALIASES.push [ali, cmd_name, flag] - end - end - - # Installs alias methods for the default irb commands, see - # ::install_extend_commands. - def install_alias_method(to, from, override = NO_OVERRIDE) - to = to.id2name unless to.kind_of?(String) - from = from.id2name unless from.kind_of?(String) - - if override == OVERRIDE_ALL or - (override == OVERRIDE_PRIVATE_ONLY) && !respond_to?(to) or - (override == NO_OVERRIDE) && !respond_to?(to, true) - target = self - (class << self; self; end).instance_eval{ - if target.respond_to?(to, true) && - !target.respond_to?(EXCB.irb_original_method_name(to), true) - alias_method(EXCB.irb_original_method_name(to), to) - end - alias_method to, from - } - else - Kernel.warn "irb: warn: can't alias #{to} from #{from}.\n" - end - end - - def self.irb_original_method_name(method_name) # :nodoc: - "irb_" + method_name + "_org" - end - - # Installs alias methods for the default irb commands on the given object - # using #install_alias_method. - def self.extend_object(obj) - unless (class << obj; ancestors; end).include?(EXCB) - super - for ali, com, flg in @ALIASES - obj.install_alias_method(ali, com, flg) - end - end - end - - install_extend_commands - end - - # Extends methods for the Context module - module ContextExtender - CE = ContextExtender # :nodoc: - - @EXTEND_COMMANDS = [ - [:eval_history=, "ext/eval_history.rb"], - [:use_loader=, "ext/use-loader.rb"], - ] - - # Installs the default context extensions as irb commands: - # - # Context#eval_history=:: +irb/ext/history.rb+ - # Context#use_tracer=:: +irb/ext/tracer.rb+ - # Context#use_loader=:: +irb/ext/use-loader.rb+ - def self.install_extend_commands - for args in @EXTEND_COMMANDS - def_extend_command(*args) - end - end - - # Evaluate the given +command+ from the given +load_file+ on the Context - # module. - # - # Will also define any given +aliases+ for the method. - def self.def_extend_command(cmd_name, load_file, *aliases) - line = __LINE__; Context.module_eval %[ - def #{cmd_name}(*opts, &b) - Context.module_eval {remove_method(:#{cmd_name})} - require_relative "#{load_file}" - __send__ :#{cmd_name}, *opts, &b - end - for ali in aliases - alias_method ali, cmd_name - end - ], __FILE__, line - end - - CE.install_extend_commands - end -end diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb index 4e17e51434..009eb2d20b 100644 --- a/lib/irb/statement.rb +++ b/lib/irb/statement.rb @@ -83,9 +83,9 @@ module IRB end def should_be_handled_by_debugger? - require_relative 'cmd/help' - require_relative 'cmd/debug' - IRB::ExtendCommand::DebugCommand > @command_class || IRB::ExtendCommand::Help == @command_class + require_relative 'command/help' + require_relative 'command/debug' + IRB::Command::DebugCommand > @command_class || IRB::Command::Help == @command_class end def evaluable_code diff --git a/test/irb/test_cmd.rb b/test/irb/test_cmd.rb deleted file mode 100644 index b9a155ec09..0000000000 --- a/test/irb/test_cmd.rb +++ /dev/null @@ -1,981 +0,0 @@ -# frozen_string_literal: false -require "rubygems" -require "irb" -require "irb/extend-command" - -require_relative "helper" - -module TestIRB - class CommandTestCase < TestCase - def setup - @pwd = Dir.pwd - @tmpdir = File.join(Dir.tmpdir, "test_reline_config_#{$$}") - begin - Dir.mkdir(@tmpdir) - rescue Errno::EEXIST - FileUtils.rm_rf(@tmpdir) - Dir.mkdir(@tmpdir) - end - Dir.chdir(@tmpdir) - @home_backup = ENV["HOME"] - ENV["HOME"] = @tmpdir - @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") - save_encodings - IRB.instance_variable_get(:@CONF).clear - @is_win = (RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/) - end - - def teardown - ENV["XDG_CONFIG_HOME"] = @xdg_config_home_backup - ENV["HOME"] = @home_backup - Dir.chdir(@pwd) - FileUtils.rm_rf(@tmpdir) - restore_encodings - end - - def execute_lines(*lines, conf: {}, main: self, irb_path: nil) - capture_output do - IRB.init_config(nil) - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :SIMPLE - IRB.conf[:USE_PAGER] = false - IRB.conf.merge!(conf) - input = TestInputMethod.new(lines) - irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) - irb.context.return_format = "=> %s\n" - irb.context.irb_path = irb_path if irb_path - IRB.conf[:MAIN_CONTEXT] = irb.context - irb.eval_input - end - end - end - - class FrozenObjectTest < CommandTestCase - def test_calling_command_on_a_frozen_main - main = Object.new.freeze - - out, err = execute_lines( - "irb_info", - main: main - ) - assert_empty(err) - assert_match(/RUBY_PLATFORM/, out) - end - end - - class InfoTest < CommandTestCase - def setup - super - @locals_backup = ENV.delete("LANG"), ENV.delete("LC_ALL") - end - - def teardown - super - ENV["LANG"], ENV["LC_ALL"] = @locals_backup - end - - def test_irb_info_multiline - FileUtils.touch("#{@tmpdir}/.inputrc") - FileUtils.touch("#{@tmpdir}/.irbrc") - - out, err = execute_lines( - "irb_info", - conf: { USE_MULTILINE: true, USE_SINGLELINE: false } - ) - - expected = %r{ - Ruby\sversion:\s.+\n - IRB\sversion:\sirb\s.+\n - InputMethod:\sAbstract\sInputMethod\n - Completion: .+\n - \.irbrc\spath:\s.+\n - RUBY_PLATFORM:\s.+\n - East\sAsian\sAmbiguous\sWidth:\s\d\n - #{@is_win ? 'Code\spage:\s\d+\n' : ''} - }x - - assert_empty err - assert_match expected, out - end - - def test_irb_info_singleline - FileUtils.touch("#{@tmpdir}/.inputrc") - FileUtils.touch("#{@tmpdir}/.irbrc") - - out, err = execute_lines( - "irb_info", - conf: { USE_MULTILINE: false, USE_SINGLELINE: true } - ) - - expected = %r{ - Ruby\sversion:\s.+\n - IRB\sversion:\sirb\s.+\n - InputMethod:\sAbstract\sInputMethod\n - Completion: .+\n - \.irbrc\spath:\s.+\n - RUBY_PLATFORM:\s.+\n - East\sAsian\sAmbiguous\sWidth:\s\d\n - #{@is_win ? 'Code\spage:\s\d+\n' : ''} - }x - - assert_empty err - assert_match expected, out - end - - def test_irb_info_multiline_without_rc_files - inputrc_backup = ENV["INPUTRC"] - ENV["INPUTRC"] = "unknown_inpurc" - ext_backup = IRB::IRBRC_EXT - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, "unknown_ext") - - out, err = execute_lines( - "irb_info", - conf: { USE_MULTILINE: true, USE_SINGLELINE: false } - ) - - expected = %r{ - Ruby\sversion:\s.+\n - IRB\sversion:\sirb\s.+\n - InputMethod:\sAbstract\sInputMethod\n - Completion: .+\n - RUBY_PLATFORM:\s.+\n - East\sAsian\sAmbiguous\sWidth:\s\d\n - #{@is_win ? 'Code\spage:\s\d+\n' : ''} - }x - - assert_empty err - assert_match expected, out - ensure - ENV["INPUTRC"] = inputrc_backup - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, ext_backup) - end - - def test_irb_info_singleline_without_rc_files - inputrc_backup = ENV["INPUTRC"] - ENV["INPUTRC"] = "unknown_inpurc" - ext_backup = IRB::IRBRC_EXT - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, "unknown_ext") - - out, err = execute_lines( - "irb_info", - conf: { USE_MULTILINE: false, USE_SINGLELINE: true } - ) - - expected = %r{ - Ruby\sversion:\s.+\n - IRB\sversion:\sirb\s.+\n - InputMethod:\sAbstract\sInputMethod\n - Completion: .+\n - RUBY_PLATFORM:\s.+\n - East\sAsian\sAmbiguous\sWidth:\s\d\n - #{@is_win ? 'Code\spage:\s\d+\n' : ''} - }x - - assert_empty err - assert_match expected, out - ensure - ENV["INPUTRC"] = inputrc_backup - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, ext_backup) - end - - def test_irb_info_lang - FileUtils.touch("#{@tmpdir}/.inputrc") - FileUtils.touch("#{@tmpdir}/.irbrc") - ENV["LANG"] = "ja_JP.UTF-8" - ENV["LC_ALL"] = "en_US.UTF-8" - - out, err = execute_lines( - "irb_info", - conf: { USE_MULTILINE: true, USE_SINGLELINE: false } - ) - - expected = %r{ - Ruby\sversion: .+\n - IRB\sversion:\sirb .+\n - InputMethod:\sAbstract\sInputMethod\n - Completion: .+\n - \.irbrc\spath: .+\n - RUBY_PLATFORM: .+\n - LANG\senv:\sja_JP\.UTF-8\n - LC_ALL\senv:\sen_US\.UTF-8\n - East\sAsian\sAmbiguous\sWidth:\s\d\n - }x - - assert_empty err - assert_match expected, out - end - end - - class MeasureTest < CommandTestCase - def test_measure - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: false - } - - c = Class.new(Object) - out, err = execute_lines( - "measure\n", - "3\n", - "measure :off\n", - "3\n", - "measure :on\n", - "3\n", - "measure :off\n", - "3\n", - conf: conf, - main: c - ) - - assert_empty err - assert_match(/\A(TIME is added\.\n=> nil\nprocessing time: .+\n=> 3\n=> nil\n=> 3\n){2}/, out) - assert_empty(c.class_variables) - end - - def test_measure_keeps_previous_value - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: false - } - - c = Class.new(Object) - out, err = execute_lines( - "measure\n", - "3\n", - "_\n", - conf: conf, - main: c - ) - - assert_empty err - assert_match(/\ATIME is added\.\n=> nil\nprocessing time: .+\n=> 3\nprocessing time: .+\n=> 3/, out) - assert_empty(c.class_variables) - end - - def test_measure_enabled_by_rc - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: true - } - - out, err = execute_lines( - "3\n", - "measure :off\n", - "3\n", - conf: conf, - ) - - assert_empty err - assert_match(/\Aprocessing time: .+\n=> 3\n=> nil\n=> 3\n/, out) - end - - def test_measure_enabled_by_rc_with_custom - measuring_proc = proc { |line, line_no, &block| - time = Time.now - result = block.() - puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] - result - } - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: true, - MEASURE_PROC: { CUSTOM: measuring_proc } - } - - out, err = execute_lines( - "3\n", - "measure :off\n", - "3\n", - conf: conf, - ) - assert_empty err - assert_match(/\Acustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) - end - - def test_measure_with_custom - measuring_proc = proc { |line, line_no, &block| - time = Time.now - result = block.() - puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] - result - } - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: false, - MEASURE_PROC: { CUSTOM: measuring_proc } - } - out, err = execute_lines( - "3\n", - "measure\n", - "3\n", - "measure :off\n", - "3\n", - conf: conf - ) - - assert_empty err - assert_match(/\A=> 3\nCUSTOM is added\.\n=> nil\ncustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) - end - - def test_measure_toggle - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: false, - MEASURE_PROC: { - FOO: proc { |&block| puts 'foo'; block.call }, - BAR: proc { |&block| puts 'bar'; block.call } - } - } - out, err = execute_lines( - "measure :foo", - "measure :on, :bar", - "3\n", - "measure :off, :foo\n", - "measure :off, :bar\n", - "3\n", - conf: conf - ) - - assert_empty err - assert_match(/\AFOO is added\.\n=> nil\nfoo\nBAR is added\.\n=> nil\nbar\nfoo\n=> 3\nbar\nfoo\n=> nil\nbar\n=> nil\n=> 3\n/, out) - end - - def test_measure_with_proc_warning - conf = { - PROMPT: { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ' - } - }, - PROMPT_MODE: :DEFAULT, - MEASURE: false, - } - c = Class.new(Object) - out, err = execute_lines( - "3\n", - "measure do\n", - "end\n", - "3\n", - conf: conf, - main: c - ) - - assert_match(/to add custom measure/, err) - assert_match(/\A=> 3\n=> nil\n=> 3\n/, out) - assert_empty(c.class_variables) - end - end - - class IrbSourceTest < CommandTestCase - def test_irb_source - File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") - out, err = execute_lines( - "a = 'bug17564'\n", - "a\n", - "irb_source '#{@tmpdir}/a.rb'\n", - "a\n", - ) - assert_empty err - assert_pattern_list([ - /=> "bug17564"\n/, - /=> "bug17564"\n/, - / => "hi"\n/, - / => nil\n/, - /=> "hi"\n/, - ], out) - end - - def test_irb_source_without_argument - out, err = execute_lines( - "irb_source\n", - ) - assert_empty err - assert_match(/Please specify the file name./, out) - end - end - - class IrbLoadTest < CommandTestCase - def test_irb_load - File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") - out, err = execute_lines( - "a = 'bug17564'\n", - "a\n", - "irb_load '#{@tmpdir}/a.rb'\n", - "a\n", - ) - assert_empty err - assert_pattern_list([ - /=> "bug17564"\n/, - /=> "bug17564"\n/, - / => "hi"\n/, - / => nil\n/, - /=> "bug17564"\n/, - ], out) - end - - def test_irb_load_without_argument - out, err = execute_lines( - "irb_load\n", - ) - - assert_empty err - assert_match(/Please specify the file name./, out) - end - end - - class WorkspaceCommandTestCase < CommandTestCase - def setup - super - # create Foo under the test class's namespace so it doesn't pollute global namespace - self.class.class_eval <<~RUBY - class Foo; end - RUBY - end - end - - class CwwsTest < WorkspaceCommandTestCase - def test_cwws_returns_the_current_workspace_object - out, err = execute_lines( - "cwws.class", - ) - - assert_empty err - assert_include(out, self.class.name) - end - end - - class PushwsTest < WorkspaceCommandTestCase - def test_pushws_switches_to_new_workspace_and_pushes_the_current_one_to_the_stack - out, err = execute_lines( - "pushws #{self.class}::Foo.new\n", - "cwws.class", - ) - assert_empty err - assert_include(out, "#{self.class}::Foo") - end - - def test_pushws_extends_the_new_workspace_with_command_bundle - out, err = execute_lines( - "pushws Object.new\n", - "self.singleton_class.ancestors" - ) - assert_empty err - assert_include(out, "IRB::ExtendCommandBundle") - end - - def test_pushws_prints_help_message_when_no_arg_is_given - out, err = execute_lines( - "pushws\n", - ) - assert_empty err - assert_match(/No other workspace/, out) - end - end - - class WorkspacesTest < WorkspaceCommandTestCase - def test_workspaces_returns_the_array_of_non_main_workspaces - out, err = execute_lines( - "pushws #{self.class}::Foo.new\n", - "workspaces.map { |w| w.class.name }", - ) - - assert_empty err - # self.class::Foo would be the current workspace - # self.class would be the old workspace that's pushed to the stack - assert_include(out, "=> [\"#{self.class}\"]") - end - - def test_workspaces_returns_empty_array_when_no_workspaces_were_added - out, err = execute_lines( - "workspaces.map(&:to_s)", - ) - - assert_empty err - assert_include(out, "=> []") - end - end - - class PopwsTest < WorkspaceCommandTestCase - def test_popws_replaces_the_current_workspace_with_the_previous_one - out, err = execute_lines( - "pushws Foo.new\n", - "popws\n", - "cwws.class", - ) - assert_empty err - assert_include(out, "=> #{self.class}") - end - - def test_popws_prints_help_message_if_the_workspace_is_empty - out, err = execute_lines( - "popws\n", - ) - assert_empty err - assert_match(/workspace stack empty/, out) - end - end - - class ChwsTest < WorkspaceCommandTestCase - def test_chws_replaces_the_current_workspace - out, err = execute_lines( - "chws #{self.class}::Foo.new\n", - "cwws.class", - ) - assert_empty err - assert_include(out, "=> #{self.class}::Foo") - end - - def test_chws_does_nothing_when_receiving_no_argument - out, err = execute_lines( - "chws\n", - "cwws.class", - ) - assert_empty err - assert_include(out, "=> #{self.class}") - end - end - - class WhereamiTest < CommandTestCase - def test_whereami - out, err = execute_lines( - "whereami\n", - ) - assert_empty err - assert_match(/^From: .+ @ line \d+ :\n/, out) - end - - def test_whereami_alias - out, err = execute_lines( - "@\n", - ) - assert_empty err - assert_match(/^From: .+ @ line \d+ :\n/, out) - end - end - - - class ShowCmdsTest < CommandTestCase - def test_help - out, err = execute_lines( - "help\n", - ) - - assert_empty err - assert_match(/List all available commands and their description/, out) - assert_match(/Start the debugger of debug\.gem/, out) - end - - def test_show_cmds - out, err = execute_lines( - "show_cmds\n" - ) - - assert_empty err - assert_match(/List all available commands and their description/, out) - assert_match(/Start the debugger of debug\.gem/, out) - end - - def test_show_cmds_list_user_aliases - out, err = execute_lines( - "show_cmds\n" - ) - - assert_empty err - assert_match(/\$\s+Alias for `show_source`/, out) - assert_match(/@\s+Alias for `whereami`/, out) - end - end - - class LsTest < CommandTestCase - def test_ls - out, err = execute_lines( - "class P\n", - " def m() end\n", - " def m2() end\n", - "end\n", - - "class C < P\n", - " def m1() end\n", - " def m2() end\n", - "end\n", - - "module M\n", - " def m1() end\n", - " def m3() end\n", - "end\n", - - "module M2\n", - " include M\n", - " def m4() end\n", - "end\n", - - "obj = C.new\n", - "obj.instance_variable_set(:@a, 1)\n", - "obj.extend M2\n", - "def obj.m5() end\n", - "ls obj\n", - ) - - assert_empty err - assert_match(/^instance variables:\s+@a\n/m, out) - assert_match(/P#methods:\s+m\n/m, out) - assert_match(/C#methods:\s+m2\n/m, out) - assert_match(/M#methods:\s+m1\s+m3\n/m, out) - assert_match(/M2#methods:\s+m4\n/m, out) - assert_match(/C.methods:\s+m5\n/m, out) - end - - def test_ls_class - out, err = execute_lines( - "module M1\n", - " def m2; end\n", - " def m3; end\n", - "end\n", - - "class C1\n", - " def m1; end\n", - " def m2; end\n", - "end\n", - - "class C2 < C1\n", - " include M1\n", - " def m3; end\n", - " def m4; end\n", - " def self.m3; end\n", - " def self.m5; end\n", - "end\n", - "ls C2" - ) - - assert_empty err - assert_match(/C2.methods:\s+m3\s+m5\n/, out) - assert_match(/C2#methods:\s+m3\s+m4\n.*M1#methods:\s+m2\n.*C1#methods:\s+m1\n/, out) - assert_not_match(/Module#methods/, out) - assert_not_match(/Class#methods/, out) - end - - def test_ls_module - out, err = execute_lines( - "module M1\n", - " def m1; end\n", - " def m2; end\n", - "end\n", - - "module M2\n", - " include M1\n", - " def m1; end\n", - " def m3; end\n", - " def self.m4; end\n", - "end\n", - "ls M2" - ) - - assert_empty err - assert_match(/M2\.methods:\s+m4\n/, out) - assert_match(/M2#methods:\s+m1\s+m3\n.*M1#methods:\s+m2\n/, out) - assert_not_match(/Module#methods/, out) - end - - def test_ls_instance - out, err = execute_lines( - "class Foo; def bar; end; end\n", - "ls Foo.new" - ) - - assert_empty err - assert_match(/Foo#methods:\s+bar/, out) - # don't duplicate - assert_not_match(/Foo#methods:\s+bar\n.*Foo#methods/, out) - end - - def test_ls_grep - out, err = execute_lines("ls 42\n") - assert_empty err - assert_match(/times/, out) - assert_match(/polar/, out) - - [ - "ls 42, grep: /times/\n", - "ls 42 -g times\n", - "ls 42 -G times\n", - ].each do |line| - out, err = execute_lines(line) - assert_empty err - assert_match(/times/, out) - assert_not_match(/polar/, out) - end - end - - def test_ls_grep_empty - out, err = execute_lines("ls\n") - assert_empty err - assert_match(/whereami/, out) - assert_match(/show_source/, out) - - [ - "ls grep: /whereami/\n", - "ls -g whereami\n", - "ls -G whereami\n", - ].each do |line| - out, err = execute_lines(line) - assert_empty err - assert_match(/whereami/, out) - assert_not_match(/show_source/, out) - end - end - - def test_ls_with_no_singleton_class - out, err = execute_lines( - "ls 42", - ) - assert_empty err - assert_match(/Comparable#methods:\s+/, out) - assert_match(/Numeric#methods:\s+/, out) - assert_match(/Integer#methods:\s+/, out) - end - end - - class ShowDocTest < CommandTestCase - def test_show_doc - out, err = execute_lines( - "show_doc String#gsub\n", - "\n", - ) - - # the former is what we'd get without document content installed, like on CI - # the latter is what we may get locally - possible_rdoc_output = [/Nothing known about String#gsub/, /gsub\(pattern\)/] - assert_not_include err, "[Deprecation]" - assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `show_doc` command to match one of the possible outputs. Got:\n#{out}") - ensure - # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } - end - - def test_show_doc_without_rdoc - out, err = without_rdoc do - execute_lines( - "show_doc String#gsub\n", - "\n", - ) - end - - # if it fails to require rdoc, it only returns the command object - assert_match(/=> nil\n/, out) - assert_include(err, "Can't display document because `rdoc` is not installed.\n") - ensure - # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } - end - end - - class EditTest < CommandTestCase - def setup - @original_visual = ENV["VISUAL"] - @original_editor = ENV["EDITOR"] - # noop the command so nothing gets executed - ENV["VISUAL"] = ": code" - ENV["EDITOR"] = ": code2" - end - - def teardown - ENV["VISUAL"] = @original_visual - ENV["EDITOR"] = @original_editor - end - - def test_edit_without_arg - out, err = execute_lines( - "edit", - irb_path: __FILE__ - ) - - assert_empty err - assert_match("path: #{__FILE__}", out) - assert_match("command: ': code'", out) - end - - def test_edit_without_arg_and_non_existing_irb_path - out, err = execute_lines( - "edit", - irb_path: '/path/to/file.rb(irb)' - ) - - assert_empty err - assert_match(/Can not find file: \/path\/to\/file\.rb\(irb\)/, out) - end - - def test_edit_with_path - out, err = execute_lines( - "edit #{__FILE__}" - ) - - assert_empty err - assert_match("path: #{__FILE__}", out) - assert_match("command: ': code'", out) - end - - def test_edit_with_non_existing_path - out, err = execute_lines( - "edit test_cmd_non_existing_path.rb" - ) - - assert_empty err - assert_match(/Can not find file: test_cmd_non_existing_path\.rb/, out) - end - - def test_edit_with_constant - out, err = execute_lines( - "edit IRB::Irb" - ) - - assert_empty err - assert_match(/path: .*\/lib\/irb\.rb/, out) - assert_match("command: ': code'", out) - end - - def test_edit_with_class_method - out, err = execute_lines( - "edit IRB.start" - ) - - assert_empty err - assert_match(/path: .*\/lib\/irb\.rb/, out) - assert_match("command: ': code'", out) - end - - def test_edit_with_instance_method - out, err = execute_lines( - "edit IRB::Irb#run" - ) - - assert_empty err - assert_match(/path: .*\/lib\/irb\.rb/, out) - assert_match("command: ': code'", out) - end - - def test_edit_with_editor_env_var - ENV.delete("VISUAL") - - out, err = execute_lines( - "edit", - irb_path: __FILE__ - ) - - assert_empty err - assert_match("path: #{__FILE__}", out) - assert_match("command: ': code2'", out) - end - end - - class HistoryCmdTest < CommandTestCase - def teardown - TestInputMethod.send(:remove_const, "HISTORY") if defined?(TestInputMethod::HISTORY) - super - end - - def test_history - TestInputMethod.const_set("HISTORY", %w[foo bar baz]) - - out, err = without_rdoc do - execute_lines("history") - end - - assert_include(out, <<~EOF) - 2: baz - 1: bar - 0: foo - EOF - assert_empty err - end - - def test_multiline_history_with_truncation - TestInputMethod.const_set("HISTORY", ["foo", "bar", <<~INPUT]) - [].each do |x| - puts x - end - INPUT - - out, err = without_rdoc do - execute_lines("hist") - end - - assert_include(out, <<~EOF) - 2: [].each do |x| - puts x - ... - 1: bar - 0: foo - EOF - assert_empty err - end - - def test_history_grep - TestInputMethod.const_set("HISTORY", ["foo", "bar", <<~INPUT]) - [].each do |x| - puts x - end - INPUT - - out, err = without_rdoc do - execute_lines("hist -g each\n") - end - - assert_include(out, <<~EOF) - 2: [].each do |x| - puts x - ... - EOF - assert_empty err - end - - end - -end diff --git a/test/irb/test_command.rb b/test/irb/test_command.rb new file mode 100644 index 0000000000..15ae6a658e --- /dev/null +++ b/test/irb/test_command.rb @@ -0,0 +1,980 @@ +# frozen_string_literal: false +require "rubygems" +require "irb" + +require_relative "helper" + +module TestIRB + class CommandTestCase < TestCase + def setup + @pwd = Dir.pwd + @tmpdir = File.join(Dir.tmpdir, "test_reline_config_#{$$}") + begin + Dir.mkdir(@tmpdir) + rescue Errno::EEXIST + FileUtils.rm_rf(@tmpdir) + Dir.mkdir(@tmpdir) + end + Dir.chdir(@tmpdir) + @home_backup = ENV["HOME"] + ENV["HOME"] = @tmpdir + @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") + save_encodings + IRB.instance_variable_get(:@CONF).clear + @is_win = (RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/) + end + + def teardown + ENV["XDG_CONFIG_HOME"] = @xdg_config_home_backup + ENV["HOME"] = @home_backup + Dir.chdir(@pwd) + FileUtils.rm_rf(@tmpdir) + restore_encodings + end + + def execute_lines(*lines, conf: {}, main: self, irb_path: nil) + capture_output do + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:USE_PAGER] = false + IRB.conf.merge!(conf) + input = TestInputMethod.new(lines) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + irb.context.irb_path = irb_path if irb_path + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.eval_input + end + end + end + + class FrozenObjectTest < CommandTestCase + def test_calling_command_on_a_frozen_main + main = Object.new.freeze + + out, err = execute_lines( + "irb_info", + main: main + ) + assert_empty(err) + assert_match(/RUBY_PLATFORM/, out) + end + end + + class InfoTest < CommandTestCase + def setup + super + @locals_backup = ENV.delete("LANG"), ENV.delete("LC_ALL") + end + + def teardown + super + ENV["LANG"], ENV["LC_ALL"] = @locals_backup + end + + def test_irb_info_multiline + FileUtils.touch("#{@tmpdir}/.inputrc") + FileUtils.touch("#{@tmpdir}/.irbrc") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: true, USE_SINGLELINE: false } + ) + + expected = %r{ + Ruby\sversion:\s.+\n + IRB\sversion:\sirb\s.+\n + InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n + \.irbrc\spath:\s.+\n + RUBY_PLATFORM:\s.+\n + East\sAsian\sAmbiguous\sWidth:\s\d\n + #{@is_win ? 'Code\spage:\s\d+\n' : ''} + }x + + assert_empty err + assert_match expected, out + end + + def test_irb_info_singleline + FileUtils.touch("#{@tmpdir}/.inputrc") + FileUtils.touch("#{@tmpdir}/.irbrc") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: false, USE_SINGLELINE: true } + ) + + expected = %r{ + Ruby\sversion:\s.+\n + IRB\sversion:\sirb\s.+\n + InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n + \.irbrc\spath:\s.+\n + RUBY_PLATFORM:\s.+\n + East\sAsian\sAmbiguous\sWidth:\s\d\n + #{@is_win ? 'Code\spage:\s\d+\n' : ''} + }x + + assert_empty err + assert_match expected, out + end + + def test_irb_info_multiline_without_rc_files + inputrc_backup = ENV["INPUTRC"] + ENV["INPUTRC"] = "unknown_inpurc" + ext_backup = IRB::IRBRC_EXT + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, "unknown_ext") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: true, USE_SINGLELINE: false } + ) + + expected = %r{ + Ruby\sversion:\s.+\n + IRB\sversion:\sirb\s.+\n + InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n + RUBY_PLATFORM:\s.+\n + East\sAsian\sAmbiguous\sWidth:\s\d\n + #{@is_win ? 'Code\spage:\s\d+\n' : ''} + }x + + assert_empty err + assert_match expected, out + ensure + ENV["INPUTRC"] = inputrc_backup + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, ext_backup) + end + + def test_irb_info_singleline_without_rc_files + inputrc_backup = ENV["INPUTRC"] + ENV["INPUTRC"] = "unknown_inpurc" + ext_backup = IRB::IRBRC_EXT + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, "unknown_ext") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: false, USE_SINGLELINE: true } + ) + + expected = %r{ + Ruby\sversion:\s.+\n + IRB\sversion:\sirb\s.+\n + InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n + RUBY_PLATFORM:\s.+\n + East\sAsian\sAmbiguous\sWidth:\s\d\n + #{@is_win ? 'Code\spage:\s\d+\n' : ''} + }x + + assert_empty err + assert_match expected, out + ensure + ENV["INPUTRC"] = inputrc_backup + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, ext_backup) + end + + def test_irb_info_lang + FileUtils.touch("#{@tmpdir}/.inputrc") + FileUtils.touch("#{@tmpdir}/.irbrc") + ENV["LANG"] = "ja_JP.UTF-8" + ENV["LC_ALL"] = "en_US.UTF-8" + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: true, USE_SINGLELINE: false } + ) + + expected = %r{ + Ruby\sversion: .+\n + IRB\sversion:\sirb .+\n + InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n + \.irbrc\spath: .+\n + RUBY_PLATFORM: .+\n + LANG\senv:\sja_JP\.UTF-8\n + LC_ALL\senv:\sen_US\.UTF-8\n + East\sAsian\sAmbiguous\sWidth:\s\d\n + }x + + assert_empty err + assert_match expected, out + end + end + + class MeasureTest < CommandTestCase + def test_measure + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false + } + + c = Class.new(Object) + out, err = execute_lines( + "measure\n", + "3\n", + "measure :off\n", + "3\n", + "measure :on\n", + "3\n", + "measure :off\n", + "3\n", + conf: conf, + main: c + ) + + assert_empty err + assert_match(/\A(TIME is added\.\n=> nil\nprocessing time: .+\n=> 3\n=> nil\n=> 3\n){2}/, out) + assert_empty(c.class_variables) + end + + def test_measure_keeps_previous_value + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false + } + + c = Class.new(Object) + out, err = execute_lines( + "measure\n", + "3\n", + "_\n", + conf: conf, + main: c + ) + + assert_empty err + assert_match(/\ATIME is added\.\n=> nil\nprocessing time: .+\n=> 3\nprocessing time: .+\n=> 3/, out) + assert_empty(c.class_variables) + end + + def test_measure_enabled_by_rc + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: true + } + + out, err = execute_lines( + "3\n", + "measure :off\n", + "3\n", + conf: conf, + ) + + assert_empty err + assert_match(/\Aprocessing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + end + + def test_measure_enabled_by_rc_with_custom + measuring_proc = proc { |line, line_no, &block| + time = Time.now + result = block.() + puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] + result + } + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: true, + MEASURE_PROC: { CUSTOM: measuring_proc } + } + + out, err = execute_lines( + "3\n", + "measure :off\n", + "3\n", + conf: conf, + ) + assert_empty err + assert_match(/\Acustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + end + + def test_measure_with_custom + measuring_proc = proc { |line, line_no, &block| + time = Time.now + result = block.() + puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] + result + } + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false, + MEASURE_PROC: { CUSTOM: measuring_proc } + } + out, err = execute_lines( + "3\n", + "measure\n", + "3\n", + "measure :off\n", + "3\n", + conf: conf + ) + + assert_empty err + assert_match(/\A=> 3\nCUSTOM is added\.\n=> nil\ncustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + end + + def test_measure_toggle + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false, + MEASURE_PROC: { + FOO: proc { |&block| puts 'foo'; block.call }, + BAR: proc { |&block| puts 'bar'; block.call } + } + } + out, err = execute_lines( + "measure :foo", + "measure :on, :bar", + "3\n", + "measure :off, :foo\n", + "measure :off, :bar\n", + "3\n", + conf: conf + ) + + assert_empty err + assert_match(/\AFOO is added\.\n=> nil\nfoo\nBAR is added\.\n=> nil\nbar\nfoo\n=> 3\nbar\nfoo\n=> nil\nbar\n=> nil\n=> 3\n/, out) + end + + def test_measure_with_proc_warning + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false, + } + c = Class.new(Object) + out, err = execute_lines( + "3\n", + "measure do\n", + "end\n", + "3\n", + conf: conf, + main: c + ) + + assert_match(/to add custom measure/, err) + assert_match(/\A=> 3\n=> nil\n=> 3\n/, out) + assert_empty(c.class_variables) + end + end + + class IrbSourceTest < CommandTestCase + def test_irb_source + File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") + out, err = execute_lines( + "a = 'bug17564'\n", + "a\n", + "irb_source '#{@tmpdir}/a.rb'\n", + "a\n", + ) + assert_empty err + assert_pattern_list([ + /=> "bug17564"\n/, + /=> "bug17564"\n/, + / => "hi"\n/, + / => nil\n/, + /=> "hi"\n/, + ], out) + end + + def test_irb_source_without_argument + out, err = execute_lines( + "irb_source\n", + ) + assert_empty err + assert_match(/Please specify the file name./, out) + end + end + + class IrbLoadTest < CommandTestCase + def test_irb_load + File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") + out, err = execute_lines( + "a = 'bug17564'\n", + "a\n", + "irb_load '#{@tmpdir}/a.rb'\n", + "a\n", + ) + assert_empty err + assert_pattern_list([ + /=> "bug17564"\n/, + /=> "bug17564"\n/, + / => "hi"\n/, + / => nil\n/, + /=> "bug17564"\n/, + ], out) + end + + def test_irb_load_without_argument + out, err = execute_lines( + "irb_load\n", + ) + + assert_empty err + assert_match(/Please specify the file name./, out) + end + end + + class WorkspaceCommandTestCase < CommandTestCase + def setup + super + # create Foo under the test class's namespace so it doesn't pollute global namespace + self.class.class_eval <<~RUBY + class Foo; end + RUBY + end + end + + class CwwsTest < WorkspaceCommandTestCase + def test_cwws_returns_the_current_workspace_object + out, err = execute_lines( + "cwws.class", + ) + + assert_empty err + assert_include(out, self.class.name) + end + end + + class PushwsTest < WorkspaceCommandTestCase + def test_pushws_switches_to_new_workspace_and_pushes_the_current_one_to_the_stack + out, err = execute_lines( + "pushws #{self.class}::Foo.new\n", + "cwws.class", + ) + assert_empty err + assert_include(out, "#{self.class}::Foo") + end + + def test_pushws_extends_the_new_workspace_with_command_bundle + out, err = execute_lines( + "pushws Object.new\n", + "self.singleton_class.ancestors" + ) + assert_empty err + assert_include(out, "IRB::ExtendCommandBundle") + end + + def test_pushws_prints_help_message_when_no_arg_is_given + out, err = execute_lines( + "pushws\n", + ) + assert_empty err + assert_match(/No other workspace/, out) + end + end + + class WorkspacesTest < WorkspaceCommandTestCase + def test_workspaces_returns_the_array_of_non_main_workspaces + out, err = execute_lines( + "pushws #{self.class}::Foo.new\n", + "workspaces.map { |w| w.class.name }", + ) + + assert_empty err + # self.class::Foo would be the current workspace + # self.class would be the old workspace that's pushed to the stack + assert_include(out, "=> [\"#{self.class}\"]") + end + + def test_workspaces_returns_empty_array_when_no_workspaces_were_added + out, err = execute_lines( + "workspaces.map(&:to_s)", + ) + + assert_empty err + assert_include(out, "=> []") + end + end + + class PopwsTest < WorkspaceCommandTestCase + def test_popws_replaces_the_current_workspace_with_the_previous_one + out, err = execute_lines( + "pushws Foo.new\n", + "popws\n", + "cwws.class", + ) + assert_empty err + assert_include(out, "=> #{self.class}") + end + + def test_popws_prints_help_message_if_the_workspace_is_empty + out, err = execute_lines( + "popws\n", + ) + assert_empty err + assert_match(/workspace stack empty/, out) + end + end + + class ChwsTest < WorkspaceCommandTestCase + def test_chws_replaces_the_current_workspace + out, err = execute_lines( + "chws #{self.class}::Foo.new\n", + "cwws.class", + ) + assert_empty err + assert_include(out, "=> #{self.class}::Foo") + end + + def test_chws_does_nothing_when_receiving_no_argument + out, err = execute_lines( + "chws\n", + "cwws.class", + ) + assert_empty err + assert_include(out, "=> #{self.class}") + end + end + + class WhereamiTest < CommandTestCase + def test_whereami + out, err = execute_lines( + "whereami\n", + ) + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end + + def test_whereami_alias + out, err = execute_lines( + "@\n", + ) + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end + end + + + class ShowCmdsTest < CommandTestCase + def test_help + out, err = execute_lines( + "help\n", + ) + + assert_empty err + assert_match(/List all available commands and their description/, out) + assert_match(/Start the debugger of debug\.gem/, out) + end + + def test_show_cmds + out, err = execute_lines( + "show_cmds\n" + ) + + assert_empty err + assert_match(/List all available commands and their description/, out) + assert_match(/Start the debugger of debug\.gem/, out) + end + + def test_show_cmds_list_user_aliases + out, err = execute_lines( + "show_cmds\n" + ) + + assert_empty err + assert_match(/\$\s+Alias for `show_source`/, out) + assert_match(/@\s+Alias for `whereami`/, out) + end + end + + class LsTest < CommandTestCase + def test_ls + out, err = execute_lines( + "class P\n", + " def m() end\n", + " def m2() end\n", + "end\n", + + "class C < P\n", + " def m1() end\n", + " def m2() end\n", + "end\n", + + "module M\n", + " def m1() end\n", + " def m3() end\n", + "end\n", + + "module M2\n", + " include M\n", + " def m4() end\n", + "end\n", + + "obj = C.new\n", + "obj.instance_variable_set(:@a, 1)\n", + "obj.extend M2\n", + "def obj.m5() end\n", + "ls obj\n", + ) + + assert_empty err + assert_match(/^instance variables:\s+@a\n/m, out) + assert_match(/P#methods:\s+m\n/m, out) + assert_match(/C#methods:\s+m2\n/m, out) + assert_match(/M#methods:\s+m1\s+m3\n/m, out) + assert_match(/M2#methods:\s+m4\n/m, out) + assert_match(/C.methods:\s+m5\n/m, out) + end + + def test_ls_class + out, err = execute_lines( + "module M1\n", + " def m2; end\n", + " def m3; end\n", + "end\n", + + "class C1\n", + " def m1; end\n", + " def m2; end\n", + "end\n", + + "class C2 < C1\n", + " include M1\n", + " def m3; end\n", + " def m4; end\n", + " def self.m3; end\n", + " def self.m5; end\n", + "end\n", + "ls C2" + ) + + assert_empty err + assert_match(/C2.methods:\s+m3\s+m5\n/, out) + assert_match(/C2#methods:\s+m3\s+m4\n.*M1#methods:\s+m2\n.*C1#methods:\s+m1\n/, out) + assert_not_match(/Module#methods/, out) + assert_not_match(/Class#methods/, out) + end + + def test_ls_module + out, err = execute_lines( + "module M1\n", + " def m1; end\n", + " def m2; end\n", + "end\n", + + "module M2\n", + " include M1\n", + " def m1; end\n", + " def m3; end\n", + " def self.m4; end\n", + "end\n", + "ls M2" + ) + + assert_empty err + assert_match(/M2\.methods:\s+m4\n/, out) + assert_match(/M2#methods:\s+m1\s+m3\n.*M1#methods:\s+m2\n/, out) + assert_not_match(/Module#methods/, out) + end + + def test_ls_instance + out, err = execute_lines( + "class Foo; def bar; end; end\n", + "ls Foo.new" + ) + + assert_empty err + assert_match(/Foo#methods:\s+bar/, out) + # don't duplicate + assert_not_match(/Foo#methods:\s+bar\n.*Foo#methods/, out) + end + + def test_ls_grep + out, err = execute_lines("ls 42\n") + assert_empty err + assert_match(/times/, out) + assert_match(/polar/, out) + + [ + "ls 42, grep: /times/\n", + "ls 42 -g times\n", + "ls 42 -G times\n", + ].each do |line| + out, err = execute_lines(line) + assert_empty err + assert_match(/times/, out) + assert_not_match(/polar/, out) + end + end + + def test_ls_grep_empty + out, err = execute_lines("ls\n") + assert_empty err + assert_match(/whereami/, out) + assert_match(/show_source/, out) + + [ + "ls grep: /whereami/\n", + "ls -g whereami\n", + "ls -G whereami\n", + ].each do |line| + out, err = execute_lines(line) + assert_empty err + assert_match(/whereami/, out) + assert_not_match(/show_source/, out) + end + end + + def test_ls_with_no_singleton_class + out, err = execute_lines( + "ls 42", + ) + assert_empty err + assert_match(/Comparable#methods:\s+/, out) + assert_match(/Numeric#methods:\s+/, out) + assert_match(/Integer#methods:\s+/, out) + end + end + + class ShowDocTest < CommandTestCase + def test_show_doc + out, err = execute_lines( + "show_doc String#gsub\n", + "\n", + ) + + # the former is what we'd get without document content installed, like on CI + # the latter is what we may get locally + possible_rdoc_output = [/Nothing known about String#gsub/, /gsub\(pattern\)/] + assert_not_include err, "[Deprecation]" + assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `show_doc` command to match one of the possible outputs. Got:\n#{out}") + ensure + # this is the only way to reset the redefined method without coupling the test with its implementation + EnvUtil.suppress_warning { load "irb/command/help.rb" } + end + + def test_show_doc_without_rdoc + out, err = without_rdoc do + execute_lines( + "show_doc String#gsub\n", + "\n", + ) + end + + # if it fails to require rdoc, it only returns the command object + assert_match(/=> nil\n/, out) + assert_include(err, "Can't display document because `rdoc` is not installed.\n") + ensure + # this is the only way to reset the redefined method without coupling the test with its implementation + EnvUtil.suppress_warning { load "irb/command/help.rb" } + end + end + + class EditTest < CommandTestCase + def setup + @original_visual = ENV["VISUAL"] + @original_editor = ENV["EDITOR"] + # noop the command so nothing gets executed + ENV["VISUAL"] = ": code" + ENV["EDITOR"] = ": code2" + end + + def teardown + ENV["VISUAL"] = @original_visual + ENV["EDITOR"] = @original_editor + end + + def test_edit_without_arg + out, err = execute_lines( + "edit", + irb_path: __FILE__ + ) + + assert_empty err + assert_match("path: #{__FILE__}", out) + assert_match("command: ': code'", out) + end + + def test_edit_without_arg_and_non_existing_irb_path + out, err = execute_lines( + "edit", + irb_path: '/path/to/file.rb(irb)' + ) + + assert_empty err + assert_match(/Can not find file: \/path\/to\/file\.rb\(irb\)/, out) + end + + def test_edit_with_path + out, err = execute_lines( + "edit #{__FILE__}" + ) + + assert_empty err + assert_match("path: #{__FILE__}", out) + assert_match("command: ': code'", out) + end + + def test_edit_with_non_existing_path + out, err = execute_lines( + "edit test_cmd_non_existing_path.rb" + ) + + assert_empty err + assert_match(/Can not find file: test_cmd_non_existing_path\.rb/, out) + end + + def test_edit_with_constant + out, err = execute_lines( + "edit IRB::Irb" + ) + + assert_empty err + assert_match(/path: .*\/lib\/irb\.rb/, out) + assert_match("command: ': code'", out) + end + + def test_edit_with_class_method + out, err = execute_lines( + "edit IRB.start" + ) + + assert_empty err + assert_match(/path: .*\/lib\/irb\.rb/, out) + assert_match("command: ': code'", out) + end + + def test_edit_with_instance_method + out, err = execute_lines( + "edit IRB::Irb#run" + ) + + assert_empty err + assert_match(/path: .*\/lib\/irb\.rb/, out) + assert_match("command: ': code'", out) + end + + def test_edit_with_editor_env_var + ENV.delete("VISUAL") + + out, err = execute_lines( + "edit", + irb_path: __FILE__ + ) + + assert_empty err + assert_match("path: #{__FILE__}", out) + assert_match("command: ': code2'", out) + end + end + + class HistoryCmdTest < CommandTestCase + def teardown + TestInputMethod.send(:remove_const, "HISTORY") if defined?(TestInputMethod::HISTORY) + super + end + + def test_history + TestInputMethod.const_set("HISTORY", %w[foo bar baz]) + + out, err = without_rdoc do + execute_lines("history") + end + + assert_include(out, <<~EOF) + 2: baz + 1: bar + 0: foo + EOF + assert_empty err + end + + def test_multiline_history_with_truncation + TestInputMethod.const_set("HISTORY", ["foo", "bar", <<~INPUT]) + [].each do |x| + puts x + end + INPUT + + out, err = without_rdoc do + execute_lines("hist") + end + + assert_include(out, <<~EOF) + 2: [].each do |x| + puts x + ... + 1: bar + 0: foo + EOF + assert_empty err + end + + def test_history_grep + TestInputMethod.const_set("HISTORY", ["foo", "bar", <<~INPUT]) + [].each do |x| + puts x + end + INPUT + + out, err = without_rdoc do + execute_lines("hist -g each\n") + end + + assert_include(out, <<~EOF) + 2: [].each do |x| + puts x + ... + EOF + assert_empty err + end + + end + +end -- cgit v1.2.3