added import_from, delete, run to command API. Also added examples, added more tests, and changed the way that commands work using split instead of regex captures. Also added output and trget methods to Pry::CommandBase

This commit is contained in:
John Mair 2011-01-21 03:41:41 +13:00
parent ec66e9bade
commit 548b4f82d8
13 changed files with 458 additions and 85 deletions

View File

@ -343,14 +343,19 @@ and executed before a Ruby eval takes place. Pry comes with a default
command set (`Pry::Commands`), but these commands can be augmented or overriden by
user-specified ones.
The Pry command API is quite sophisticated supporting features such as:
command set inheritance, importing of specific commands from another
command set, deletion of commands, calling of commands within other
commands, and so on.
A valid Pry command object must inherit from
`Pry::CommandBase` and use the special command API:
#### Example: Defining a command object and setting it globally
class MyCommands < Pry::CommandBase
command "hello", "Output hello to the user." do |name|
opts[:output].puts "hello #{name}!"
command greet", "Greet the user." do |name|
output.puts "Hello #{name.capitalize}, how are you?"
end
end
@ -358,8 +363,8 @@ A valid Pry command object must inherit from
Then inside a pry session:
pry(main)> hello john
hello john!
pry(main)> greet john
hello John, how are you?
=> nil
#### Example: Using a command object in a specific session
@ -367,7 +372,7 @@ Then inside a pry session:
As in the case of `input` and `output`:
##### At session start:
nnnnn
Pry.start(self, :commands => MyCommands)
##### At runtime:
@ -377,9 +382,9 @@ nnnnn
#### The command API
The command API is defined by the `Pry::CommandBase` class (hence why
all commands must inherit from it). The API works as follows:
all commands must inherit from it or a subclass). The API works as follows:
The `command` method defines a new command, its parameter is the
* The `command` method defines a new command, its parameter is the
name of the command and an optional second parameter is a description of
the command.
@ -398,23 +403,64 @@ for the command name - all these strings will be valid names for the
command.
command ["ls", "dir"], "show a list of local vars" do
opts[:output].puts opts[:target].eval("local_variables")
output.puts target.eval("local_variables")
end
* The `delete` method deletes a command or a group of a commands; it
can be useful when inheriting from another command set when you decide
to keep only a portion of inherited commands.
#### opts hash
class MyCommands < Pry::Commands
delete "show_method", "show_imethod"
end
Note that in the example above we are using `opts[:output]` for output; this is the output
object in use by the current pry session. Other hash values accessible
within a `command` block include:
* The `import_from` method enables you to specifically select which
commands will be copied across from another command set, useful when
you only want a small number of commands and so inheriting and then
deleting would be inefficient. The first parameter to `import_from`
is the class to import from and the other paramters are the names of
the commands to import:
class MyCommands < Pry::CommandBase
import_from Pry::Commands, "ls", "status", "!"
end
* The `run` command invokes one command from within another.
The first parameter is the name of the command to invoke
and the remainder of the parameters will be passed on to the command
being invoked:
class MyCommands < Pry::Commands
command "ls_with_hello" do
output.puts "hello!"
run "ls"
end
end
#### Utility methods for commands
All commands can access the special `output` and `target` methods. The
`output` method returns the `output` object for the active pry session.
Ensuring that your commands invoke `puts` on this rather than using
the top-level `puts` will ensure that all your session output goes to
the same place.
The `target` method returns the `Binding` object the Pry session is currently
active on - useful when your commands need to manipulate or examine
the state of the object. E.g, the "ls" command is implemented as follows
command "ls" do
output.puts target.eval("local_variables + instance_variables").inspect
end
#### The opts hash
These are miscellaneous variables that may be useful to your own commands:
* `opts[:output]` - The session's output object.
* `opts[:val]` - The line of input that invoked the command.
* `opts[:eval_string]` - The cumulative lines of input for multi-line input.
* `opts[:target]` - The object the Pry session is currently on.
* `opts[:captures]` - The array of regex captures generated by the command (if any).
* `opts[:nesting]` - Lowlevel session nesting information.
* `opts[:command_info]` - Lowlevel information about all Pry commands.
* `opts[:commands]` - Lowlevel data of all Pry commands.
(see commands.rb for examples of how some of these options are used)
@ -424,7 +470,11 @@ The `Pry::CommandBase` class automatically defines a `help` command
for you. Typing `help` in a Pry session will show a list of commands
to the user followed by their descriptions. Passing a parameter to
`help` with the command name will just return the description of that
specific command.
specific command. If a description is left out it will automatically
be given the description "No description.".
If the description is explicitly set to `""` then this command will
not be displayed in `help`.
### Hooks

