From a955ee296545634c346b5b4c71df6e46552cf5a1 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 7 Dec 2023 14:32:57 +0000 Subject: [PATCH] Improve command input recognition Currently, we simply split the input on whitespace and use the first word as the command name to check if it's a command. But this means that assigning a local variable which's name is the same as a command will also be recognized as a command. For example, in the following case, `info` is recognized as a command: ``` irb(main):001> info = 123 `debug` command is only available when IRB is started with binding.irb => nil ``` This commit improves the command input recognition by using more sophis- ticated regular expressions. --- lib/irb.rb | 33 +++++++++++++++---- lib/irb/cmd/ls.rb | 2 +- ...debug_cmd.rb => test_debug_integration.rb} | 19 ++++++++++- 3 files changed, 45 insertions(+), 9 deletions(-) rename test/irb/{test_debug_cmd.rb => test_debug_integration.rb} (94%) diff --git a/lib/irb.rb b/lib/irb.rb index 1ba335c08..6e572c0f9 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -595,13 +595,30 @@ def each_top_level_statement end end + SIMPLE_COMMAND_REGEXP = /^(?\S+)$/ + COMMAND_WITH_ARGS_REGEXP = /^(?\S+) +(?[^-]\S*)$/ + COMMAND_WITH_FLAGS_REGEXP = /^(?\S+) +(?-[a-zA-Z]+( +\S+)*)$/ + COMMAND_WITH_ARGS_AND_FLAGS_REGEXP = /^(?\S+) +(?[^-]\S*) +(?-[a-zA-Z]+( +\S+)*)$/ + + COMMAND_REGEXP = Regexp.union( + SIMPLE_COMMAND_REGEXP, + COMMAND_WITH_ARGS_REGEXP, + COMMAND_WITH_FLAGS_REGEXP, + COMMAND_WITH_ARGS_AND_FLAGS_REGEXP + ) + def build_statement(code) - code.force_encoding(@context.io.encoding) - command_or_alias, arg = code.split(/\s/, 2) - # Transform a non-identifier alias (@, $) or keywords (next, break) - command_name = @context.command_aliases[command_or_alias.to_sym] - command = command_name || command_or_alias - command_class = ExtendCommandBundle.load_command(command) + code.force_encoding(@context.io.encoding) unless code.frozen? + command_match = COMMAND_REGEXP.match(code.strip) + + if command_match + command_or_alias = command_match[:cmd_name] + arg = [command_match[:cmd_arg], command_match[:cmd_flag]].compact.join(' ') + # 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) + end if command_class Statement::Command.new(code, command, arg, command_class) @@ -612,7 +629,9 @@ def build_statement(code) end def single_line_command?(code) - command = code.split(/\s/, 2).first + command_match = COMMAND_REGEXP.match(code) + return unless command_match + command = command_match[:cmd_name] @context.symbol_alias?(command) || @context.transform_args?(command) end diff --git a/lib/irb/cmd/ls.rb b/lib/irb/cmd/ls.rb index 791b1c1b2..648afddf1 100644 --- a/lib/irb/cmd/ls.rb +++ b/lib/irb/cmd/ls.rb @@ -15,7 +15,7 @@ class Ls < Nop 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/) + if match = args&.match(/\A(?.+\s|)(-g|-G)\s+(?[^\s]+)\s*\z/) args = match[:args] "#{args}#{',' unless args.chomp.empty?} grep: /#{match[:grep]}/" else diff --git a/test/irb/test_debug_cmd.rb b/test/irb/test_debug_integration.rb similarity index 94% rename from test/irb/test_debug_cmd.rb rename to test/irb/test_debug_integration.rb index 0fb45af47..f7e770170 100644 --- a/test/irb/test_debug_cmd.rb +++ b/test/irb/test_debug_integration.rb @@ -6,7 +6,7 @@ require_relative "helper" module TestIRB - class DebugCommandTest < IntegrationTestCase + class DebugIntegrationTest < IntegrationTestCase def setup super @@ -434,5 +434,22 @@ def test_multi_irb_commands_are_not_available_after_activating_the_debugger assert_match(/irb\(main\):001> next/, output) assert_include(output, "Multi-IRB commands are not available when the debugger is enabled.") end + + def test_locals_assignment_not_being_treated_as_debugging_command + write_ruby <<~'ruby' + binding.irb + ruby + + output = run_ruby_file do + type "info = 4" + type "info + 1" + type "quit" + end + + assert_match(/=> 5/, output) + # Since neither `info = ` nor `info + ` are debugging commands, debugger should not be activated in this + # session. + assert_not_match(/irb:rdbg/, output) + end end end