Skip to content

Commit

Permalink
Add commands to start and use the debugger (#449)
Browse files Browse the repository at this point in the history
* Seamlessly integrate a few debug commands

* Improve the break command support

* Utilize skip_src option if available

* Add step and delete commands

* Write end-to-end tests for each debugger command

* Add documentation

* Add backtrace, info, catch commands
  • Loading branch information
k0kubun authored Nov 21, 2022
1 parent ae7de1e commit 976100c
Show file tree
Hide file tree
Showing 19 changed files with 487 additions and 37 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ group :development do
gem "stackprof" if is_unix && !is_truffleruby
gem "test-unit"
gem "reline", github: "ruby/reline" if ENV["WITH_LATEST_RELINE"] == "true"
gem "debug"
end
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ The following commands are available on IRB.
* Show the source code around binding.irb again.
* `debug`
* Start the debugger of debug.gem.
* `break`, `delete`, `next`, `step`, `continue`, `finish`, `backtrace`, `info`, `catch`
* Start the debugger of debug.gem and run the command on it.

## Documentation

Expand Down
6 changes: 2 additions & 4 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
# * Show the source code around binding.irb again.
# * debug
# * Start the debugger of debug.gem.
# * break, delete, next, step, continue, finish, backtrace, info, catch
# * Start the debugger of debug.gem and run the command on it.
#
# == Configuration
#
Expand Down Expand Up @@ -470,10 +472,6 @@ class Irb
def initialize(workspace = nil, input_method = nil)
@context = Context.new(self, workspace, input_method)
@context.main.extend ExtendCommandBundle
@context.command_aliases.each do |alias_name, cmd_name|
next if @context.symbol_alias(alias_name)
@context.main.install_alias_method(alias_name, cmd_name)
end
@signal_status = :IN_IRB
@scanner = RubyLex.new
end
Expand Down
21 changes: 21 additions & 0 deletions lib/irb/cmd/backtrace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Backtrace < Debug
def self.transform_args(args)
args&.dump
end

def execute(*args)
super(pre_cmds: ["backtrace", *args].join(" "))
end
end
end

# :startdoc:
end
21 changes: 21 additions & 0 deletions lib/irb/cmd/break.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Break < Debug
def self.transform_args(args)
args&.dump
end

def execute(args = nil)
super(pre_cmds: "break #{args}")
end
end
end

# :startdoc:
end
21 changes: 21 additions & 0 deletions lib/irb/cmd/catch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Catch < Debug
def self.transform_args(args)
args&.dump
end

def execute(*args)
super(pre_cmds: ["catch", *args].join(" "))
end
end
end

# :startdoc:
end
17 changes: 17 additions & 0 deletions lib/irb/cmd/continue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Continue < Debug
def execute(*args)
super(do_cmds: ["continue", *args].join(" "))
end
end
end

# :startdoc:
end
12 changes: 10 additions & 2 deletions lib/irb/cmd/debug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Debug < Nop
].map { |file| /\A#{Regexp.escape(file)}:\d+:in `irb'\z/ }
IRB_DIR = File.expand_path('..', __dir__)

def execute(*args)
def execute(pre_cmds: nil, do_cmds: nil)
unless binding_irb?
puts "`debug` command is only available when IRB is started with binding.irb"
return
Expand All @@ -25,11 +25,19 @@ def execute(*args)
return
end

options = { oneshot: true, hook_call: false }
if pre_cmds || do_cmds
options[:command] = ['irb', pre_cmds, do_cmds]
end
if DEBUGGER__::LineBreakpoint.instance_method(:initialize).parameters.include?([:key, :skip_src])
options[:skip_src] = true
end

# To make debugger commands like `next` or `continue` work without asking
# the user to quit IRB after that, we need to exit IRB first and then hit
# a TracePoint on #debug_break.
file, lineno = IRB::Irb.instance_method(:debug_break).source_location
DEBUGGER__::SESSION.add_line_breakpoint(file, lineno + 1, oneshot: true, hook_call: false)
DEBUGGER__::SESSION.add_line_breakpoint(file, lineno + 1, **options)
# exit current Irb#run call
throw :IRB_EXIT
end
Expand Down
17 changes: 17 additions & 0 deletions lib/irb/cmd/delete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Delete < Debug
def execute(*args)
super(pre_cmds: ["delete", *args].join(" "))
end
end
end

# :startdoc:
end
17 changes: 17 additions & 0 deletions lib/irb/cmd/finish.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Finish < Debug
def execute(*args)
super(do_cmds: ["finish", *args].join(" "))
end
end
end

# :startdoc:
end
31 changes: 9 additions & 22 deletions lib/irb/cmd/info.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,18 @@
# frozen_string_literal: false
# frozen_string_literal: true

require_relative "nop"
require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Info < Nop
def execute
Class.new {
def inspect
str = "Ruby version: #{RUBY_VERSION}\n"
str += "IRB version: #{IRB.version}\n"
str += "InputMethod: #{IRB.CurrentContext.io.inspect}\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
str
end
alias_method :to_s, :inspect
}.new
class Info < Debug
def self.transform_args(args)
args&.dump
end

def execute(*args)
super(pre_cmds: ["info", *args].join(" "))
end
end
end
Expand Down
34 changes: 34 additions & 0 deletions lib/irb/cmd/irb_info.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: false

require_relative "nop"

module IRB
# :stopdoc:

module ExtendCommand
class IrbInfo < Nop
def execute
Class.new {
def inspect
str = "Ruby version: #{RUBY_VERSION}\n"
str += "IRB version: #{IRB.version}\n"
str += "InputMethod: #{IRB.CurrentContext.io.inspect}\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
str
end
alias_method :to_s, :inspect
}.new
end
end
end

# :startdoc:
end
17 changes: 17 additions & 0 deletions lib/irb/cmd/next.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Next < Debug
def execute(*args)
super(do_cmds: ["next", *args].join(" "))
end
end
end

# :startdoc:
end
18 changes: 18 additions & 0 deletions lib/irb/cmd/step.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require_relative "debug"

module IRB
# :stopdoc:

module ExtendCommand
class Step < Debug
def execute(*args)
# Run `next` first to move out of binding.irb
super(pre_cmds: "next", do_cmds: ["step", *args].join(" "))
end
end
end

# :startdoc:
end
16 changes: 11 additions & 5 deletions lib/irb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,9 @@ def evaluate(line, line_no, exception: nil) # :nodoc:
@workspace.local_variable_set(:_, exception)
end

# Transform a non-identifier alias (ex: @, $)
# Transform a non-identifier alias (@, $) or keywords (next, break)
command, args = line.split(/\s/, 2)
if original = symbol_alias(command)
if original = command_aliases[command.to_sym]
line = line.gsub(/\A#{Regexp.escape(command)}/, original.to_s)
command = original
end
Expand Down Expand Up @@ -545,10 +545,16 @@ def local_variables # :nodoc:
workspace.binding.local_variables
end

# Return a command name if it's aliased from the argument and it's not an identifier.
def symbol_alias(command)
# Return true if it's aliased from the argument and it's not an identifier.
def symbol_alias?(command)
return nil if command.match?(/\A\w+\z/)
command_aliases[command.to_sym]
command_aliases.key?(command.to_sym)
end

# Return true if the command supports transforming args
def transform_args?(command)
command = command_aliases.fetch(command.to_sym, command)
ExtendCommandBundle.load_command(command)&.respond_to?(:transform_args)
end
end
end
37 changes: 36 additions & 1 deletion lib/irb/extend-command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,48 @@ def irb_context
: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_info, :Info, "cmd/info"
:irb_info, :IrbInfo, "cmd/irb_info"
],

[
Expand Down
5 changes: 5 additions & 0 deletions lib/irb/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,13 @@ def IRB.init_config(ap_path)
@CONF[:AT_EXIT] = []

@CONF[:COMMAND_ALIASES] = {
# Symbol aliases
:'$' => :show_source,
:'@' => :whereami,
# Keyword aliases
:break => :irb_break,
:catch => :irb_catch,
:next => :irb_next,
}
end

Expand Down
Loading

0 comments on commit 976100c

Please sign in to comment.