View File

@ -0,0 +1,27 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
# inherit standard command set, but tweak them by deleting some and
# overriding others
class MyCommands < Pry::CommandBase
# Override ls command
command "ls", "An unhelpful ls" do
output.puts "No, i refuse to display any useful information."
end
# Invoke one command from within another using `run`
command "status2" do |x|
output.puts "About to show status, are you ready?"
run "status", x
output.puts "Finished showing status."
end
import_from Pry::Commands, "quit", "show_method", "ls"
end
# Start a Pry session using the commands defined in MyCommands
# Type 'help' in Pry to get a list of the commands and their descriptions
Pry.start(TOPLEVEL_BINDING, :commands => MyCommands)

View File

@ -0,0 +1,36 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
class MathCommands < Pry::CommandBase
command "greet", "Greet a person, e.g: greet john" do |name|
output.puts "Good afternoon #{name.capitalize}! Do you like Math?"
end
command "add", "Add a list of numbers together, e.g: add 1 2 3 4" do |*args|
output.puts "Total: #{args.map(&:to_f).inject(&:+)}"
end
command "multiply", "Multiply a list of numbers together, e.g: multiply 1 2 3 4" do |*args|
output.puts "Total: #{args.map(&:to_f).inject(&:*)}"
end
# Explicitly giving a description of "" to prevent command being
# displayed in 'help'
command "exit", "" do
throw :breakout, 0
end
end
# Since we provide math commands, let's have mathematical
# before_session and after_session hooks, and a mathematical prompt
math_prompt = proc { "math> " }
math_hooks = {
:before_session => proc { |output, *| output.puts "Welcome! Let's do some math! Type 'help' for a list of commands." },
:after_session => proc { |output, *| output.puts "Goodbye!" }
}
# Start a Pry session using the commands defined in MyCommands
# Type 'help' in Pry to get a list of the commands and their descriptions
Pry.start(TOPLEVEL_BINDING, :commands => MathCommands, :prompt => math_prompt, :hooks => math_hooks)

12
examples/example_hooks.rb Normal file
View File

@ -0,0 +1,12 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
my_hooks = {
:before_session => proc { |out, obj| out.puts "Opening #{obj}." },
:after_session => proc { |out, obj| out.puts "Closing #{obj}." }
}
# Start a Pry session using the hooks hash defined in my_hooks
Pry.start(TOPLEVEL_BINDING, :hooks => my_hooks)

View File

