1
0
Fork 0
mirror of https://github.com/pry/pry.git synced 2022-11-09 12:35:05 -05:00

Split CommandProcessor responsibility between CommandSet and Command

The CommandSet is responsible for storing Commands, while the Commands
themselves are responsible for all the parsing.

(Before this change, the CommandProcessor used to do both the searching
within the CommandSet's and also the tokenization of command arguments)
This commit is contained in:
Conrad Irwin 2012-01-03 00:04:47 +00:00
parent 831540b84e
commit 45c6492e7e
8 changed files with 383 additions and 361 deletions

View file

@ -52,16 +52,40 @@ class Pry
klass
end
# Should this command be called for the given line?
#
# @param String a line input at the REPL
# @return Boolean
def matches?(val)
command_regex =~ val
end
# Store hooks to be run before or after the command body.
# @see {Pry::CommandSet#before_command}
# @see {Pry::CommandSet#after_command}
def hooks
@hooks ||= {:before => [], :after => []}
end
def command_regex
prefix = convert_to_regex(Pry.config.command_prefix)
prefix = "(?:#{prefix})?" unless options[:use_prefix]
/^#{prefix}#{convert_to_regex(name)}(?!\S)/
end
def convert_to_regex(obj)
case obj
when String
Regexp.escape(obj)
else
obj
end
end
end
# Properties of one execution of a command (passed by the {Pry::CommandProcessor} as a hash of
# Properties of one execution of a command (passed by {Pry#run_command} as a hash of
# context and expanded in {#initialize}
attr_accessor :output
attr_accessor :target
@ -70,7 +94,6 @@ class Pry
attr_accessor :arg_string
attr_accessor :context
attr_accessor :command_set
attr_accessor :command_processor
attr_accessor :_pry_
# Run a command from another command.
@ -84,7 +107,7 @@ class Pry
# run "amend-line", "5", 'puts "hello world"'
def run(command_string, *args)
complete_string = "#{command_string} #{args.join(" ")}"
command_processor.process_commands(complete_string, eval_string, target)
command_set.process_line(complete_string, context)
end
def commands
@ -110,17 +133,87 @@ class Pry
self.context = context
self.target = context[:target]
self.output = context[:output]
self.captures = context[:captures]
self.eval_string = context[:eval_string]
self.arg_string = context[:arg_string]
self.command_set = context[:command_set]
self._pry_ = context[:pry_instance]
self.command_processor = context[:command_processor]
end
# The value of {self} inside the {target} binding.
def target_self; target.eval('self'); end
# Revaluate the string (str) and perform interpolation.
# @param [String] str The string to reevaluate with interpolation.
#
# @return [String] The reevaluated string with interpolations
# applied (if any).
def interpolate_string(str)
dumped_str = str.dump
if dumped_str.gsub!(/\\\#\{/, '#{')
target.eval(dumped_str)
else
str
end
end
# Display a warning if a command collides with a local/method in
# the current scope.
# @param [String] command_name_match The name of the colliding command.
# @param [Binding] target The current binding context.
def check_for_command_name_collision(command_name_match)
if collision_type = target.eval("defined?(#{command_name_match})")
output.puts "#{Pry::Helpers::Text.bold('WARNING:')} Command name collision with a #{collision_type}: '#{command_name_match}'\n\n"
end
rescue Pry::RescuableException
end
# Extract necessary information from a line that Command.matches? this command.
#
# @param String the line of input
# @return [
# String the command name used, or portion of line that matched the command_regex
# String a string of all the arguments (i.e. everything but the name)
# Array the captures caught by the command_regex
# Array args the arguments got by splitting the arg_string
# ]
def tokenize(val)
val.replace(interpolate_string(val)) if command_options[:interpolate]
self.class.command_regex =~ val
# please call Command.matches? before Command#call_safely
raise CommandError, "fatal: called a command which didn't match?!" unless Regexp.last_match
captures = Regexp.last_match.captures
pos = Regexp.last_match.end(0)
arg_string = val[pos..-1]
# remove the one leading space if it exists
arg_string.slice!(0) if arg_string.start_with?(" ")
if arg_string
args = command_options[:shellwords] ? Shellwords.shellwords(arg_string) : arg_string.split(" ")
else
args = []
end
[val[0..pos].rstrip, arg_string, captures, args]
end
# Process a line that Command.matches? this command.
#
# @param String the line to process
# @return Object or Command::VOID_VALUE
def process_line(line)
command_name, arg_string, captures, args = tokenize(line)
check_for_command_name_collision(command_name) if Pry.config.collision_warning
self.arg_string = arg_string
self.captures = captures
call_safely(*(captures + args))
end
# Run the command with the given {args}.
#
# This is a public wrapper around {#call} which ensures all preconditions are met.

View file

@ -1,166 +0,0 @@
require 'forwardable'
class Pry
class CommandProcessor
# Wraps the return result of process_commands, indicates if the
# result IS a command and what kind of command (e.g void)
class Result
attr_reader :retval
def initialize(is_command, keep_retval = false, retval = nil)
@is_command, @keep_retval, @retval = is_command, keep_retval, retval
end
# Is the result a command?
# @return [Boolean]
def command?
@is_command
end
# Is the result a command and if it is, is it a void command?
# (one that does not return a value)
# @return [Boolean]
def void_command?
(command? && !keep_retval?) || retval == Command::VOID_VALUE
end
# Is the return value kept for this command? (i.e :keep_retval => true)
# @return [Boolean]
def keep_retval?
@keep_retval
end
end
extend Forwardable
attr_accessor :pry_instance
def initialize(pry_instance)
@pry_instance = pry_instance
end
def_delegators :@pry_instance, :commands, :output
# Is the string a valid command?
# @param [String] val The string passed in from the Pry prompt.
# @param [Binding] target The context where the string should be
# interpolated in.
# @return [Boolean] Whether the string is a valid command.
def valid_command?(val, target=binding)
!!(command_matched(val, target)[0])
end
# Convert the object to a form that can be interpolated into a
# Regexp cleanly.
# @return [String] The string to interpolate into a Regexp
def convert_to_regex(obj)
case obj
when String
Regexp.escape(obj)
else
obj
end
end
# Revaluate the string (str) and perform interpolation.
# @param [String] str The string to reevaluate with interpolation.
# @param [Binding] target The context where the string should be
# interpolated in.
# @return [String] The reevaluated string with interpolations
# applied (if any).
def interpolate_string(str, target)
dumped_str = str.dump
dumped_str.gsub!(/\\\#\{/, '#{')
target.eval(dumped_str)
end
# Determine whether a Pry command was matched and return command data
# and argument string.
# This method should not need to be invoked directly.
# @param [String] val The line of input.
# @param [Binding] target The binding to perform string
# interpolation against.
# @return [Array] The command data and arg string pair
def command_matched(val, target)
_, cmd_data = commands.commands.find do |name, data|
prefix = convert_to_regex(Pry.config.command_prefix)
prefix = "(?:#{prefix})?" unless data.options[:use_prefix]
command_regex = /^#{prefix}#{convert_to_regex(name)}(?!\S)/
if command_regex =~ val
if data.options[:interpolate]
val.replace(interpolate_string(val, target))
command_regex =~ val # re-match with the interpolated string
end
true
end
end
[cmd_data, (Regexp.last_match ? Regexp.last_match.captures : nil), (Regexp.last_match ? Regexp.last_match.end(0) : nil)]
end
# Display a warning if a command collides with a local/method in
# the current scope.
# @param [String] command_name_match The name of the colliding command.
# @param [Binding] target The current binding context.
def check_for_command_name_collision(command_name_match, target)
if collision_type = target.eval("defined?(#{command_name_match})")
pry_instance.output.puts "#{Pry::Helpers::Text.bold('WARNING:')} Command name collision with a #{collision_type}: '#{command_name_match}'\n\n"
end
rescue Pry::RescuableException
end
# Process Pry commands. Pry commands are not Ruby methods and are evaluated
# prior to Ruby expressions.
# Commands can be modified/configured by the user: see `Pry::Commands`
# This method should not need to be invoked directly - it is called
# by `Pry#r`.
# @param [String] val The current line of input.
# @param [String] eval_string The cumulative lines of input for
# multi-line input.
# @param [Binding] target The receiver of the commands.
# @return [Pry::CommandProcessor::Result] A wrapper object
# containing info about the result of the command processing
# (indicating whether it is a command and if it is what kind of
# command it is.
def process_commands(val, eval_string, target)
command, captures, pos = command_matched(val, target)
# no command was matched, so return to caller
return Result.new(false) if !command
arg_string = val[pos..-1]
check_for_command_name_collision(val[0..pos].rstrip, target) if Pry.config.collision_warning
# remove the one leading space if it exists
arg_string.slice!(0) if arg_string.start_with?(" ")
if arg_string
args = command.options[:shellwords] ? Shellwords.shellwords(arg_string) : arg_string.split(" ")
else
args = []
end
context = {
:val => val,
:arg_string => arg_string,
:eval_string => eval_string,
:commands => commands.commands,
:captures => captures,
:pry_instance => @pry_instance,
:output => output,
:command_processor => self,
:command_set => commands,
:target => target
}
ret = command.new(context).call_safely(*(captures + args))
Result.new(true, command.options[:keep_retval], ret)
end
end
end

View file

@ -273,6 +273,38 @@ class Pry
commands.keys
end
# Find a command that matches the given line
#
# @param [String] the line that may be a command invocation
# @return [Pry::Command, nil]
def find_command(val)
commands.values.detect{ |c| c.matches?(val) }
end
# Is the given line a command invocation?
#
# @param [String]
# @return [Boolean]
def valid_command?(val)
!!find_command(val)
end
# Process the given line to see whether it needs executing as a command.
#
# @param String the line to execute
# @param Hash the context to execute the commands with
# @return CommandSet::Result
#
def process_line(val, context={})
if command = find_command(val)
context = context.merge(:command_set => self)
retval = command.new(context).process_line(val)
Result.new(true, retval)
else
Result.new(false)
end
end
# @nodoc used for testing
def run_command(context, name, *args)
command = commands[name] or raise NoCommandError.new(name, self)
@ -358,4 +390,27 @@ class Pry
end
end
end
# Wraps the return result of process_commands, indicates if the
# result IS a command and what kind of command (e.g void)
class Result
attr_reader :retval
def initialize(is_command, retval = nil)
@is_command, @retval = is_command, retval
end
# Is the result a command?
# @return [Boolean]
def command?
@is_command
end
# Is the result a command and if it is, is it a void command?
# (one that does not return a value)
# @return [Boolean]
def void_command?
retval == Command::VOID_VALUE
end
end
end

View file

@ -214,7 +214,7 @@ class Pry
if opts.present?(:exclude)
history.map!.with_index do |element, index|
unless command_processor.valid_command? element
unless command_set.valid_command? element
if opts.present?(:'no-numbers')
element
else
@ -262,7 +262,7 @@ class Pry
output.puts "Saving history in #{file_name} ..."
# exclude pry commands
hist_array.reject! do |element|
command_processor.valid_command?(element)
command_set.valid_command?(element)
end
File.open(file_name, 'w') do |f|

View file

@ -1,4 +1,3 @@
require "pry/command_processor.rb"
require "pry/indent"
class Pry
@ -37,7 +36,6 @@ class Pry
def initialize(options={})
refresh(options)
@command_processor = CommandProcessor.new(self)
@binding_stack = []
@indent = Pry::Indent.new
end
@ -353,7 +351,12 @@ class Pry
# @param [Binding] target The target of the Pry session.
# @return [Boolean] `true` if `val` is a command, `false` otherwise
def process_command(val, eval_string, target)
result = @command_processor.process_commands(val, eval_string, target)
result = commands.process_line(val, {
:target => target,
:output => output,
:eval_string => eval_string,
:pry_instance => self
})
# set a temporary (just so we can inject the value we want into eval_string)
Thread.current[:__pry_cmd_result__] = result
@ -382,7 +385,12 @@ class Pry
# @example
# pry_instance.run_command("ls -m")
def run_command(val, eval_string = "", target = binding_stack.last)
@command_processor.process_commands(val, eval_string, target)
commands.process_line(val,
:eval_string => eval_string,
:target => target,
:pry_instance => self,
:output => output
)
Pry::Command::VOID_VALUE
end

View file

@ -142,12 +142,9 @@ describe "Pry::Command" do
context = {
:target => binding,
:output => StringIO.new,
:captures => [],
:eval_string => "eval-string",
:arg_string => "arg-string",
:command_set => @set,
:pry_instance => Object.new,
:command_processor => Object.new
:pry_instance => Object.new
}
it 'should capture lots of stuff from the hash passed to new before setup' do
@ -160,12 +157,9 @@ describe "Pry::Command" do
end
define_method(:process) do
captures.should.equal?(context[:captures])
eval_string.should == "eval-string"
arg_string.should == "arg-string"
command_set.should == @set
_pry_.should == context[:pry_instance]
command_processor.should == context[:command_processor]
end
end
@ -230,4 +224,94 @@ describe "Pry::Command" do
mock_command(cmd, %w(--four 4 four))
end
end
describe 'tokenize' do
it 'should interpolate string with #{} in them' do
cmd = @set.command 'random-dent' do |*args|
args.should == ["3", "8"]
end
foo = 5
cmd.new(:target => binding).process_line 'random-dent #{1 + 2} #{3 + foo}'
end
it 'should not fail if interpolation is not needed and target is not set' do
cmd = @set.command 'the-book' do |*args|
args.should == ['--help']
end
cmd.new.process_line 'the-book --help'
end
it 'should not interpolate commands with :interpolate => false' do
cmd = @set.command 'thor', 'norse god', :interpolate => false do |*args|
args.should == ['%(#{foo})']
end
cmd.new.process_line 'thor %(#{foo})'
end
it 'should use shell-words to split strings' do
cmd = @set.command 'eccentrica' do |*args|
args.should == ['gallumbits', 'eroticon', '6']
end
cmd.new.process_line %(eccentrica "gallumbits" 'erot''icon' 6)
end
it 'should split on spaces if shellwords is not used' do
cmd = @set.command 'bugblatter-beast', 'would eat its grandmother', :shellwords => false do |*args|
args.should == ['"of', 'traal"']
end
cmd.new.process_line %(bugblatter-beast "of traal")
end
it 'should add captures to arguments for regex commands' do
cmd = @set.command /perfectly (normal)( beast)?/i do |*args|
args.should == ['Normal', ' Beast', '(honest!)']
end
cmd.new.process_line %(Perfectly Normal Beast (honest!))
end
end
describe 'process_line' do
it 'should check for command name collisions if configured' do
old = Pry.config.collision_warning
Pry.config.collision_warning = true
cmd = @set.command 'frankie' do
end
frankie = 'boyle'
output = StringIO.new
cmd.new(:target => binding, :output => output).process_line %(frankie mouse)
output.string.should =~ /Command name collision/
Pry.config.collision_warning = old
end
it "should set the commands' arg_string and captures" do
cmd = @set.command /benj(ie|ei)/ do |*args|
self.arg_string.should == "mouse"
self.captures.should == ['ie']
args.should == ['ie', 'mouse']
end
cmd.new.process_line %(benjie mouse)
end
it "should raise an error if the line doesn't match the command" do
cmd = @set.command 'grunthos', 'the flatulent'
lambda {
cmd.new.process_line %(grumpos)
}.should.raise(Pry::CommandError)
end
end
end

View file

@ -1,176 +0,0 @@
require 'helper'
describe "Pry::CommandProcessor" do
before do
@pry = Pry.new
@pry.commands = Pry::CommandSet.new
@command_processor = Pry::CommandProcessor.new(@pry)
end
after do
@pry.commands = Pry::CommandSet.new
end
it 'should accurately determine if a command is valid' do
@pry.commands.command("test-command") {}
valid = @command_processor.valid_command? "test-command"
valid.should == true
valid = @command_processor.valid_command? "blah"
valid.should == false
end
it 'should correctly match a simple string command' do
@pry.commands.command("test-command") {}
command, captures, pos = @command_processor.command_matched "test-command", binding
command.name.should == "test-command"
captures.should == []
pos.should == "test-command".length
end
it 'should correctly match a simple string command with parameters' do
@pry.commands.command("test-command") { |arg|}
command, captures, pos = @command_processor.command_matched "test-command hello", binding
command.name.should == "test-command"
captures.should == []
pos.should == "test-command".length
end
it 'should not match when the relevant command does not exist' do
command, captures, pos = @command_processor.command_matched "test-command", binding
command.should == nil
captures.should == nil
end
it 'should correctly match a regex command' do
@pry.commands.command(/rue(.?)/) { }
command, captures, pos = @command_processor.command_matched "rue hello", binding
command.name.should == /rue(.?)/
captures.should == [""]
pos.should == 3
end
it 'should correctly match a regex command and extract the capture groups' do
@pry.commands.command(/rue(.?)/) { }
command, captures, pos = @command_processor.command_matched "rue5 hello", binding
command.name.should == /rue(.?)/
captures.should == ["5"]
pos.should == 4
end
it 'should correctly match a string command with spaces in its name' do
@pry.commands.command("test command") {}
command, captures, pos = @command_processor.command_matched "test command", binding
command.name.should == "test command"
captures.should == []
pos.should == command.name.length
end
it 'should correctly match a string command with spaces in its name with parameters' do
@pry.commands.command("test command") {}
command, captures, pos = @command_processor.command_matched "test command param1 param2", binding
command.name.should == "test command"
captures.should == []
pos.should == command.name.length
end
it 'should correctly match a command preceded by the command_prefix if one is defined' do
Pry.config.command_prefix = "%"
@pry.commands.command("test-command") {}
command, captures, pos = @command_processor.command_matched "%test-command hello", binding
command.name.should == "test-command"
captures.should == []
pos.should == "test-command".length + "%".length
Pry.config.command_prefix = ''
end
it 'should not match a command not preceded by the command_prefix if one is defined' do
Pry.config.command_prefix = "%"
@pry.commands.command("test-command") {}
command, captures, pos = @command_processor.command_matched "test-command hello", binding
command.should == nil
captures.should == nil
Pry.config.command_prefix = ''
end
it 'should match a command preceded by the command_prefix when :use_prefix => false' do
Pry.config.command_prefix = "%"
@pry.commands.command("test-command", "", :use_prefix => false) {}
command, captures, pos = @command_processor.command_matched "%test-command hello", binding
command.name.should == "test-command"
captures.should == []
pos.should == "test-command".length + "%".length
Pry.config.command_prefix = ''
end
it 'should match a command not preceded by the command_prefix when :use_prefix => false' do
Pry.config.command_prefix = "%"
@pry.commands.command("test-command", "", :use_prefix => false) {}
command, captures, pos = @command_processor.command_matched "test-command hello", binding
command.name.should == "test-command"
captures.should == []
pos.should == "test-command".length
Pry.config.command_prefix = ''
end
it 'should correctly match a regex command with spaces in its name' do
regex_command_name = /test\s+(.+)\s+command/
@pry.commands.command(regex_command_name) {}
sample_text = "test friendship command"
command, captures, pos = @command_processor.command_matched sample_text, binding
command.name.should == regex_command_name
captures.should == ["friendship"]
pos.should == sample_text.size
end
it 'should correctly match a complex regex command' do
regex_command_name = /\.(.*)/
@pry.commands.command(regex_command_name) {}
sample_text = ".cd ~/pry"
command, captures, pos = @command_processor.command_matched sample_text, binding
command.name.should == regex_command_name
captures.should == ["cd ~/pry"]
pos.should == sample_text.size
end
it 'should not interpolate commands that have :interpolate => false (interpolate_string should *not* be called)' do
@pry.commands.command("boast", "", :interpolate => false) {}
# remember to use '' instead of "" when testing interpolation or
# you'll cause yourself incredible confusion
lambda { @command_processor.command_matched('boast #{c}', binding) }.should.not.raise NameError
end
it 'should only execute the contents of an interpolation once' do
$obj = 'a'
redirect_pry_io(InputTester.new('cat #{$obj.succ!}'), StringIO.new) do
Pry.new.rep
end
$obj.should == 'b'
end
end

View file

@ -421,4 +421,128 @@ describe Pry::CommandSet do
end
end
describe 'find_command' do
it 'should find commands with the right string' do
cmd = @set.command('rincewind'){ }
@set.find_command('rincewind').should == cmd
end
it 'should not find commands with spaces before' do
cmd = @set.command('luggage'){ }
@set.find_command(' luggage').should == nil
end
it 'should find commands with arguments after' do
cmd = @set.command('vetinari'){ }
@set.find_command('vetinari --knock 3').should == cmd
end
it 'should find commands with names containing spaces' do
cmd = @set.command('nobby nobbs'){ }
@set.find_command('nobby nobbs --steal petty-cash').should == cmd
end
it 'should find command defined by regex' do
cmd = @set.command(/(capt|captain) vimes/i){ }
@set.find_command('Capt Vimes').should == cmd
end
it 'should find commands defined by regex with arguments' do
cmd = @set.command(/(cpl|corporal) Carrot/i){ }
@set.find_command('cpl carrot --write-home').should == cmd
end
it 'should not find commands by listing' do
cmd = @set.command(/werewol(f|ve)s?/, 'only once a month', :listing => "angua"){ }
@set.find_command('angua').should == nil
end
it 'should not find commands without command_prefix' do
Pry.config.command_prefix = '%'
cmd = @set.command('detritus'){ }
@set.find_command('detritus').should == nil
Pry.config.command_prefix = ''
end
it "should find commands that don't use the prefix" do
Pry.config.command_prefix = '%'
cmd = @set.command('colon', 'Sergeant Fred', :use_prefix => false){ }
@set.find_command('colon').should == cmd
Pry.config.command_prefix = ''
end
end
describe '.valid_command?' do
it 'should be true for commands that can be found' do
cmd = @set.command('archchancellor')
@set.valid_command?('archchancellor of_the?(:University)').should == true
end
it 'should be false for commands that can\'' do
@set.valid_command?('def monkey(ape)').should == false
end
end
describe '.process_line' do
it 'should return Result.new(false) if there is no matching command' do
result = @set.process_line('1 + 42')
result.command?.should == false
result.void_command?.should == false
result.retval.should == nil
end
it 'should return Result.new(true, VOID) if the command is not keep_retval' do
@set.command_class('mrs-cake') do
def process; 42; end
end
result = @set.process_line('mrs-cake')
result.command?.should == true
result.void_command?.should == true
result.retval.should == Pry::Command::VOID_VALUE
end
it 'should return Result.new(true, retval) if the command is keep_retval' do
@set.command_class('magrat', 'the maiden', :keep_retval => true) do
def process; 42; end
end
result = @set.process_line('magrat')
result.command?.should == true
result.void_command?.should == false
result.retval.should == 42
end
it 'should pass through context' do
ctx = {
:eval_string => "bloomers",
:pry_instance => Object.new,
:output => StringIO.new,
:target => binding
}
@set.command_class('agnes') do
define_method(:process) do
eval_string.should == ctx[:eval_string]
output.should == ctx[:output]
target.should == ctx[:target]
_pry_.should == ctx[:pry_instance]
end
end
@set.process_line('agnes', ctx)
end
it 'should add command_set to context' do
set = @set
@set.command_class(/nann+y ogg+/) do
define_method(:process) do
command_set.should == set
end
end
@set.process_line('nannnnnny oggggg')
end
end
end