Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move away from method-based command implementation #824

Merged
merged 13 commits into from
Apr 10, 2024
58 changes: 37 additions & 21 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ class Irb
# Creates a new irb session
def initialize(workspace = nil, input_method = nil)
@context = Context.new(self, workspace, input_method)
@context.workspace.load_commands_to_main
@context.workspace.load_helper_methods_to_main
@signal_status = :IN_IRB
@scanner = RubyLex.new
@line_no = 1
Expand All @@ -950,7 +950,7 @@ def debug_break
def debug_readline(binding)
workspace = IRB::WorkSpace.new(binding)
context.replace_workspace(workspace)
context.workspace.load_commands_to_main
context.workspace.load_helper_methods_to_main
@line_no += 1

# When users run:
Expand Down Expand Up @@ -1028,7 +1028,15 @@ def eval_input
return statement.code
end

@context.evaluate(statement.evaluable_code, line_no)
case statement
when Statement::EmptyInput
# Do nothing
when Statement::Expression
@context.evaluate(statement.code, line_no)
when Statement::Command
ret = statement.command_class.execute(@context, statement.arg)
@context.set_last_value(ret)
end

if @context.echo? && !statement.suppresses_echo?
if statement.is_assignment?
Expand Down Expand Up @@ -1084,10 +1092,7 @@ def readmultiline
end

code << line

# Accept any single-line input for symbol aliases or commands that transform
# args
return code if single_line_command?(code)
return code if command?(code)

tokens, opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
return code if terminated
Expand All @@ -1114,23 +1119,36 @@ def build_statement(code)
end

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

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

def single_line_command?(code)
command = code.split(/\s/, 2).first
@context.symbol_alias?(command) || @context.transform_args?(command)
def parse_command(code)
command_name, arg = code.strip.split(/\s+/, 2)
return unless code.lines.size == 1 && command_name

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

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

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

def configure_io
Expand All @@ -1148,9 +1166,7 @@ def configure_io
false
end
else
# Accept any single-line input for symbol aliases or commands that transform
# args
next true if single_line_command?(code)
next true if command?(code)

_tokens, _opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
terminated
Expand Down
118 changes: 31 additions & 87 deletions lib/irb/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,17 @@ module Command; end

# Installs the default irb extensions command bundle.
module ExtendCommandBundle
EXCB = ExtendCommandBundle # :nodoc:

# See #install_alias_method.
# See ExtendCommandBundle.execute_as_command?.
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_context, :Context, "command/context",
[:context, NO_OVERRIDE],
[:conf, NO_OVERRIDE],
],
[
:irb_exit, :Exit, "command/exit",
[:exit, OVERRIDE_PRIVATE_ONLY],
Expand Down Expand Up @@ -204,6 +192,26 @@ def irb_context
],
]

def self.command_override_policies
@@command_override_policies ||= @EXTEND_COMMANDS.flat_map do |cmd_name, cmd_class, load_file, *aliases|
[[cmd_name, OVERRIDE_ALL]] + aliases
end.to_h
end

def self.execute_as_command?(name, public_method:, private_method:)
case command_override_policies[name]
when OVERRIDE_ALL
true
when OVERRIDE_PRIVATE_ONLY
!public_method
when NO_OVERRIDE
!public_method && !private_method
end
end

def self.command_names
command_override_policies.keys.map(&:to_s)
end

@@commands = []

Expand Down Expand Up @@ -247,77 +255,13 @@ def self.load_command(command)
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)
@EXTEND_COMMANDS.delete_if { |name,| name == cmd_name }
@EXTEND_COMMANDS << [cmd_name, cmd_class, load_file, *aliases]

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
# Just clear memoized values
@@commands = []
@@command_override_policies = nil
end

install_extend_commands
end
end
8 changes: 2 additions & 6 deletions lib/irb/command/backtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ module IRB

module Command
class Backtrace < DebugCommand
def self.transform_args(args)
args&.dump
end

def execute(*args)
super(pre_cmds: ["backtrace", *args].join(" "))
def execute(arg)
execute_debug_command(pre_cmds: "backtrace #{arg}".rstrip)
end
end
end
Expand Down
35 changes: 26 additions & 9 deletions lib/irb/command/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ module IRB
module Command
class CommandArgumentError < StandardError; end

def self.extract_ruby_args(*args, **kwargs)
throw :EXTRACT_RUBY_ARGS, [args, kwargs]
end

class Base
class << self
def category(category = nil)
Expand All @@ -29,19 +33,13 @@ def help_message(help_message = nil)

private

def string_literal?(args)
sexp = Ripper.sexp(args)
sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal
end

def highlight(text)
Color.colorize(text, [:BOLD, :BLUE])
end
end

def self.execute(irb_context, *opts, **kwargs, &block)
command = new(irb_context)
command.execute(*opts, **kwargs, &block)
def self.execute(irb_context, arg)
new(irb_context).execute(arg)
st0012 marked this conversation as resolved.
Show resolved Hide resolved
rescue CommandArgumentError => e
puts e.message
end
Expand All @@ -52,7 +50,26 @@ def initialize(irb_context)

attr_reader :irb_context

def execute(*opts)
def unwrap_string_literal(str)
return if str.empty?

sexp = Ripper.sexp(str)
if sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal
@irb_context.workspace.binding.eval(str).to_s
else
str
end
end

def ruby_args(arg)
# Use throw and catch to handle arg that includes `;`
# For example: "1, kw: (2; 3); 4" will be parsed to [[1], { kw: 3 }]
catch(:EXTRACT_RUBY_ARGS) do
@irb_context.workspace.binding.eval "IRB::Command.extract_ruby_args #{arg}"
end || [[], {}]
end

def execute(arg)
#nop
end
end
Expand Down
8 changes: 2 additions & 6 deletions lib/irb/command/break.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ module IRB

module Command
class Break < DebugCommand
def self.transform_args(args)
args&.dump
end

def execute(args = nil)
super(pre_cmds: "break #{args}")
def execute(arg)
execute_debug_command(pre_cmds: "break #{arg}".rstrip)
end
end
end
Expand Down
8 changes: 2 additions & 6 deletions lib/irb/command/catch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ module IRB

module Command
class Catch < DebugCommand
def self.transform_args(args)
args&.dump
end

def execute(*args)
super(pre_cmds: ["catch", *args].join(" "))
def execute(arg)
execute_debug_command(pre_cmds: "catch #{arg}".rstrip)
end
end
end
Expand Down
11 changes: 8 additions & 3 deletions lib/irb/command/chws.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CurrentWorkingWorkspace < Base
category "Workspace"
description "Show the current workspace."

def execute(*obj)
def execute(_arg)
irb_context.main
end
end
Expand All @@ -23,8 +23,13 @@ class ChangeWorkspace < Base
category "Workspace"
description "Change the current workspace to an object."

def execute(*obj)
irb_context.change_workspace(*obj)
def execute(arg)
if arg.empty?
irb_context.change_workspace
else
obj = eval(arg, irb_context.workspace.binding)
irb_context.change_workspace(obj)
end
irb_context.main
end
end
Expand Down
16 changes: 16 additions & 0 deletions lib/irb/command/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module IRB
module Command
class Context < Base
category "IRB"
description "Displays current configuration."

def execute(_arg)
# This command just displays the configuration.
# Modifying the configuration is achieved by sending a message to IRB.conf.
Pager.page_content(IRB.CurrentContext.inspect)
end
end
end
end
Loading
Loading