@ -0,0 +1,70 @@
# Note: this requires you to have Gosu and TexPlay installed.
# `gem install gosu`
# `gem install texplay`
#
# Extra instructions for installing Gosu on Linux can be found here:
# http://code.google.com/p/gosu/wiki/GettingStartedOnLinux
#
# Instructions for using TexPlay can be found here:
# http://banisterfiend.wordpress.com/2008/08/23/texplay-an-image-manipulation-tool-for-ruby-and-gosu/
#
# Have fun! :)
direc = File.dirname(__FILE__)
require 'rubygems'
require "texplay"
require "#{direc}/../lib/pry"
WIDTH = 640
HEIGHT = 480
IMAGE_PROMPT = [ proc { "(image edit)> " }, proc { "(image edit)* " } ]
class ImageCommands < Pry::CommandBase
command "drawing_methods", "Show a list of TexPlay methods" do
output.puts "#{Pry.view(TexPlay.public_instance_methods)}"
end
command "exit", "Exit the program." do
output.puts "Thanks for dropping by!"
exit
end
import_from Pry::Commands, "ls", "ls_methods", "!"
end
class WinClass < Gosu::Window
def initialize
super(WIDTH, HEIGHT, false)
@img = TexPlay.create_image(self, 200, 200)
@img.rect 0, 0, @img.width - 1, @img.height - 1
@binding = @img.__binding__
@pry_instance = Pry.new(:commands => ImageCommands, :prompt => IMAGE_PROMPT)
end
def draw
@img.draw_rot(WIDTH / 2, HEIGHT / 2, 1, 0, 0.5, 0.5)
end
def update
exit if button_down?(Gosu::KbEscape)
# We do not want a REPL session as the loop prevents the image
# being updated; instead we do a REP session, and let the image
# update each time the user presses enter. We maintain the same
# binding object to keep locals between calls to `Pry#rep()`
@pry_instance.rep(@binding)
end
end
puts "Welcome to ImageEdit; type `help` for a list of commands and `drawing_methods` for a list of drawing methods available."
puts "--"
puts "Example: Try typing 'circle width/2, height/2, 95, :color => :blue, :fill => true'"
puts "If you want to save your image, type: save(\"img.png\")"
WinClass.new.show

10
examples/example_input.rb Normal file
View File

@ -0,0 +1,10 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
# Create a StringIO that contains the input data
str_input = StringIO.new("puts 'hello world!'\nputs \"I am in \#{self}\"\nexit")
# Start a Pry session on the Fixnum 5 using the input data in str_input
Pry.start(5, :input => str_input)

View File

@ -0,0 +1,14 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
# Create a StringIO to contain the output data
str_output = StringIO.new
# Start a Pry session on the Fixnum 5 using str_output to store the
# output (not writing to $stdout)
Pry.start(5, :output => str_output)
# Display all the output accumulated during the session
puts str_output.string

View File

@ -0,0 +1,9 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
my_print = proc { |out, value| out.puts "Output is: #{value.inspect}" }
# Start a Pry session using the print object defined in my_print
Pry.start(TOPLEVEL_BINDING, :print => my_print)

View File

@ -0,0 +1,12 @@
direc = File.dirname(__FILE__)
require 'rubygems'
require "#{direc}/../lib/pry"
# Remember, first prompt in array is the main prompt, second is the wait
# prompt (used for multiline input when more input is required)
my_prompt = [ proc { |obj, *| "inside #{obj}> " },
proc { |obj, *| "inside #{obj}* "} ]
# Start a Pry session using the prompt defined in my_prompt
Pry.start(TOPLEVEL_BINDING, :prompt => my_prompt)

View File

