diff --git a/lib/pry/command.rb b/lib/pry/command.rb index eecd0391..7e9c82e7 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -162,6 +162,13 @@ class Pry attr_accessor :command_set attr_accessor :_pry_ + # The block we pass *into* a command if it defines `:takes_block => true` + # @example + # my-command do + # puts "block content" + # end + attr_accessor :command_block + # Run a command from another command. # @param [String] command_string The string that invokes the command # @param [Array] args Further arguments to pass to the command @@ -281,9 +288,39 @@ class Pry self.arg_string = arg_string self.captures = captures + pass_block(args) if command_options[:takes_block] + call_safely(*(captures + args)) end + # Pass a block argument to a command. + # @param [Array] args The arguments passed to the command. + # We inspect these for a 'do' or a '{' and if we find it we use it + # to start a block input sequence. Once we have a complete + # block, we save it to an accessor that can be retrieved from the command context. + # Note that if we find the 'do' or '{' we delete this and the + # elements following it from `args`. + def pass_block(args) + if !(idx = args.find_index { |v| /^do;?$/ =~ v }) + idx = args.find_index { |v| /^\{;?$/ =~ v } + end + + if idx + block_init_string = args.slice!(idx..-1).join(" ") + prime_string = "proc #{block_init_string}\n" + + if !_pry_.complete_expression?(prime_string) + command_string = _pry_.r(target, prime_string) + else + command_string = prime_string + end + + self.command_block = target.eval(command_string) + end + end + + private :pass_block + # Run the command with the given {args}. # # This is a public wrapper around {#call} which ensures all preconditions are met. @@ -367,7 +404,11 @@ class Pry # @param *String the arguments passed # @return Object the return value of the block def call(*args) - instance_exec(*correct_arg_arity(block.arity, args), &block) + + # we need to define this method so we can pass a block in (not + # possible with `instance_exec`) + class << self; self; end.send(:define_method, :dummy, &block) + dummy(*correct_arg_arity(block.arity, args), &command_block) end def help; description; end @@ -403,7 +444,7 @@ class Pry output.puts slop.help void else - process(*correct_arg_arity(method(:process).arity, args)) + process(*correct_arg_arity(method(:process).arity, args), &command_block) end end diff --git a/test/helper.rb b/test/helper.rb index 9d4564c7..b48b5077 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -16,6 +16,14 @@ $VERBOSE = nil # Ensure we do not execute any rc files Pry::RC_FILES.clear +# inject a variable into a binding +def inject_var(name, value, b) + Thread.current[:__pry_local__] = value + b.eval("#{name} = Thread.current[:__pry_local__]") +ensure + Thread.current[:__pry_local__] = nil +end + # in case the tests call reset_defaults, ensure we reset them to # amended (test friendly) values class << Pry diff --git a/test/test_command.rb b/test/test_command.rb index 31efd760..2446aec1 100644 --- a/test/test_command.rb +++ b/test/test_command.rb @@ -340,5 +340,179 @@ describe "Pry::Command" do cmd.new.process_line %(grumpos) }.should.raise(Pry::CommandError) end - end -end + end + + describe "block parameters" do + before do + @context = Object.new + @set.command "walking-spanish", "down the hall", :takes_block => true do |&block| + inject_var(:@x, block.call, target) + end + @set.import Pry::Commands + end + + describe ":takes_block option" do + it 'should accept block when :takes_block => true' do + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + it 'should NOT accept block when :takes_block => false' do + @set.command "walking-spanish", "down the hall", :takes_block => false do |&block| + inject_var(:@x, block, target) + end + + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == nil + end + end + + it 'should accept multiline blocks' do + redirect_pry_io(InputTester.new("walking-spanish do", + " :jesus", + "end", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + describe "single line blocks" do + it 'should accept blocks with do ; end' do + redirect_pry_io(InputTester.new("walking-spanish do ; :jesus; end", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + it 'should accept blocks with do; end' do + redirect_pry_io(InputTester.new("walking-spanish do; :jesus; end", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + it 'should accept blocks with { }' do + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + end + + describe "block-related content removed from arguments" do + describe "block_command" do + it "should remove block-related content from arguments" do + @set.block_command "walking-spanish", "down the hall", :takes_block => true do |x, y| + inject_var(:@x, x, target) + inject_var(:@y, y, target) + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == nil + @context.instance_variable_get(:@y).should == nil + end + + it "should NOT remove block-related content from arguments if :takes_block => false" do + @set.block_command "walking-spanish", "down the hall", :takes_block => false do |x, y| + inject_var(:@x, x, target) + inject_var(:@y, y, target) + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == "{" + @context.instance_variable_get(:@y).should == ":jesus" + end + end + + describe "create_command" do + it "should remove block-related content from arguments" do + @set.create_command "walking-spanish", "down the hall", :takes_block => true do + def process(x, y) + inject_var(:@x, x, target) + inject_var(:@y, y, target) + end + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == nil + @context.instance_variable_get(:@y).should == nil + end + + it "should NOT remove block-related content from arguments if :takes_block => false" do + @set.create_command "walking-spanish", "down the hall", :takes_block => false do + def process(x, y) + inject_var(:@x, x, target) + inject_var(:@y, y, target) + end + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == "{" + @context.instance_variable_get(:@y).should == ":jesus" + end + end + end + + describe "exposing block parameter" do + describe "block_command" do + it "should expose block in command_block method" do + @set.block_command "walking-spanish", "down the hall", :takes_block => true do + inject_var(:@x, command_block.call, target) + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + # test for &block already completed many times in other sections + end + + describe "create_command" do + it "should expose &block in create_command's process method" do + @set.create_command "walking-spanish", "down the hall", :takes_block => true do + def process(&block) + inject_var(:@x, block.call, target) + end + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + + it "should expose block in command_block method" do + @set.create_command "walking-spanish", "down the hall", :takes_block => true do + def process + inject_var(:@x, command_block.call, target) + end + end + redirect_pry_io(InputTester.new("walking-spanish { :jesus }", + "exit-all"), out = StringIO.new) do + Pry.start @context, :commands => @set + end + @context.instance_variable_get(:@x).should == :jesus + end + end + end + end + end