@ -6,7 +6,7 @@ class Pry
class << self
attr_accessor :commands
attr_accessor :command_info
attr_accessor :opts
attr_accessor :opts, :output, :target
# private because we want to force function style invocation. We require
# that the location where the block is defined has the `opts`
@ -14,7 +14,7 @@ class Pry
private
# Defines a new Pry command.
# @param [String, Array] name The name of the command (or array of
# @param [String, Array] names The name of the command (or array of
# command name aliases).
# @param [String] description A description of the command.
# @yield The action to perform. The parameters in the block
@ -34,42 +34,73 @@ class Pry
# # Good afternoon John!
# # pry(main)> help greet
# # Greet somebody
def command(name, description="No description.", &block)
def command(names, description="No description.", &block)
@commands ||= {}
@command_info ||= {}
arg_match = '(?:\s+(\S+))?' * 20
if name.is_a?(Array)
name.each do |n|
matcher = /^#{n}(?!\S)#{arg_match}?/
commands[matcher] = block
command_info[n] = description
end
else
matcher = /^#{name}(?!\S)#{arg_match}?/
commands[matcher] = block
command_info[name] = description
Array(names).each do |name|
commands[name] = { :description => description, :action => block }
end
end
# Delete a command or an array of commands.
# Useful when inheriting from another command set and pruning
# those commands down to the ones you want.
# @param [Array<String>] names The command name or array
# of command names you want to delete
# @example Deleteing inherited commands
# class MyCommands < Pry::Commands
# delete "show_method", "show_imethod", "show_doc", "show_idoc"
# end
# Pry.commands = MyCommands
def delete(*names)
names.each { |name| commands.delete(name) }
end
# Execute a command (this enables commands to call other commands).
# @param [String] name The command to execute
# @param [Array] args The parameters to pass to the command.
# @example Wrap one command with another
# class MyCommands < Pry::Commands
# command "ls2" do
# output.puts "before ls"
# run "ls"
# output.puts "after ls"
# end
# end
def run(name, *args)
action = opts[:commands][name][:action]
instance_exec(*args, &action)
end
# Import commands from another command object.
# @param [Pry::CommandBase] klass The class to import from (must
# be a subclass of `Pry::CommandBase`)
# @param [Array<String>] names The commands to import.
# @example
# class MyCommands < Pry::CommandBase
# import_from Pry::Commands, "ls", "show_method", "cd"
# end
def import_from(klass, *names)
imported_hash = Hash[klass.commands.select { |k, v| names.include?(k) }]
commands.merge!(imported_hash)
end
end
command "help", "This menu." do |cmd|
out = opts[:output]
command_info = opts[:command_info]
command_info = opts[:commands]
param = cmd
if !param
out.puts "Command list:"
out.puts "--"
command_info.each do |k, v|
out.puts "#{k}".ljust(18) + v
output.puts "Command list:"
output.puts "--"
command_info.each do |k, data|
output.puts "#{k}".ljust(18) + data[:description] if !data[:description].empty?
end
else
key = command_info.keys.find { |v| Array(v).any? { |k| k === param } }
if key
out.puts command_info[key]
if command_info[param]
output.puts command_info[param][:description]
else
out.puts "No info for command: #{param}"
output.puts "No info for command: #{param}"
end
end
end
@ -77,7 +108,7 @@ class Pry
# Ensures that commands can be inherited
def self.inherited(klass)
klass.commands = commands.dup
klass.command_info = command_info.dup
end
end
end

View File

@ -7,12 +7,12 @@ class Pry
class Commands < CommandBase
command "!", "Refresh the REPL" do
opts[:output].puts "Refreshed REPL"
output.puts "Refreshed REPL"
opts[:eval_string].clear
end
command "!pry", "Start a Pry session on current self; this even works mid-expression." do
Pry.start(opts[:target])
Pry.start(target)
end
command ["exit_program", "quit_program"], "End the current program." do
@ -20,7 +20,7 @@ class Pry
end
command "nesting", "Show nesting information." do
out = opts[:output]
out = output
nesting = opts[:nesting]
out.puts "Nesting status:"
@ -35,9 +35,8 @@ class Pry
end
command "status", "Show status information." do
out = opts[:output]
out = output
nesting = opts[:nesting]
target = opts[:target]
out.puts "Status:"
out.puts "--"
@ -53,37 +52,37 @@ class Pry
end
command "ls", "Show the list of vars in the current scope." do
opts[:output].puts "#{Pry.view(opts[:target].eval('local_variables + instance_variables'))}"
output.puts "#{Pry.view(target.eval('local_variables + instance_variables'))}"
end
command "cat", "Show output of <var>.inspect." do |obj|
out = opts[:output]
out.puts opts[:target].eval("#{obj}.inspect")
out = output
out.puts target.eval("#{obj}.inspect")
end
command "cd", "Start a Pry session on <var> (use `cd ..` to go back)" do |obj|
throw(:breakout, opts[:nesting].level) if obj == ".."
opts[:target].eval("#{obj}.pry")
target.eval("#{obj}.pry")
end
command "show_doc", "Show the comments above <methname>" do |meth_name|
doc = opts[:target].eval("method(:#{meth_name})").comment
opts[:output].puts doc
doc = target.eval("method(:#{meth_name})").comment
output.puts doc
end
command "show_idoc", "Show the comments above instance method <methname>" do |meth_name|
doc = opts[:target].eval("instance_method(:#{meth_name})").comment
opts[:output].puts doc
doc = target.eval("instance_method(:#{meth_name})").comment
output.puts doc
end
command "show_method", "Show sourcecode for method <methname>." do |meth_name|
doc = opts[:target].eval("method(:#{meth_name})").source
opts[:output].puts doc
doc = target.eval("method(:#{meth_name})").source
output.puts doc
end
command "show_imethod", "Show sourcecode for instance method <methname>." do |meth_name|
doc = opts[:target].eval("instance_method(:#{meth_name})").source
opts[:output].puts doc
doc = target.eval("instance_method(:#{meth_name})").source
output.puts doc
end
command "jump_to", "Jump to a Pry session further up the stack, exiting all sessions below." do |break_level|
@ -92,21 +91,21 @@ class Pry
case break_level
when nesting.level
opts[:output].puts "Already at nesting level #{nesting.level}"
output.puts "Already at nesting level #{nesting.level}"
when (0...nesting.level)
throw(:breakout, break_level + 1)
else
max_nest_level = nesting.level - 1
opts[:output].puts "Invalid nest level. Must be between 0 and #{max_nest_level}. Got #{break_level}."
output.puts "Invalid nest level. Must be between 0 and #{max_nest_level}. Got #{break_level}."
end
end
command "ls_methods", "List all methods defined on class of receiver." do
opts[:output].puts "#{Pry.view(opts[:target].eval('public_methods(false) + private_methods(false) + protected_methods(false)'))}"
output.puts "#{Pry.view(target.eval('public_methods(false) + private_methods(false) + protected_methods(false)'))}"
end
command "ls_imethods", "List all instance methods defined on class of receiver." do
opts[:output].puts "#{Pry.view(opts[:target].eval('public_instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)'))}"
output.puts "#{Pry.view(target.eval('public_instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)'))}"
end
command ["exit", "quit", "back"], "End the current Pry session." do

View File

@ -125,7 +125,7 @@ class Pry
target = binding_for(target)
if input == Readline
Readline.completion_proc = Pry::InputCompleter.build_completion_proc(target, Pry.commands.command_info.keys.flatten)
Readline.completion_proc = Pry::InputCompleter.build_completion_proc(target, commands.commands.keys)
end
# eval the expression and save to last_result
@ -182,28 +182,29 @@ class Pry
def val.clear() replace("") end
def eval_string.clear() replace("") end
pattern, action = commands.commands.find { |k, v| k === val }
pattern, data = commands.commands.find do |name, data|
/^#{name}(?!\S)(?:\s+(.+))?/ =~ val
end
if pattern
captures = Regexp.last_match.captures
captures.compact!
args_string = $1
args = args_string ? args_string.split : []
action = data[:action]
options = {
:captures => captures,
:captures => args_string,
:eval_string => eval_string,
:target => target,
:val => val,
:nesting => nesting,
:output => output,
:command_info => commands.command_info
:commands => commands.commands
}
# because procs are defined in different places (e.g 'help' in CommandBase)
# we cannot simply use `commands.opts=...`; instead we have to
# retrieve the object where the block was defined; since that is
# where the `opts` method the block will have access to is defined.
action_self = action.binding.eval('self')
action_self.opts = options
# set some useful methods to be used by the action blocks
commands.opts = options
commands.target = target
commands.output = output
# send the correct number of parameters to the block (to avoid
# warnings in 1.8.7)
@ -212,14 +213,19 @@ class Pry
# if arity is negative then we have a *args in 1.8.7.
# In 1.9 we have default values or *args
action.call(*captures)
# Use instance_exec() to make the opts, target, output, etc methods available
commands.instance_exec(*args, &action)
when 1, 0
# ensure that we get the right number of parameters;
# using values_at we pad out missing parameters with nils so
# that 1.8.7 doesn't complain about incorrect arity (1.9.2
# doesn't care)
action.call(*captures.values_at(*0..(action.arity - 1)))
args_with_corrected_arity = args.values_at *0..(action.arity - 1)
# Use instance_exec() to make the opts, target, output, etc methods
# available
commands.instance_exec(*args_with_corrected_arity, &action)
end
val.clear

View File

@ -298,25 +298,122 @@ describe Pry do
end
Command2.commands.keys.size.should == 2
Command2.command_info.keys.include?("help").should == true
Command2.command_info.keys.include?("h").should == true
Command2.commands.keys.include?("help").should == true
Command2.commands.keys.include?("h").should == true
Object.remove_const(:Command2)
end
it 'should inherit comands from Pry::Commands' do
it 'should inherit commands from Pry::Commands' do
class Command3 < Pry::Commands
command "v" do
end
end
Command3.command_info.include?("nesting").should == true
Command3.command_info.include?("jump_to").should == true
Command3.command_info.include?("cd").should == true
Command3.command_info.include?("v").should == true
Command3.commands.include?("nesting").should == true
Command3.commands.include?("jump_to").should == true
Command3.commands.include?("cd").should == true
Command3.commands.include?("v").should == true
Object.remove_const(:Command3)
end
it 'should run a command from within a command' do
class Command3 < Pry::Commands
command "v" do
output.puts "v command"
end
command "run_v" do
run "v"
end
end
str_output = StringIO.new
Pry.new(:input => InputTester.new("run_v"), :output => str_output, :commands => Command3).rep
str_output.string.should =~ /v command/
Object.remove_const(:Command3)
end
it 'should enable an inherited method to access opts and output and target, due to instance_exec' do
class Command3 < Pry::Commands
command "v" do
output.puts "#{target.eval('self')}"
end
end
class Command4 < Command3
end
str_output = StringIO.new
Pry.new(:print => proc {}, :input => InputTester.new("v"),
:output => str_output, :commands => Command4).rep("john")
str_output.string.chomp.should == "john"
Object.remove_const(:Command3)
Object.remove_const(:Command4)
end
it 'should import commands from another command object' do
class Command3 < Pry::CommandBase
import_from Pry::Commands, "status", "jump_to"
end
str_output = StringIO.new
Pry.new(:print => proc {}, :input => InputTester.new("status"),
:output => str_output, :commands => Command3).rep("john")
str_output.string.should =~ /Status:/
Object.remove_const(:Command3)
end
it 'should delete some inherited commands when using delete method' do
class Command3 < Pry::Commands
command "v" do
end
delete "show_doc", "show_method"
delete "ls"
end
Command3.commands.include?("nesting").should == true
Command3.commands.include?("jump_to").should == true
Command3.commands.include?("cd").should == true
Command3.commands.include?("v").should == true
Command3.commands.include?("show_doc").should == false
Command3.commands.include?("show_method").should == false
Command3.commands.include?("ls").should == false
Object.remove_const(:Command3)
end
it 'should override some inherited commands' do
class Command3 < Pry::Commands
command "jump_to" do
output.puts "jump_to the music"
end
command "help" do
output.puts "help to the music"
end
end
# suppress evaluation output
Pry.print = proc {}
str_output = StringIO.new
Pry.new(:input => InputTester.new("jump_to"), :output => str_output, :commands => Command3).rep
str_output.string.chomp.should == "jump_to the music"
str_output = StringIO.new
Pry.new(:input => InputTester.new("help"), :output => str_output, :commands => Command3).rep
str_output.string.chomp.should == "help to the music"
Object.remove_const(:Command3)
Pry.reset_defaults
end
end
it "should set the print default, and the default should be overridable" do