mirror of
https://github.com/pry/pry.git
synced 2022-11-09 12:35:05 -05:00
422 lines
13 KiB
Ruby
422 lines
13 KiB
Ruby
class Pry
|
|
|
|
# The super-class of all commands, new commands should be created by calling
|
|
# {Pry::CommandSet#command} which creates a BlockCommand or {Pry::CommandSet#command_class}
|
|
# which creates a ClassCommand. Please don't use this class directly.
|
|
class Command
|
|
|
|
# represents a void return value for a command
|
|
VOID_VALUE = Object.new
|
|
|
|
# give it a nice inspect
|
|
def VOID_VALUE.inspect() "void" end
|
|
|
|
# Properties of the command itself (as passed as arguments to
|
|
# {CommandSet#command} or {CommandSet#command_class}).
|
|
class << self
|
|
attr_accessor :block
|
|
attr_accessor :name
|
|
attr_writer :description
|
|
attr_writer :command_options
|
|
|
|
# Define or get the command's description
|
|
def description(arg=nil)
|
|
@description = arg if arg
|
|
@description
|
|
end
|
|
|
|
# Define or get the command's options
|
|
def command_options(arg=nil)
|
|
@command_options ||= {}
|
|
@command_options.merge!(arg) if arg
|
|
@command_options
|
|
end
|
|
# backward compatibility
|
|
alias_method :options, :command_options
|
|
alias_method :options=, :command_options=
|
|
|
|
# Define or get the command's banner
|
|
def banner(arg=nil)
|
|
@banner = arg if arg
|
|
@banner || description
|
|
end
|
|
end
|
|
|
|
|
|
# Make those properties accessible to instances
|
|
def name; self.class.name; end
|
|
def description; self.class.description; end
|
|
def block; self.class.block; end
|
|
def command_options; self.class.options; end
|
|
def command_name; command_options[:listing]; end
|
|
|
|
class << self
|
|
def inspect
|
|
"#<class(Pry::Command #{name.inspect})>"
|
|
end
|
|
|
|
# Create a new command with the given properties.
|
|
#
|
|
# @param String name the name of the command
|
|
# @param String description the description to appear in {help}
|
|
# @param Hash options behavioural options (@see {Pry::CommandSet#command})
|
|
# @param Module helpers a module of helper functions to be included.
|
|
# @param Proc &block (optional, a block, used for BlockCommands)
|
|
#
|
|
# @return Class (a subclass of Pry::Command)
|
|
#
|
|
def subclass(name, description, options, helpers, &block)
|
|
klass = Class.new(self)
|
|
klass.send(:include, helpers)
|
|
klass.name = name
|
|
klass.description = description
|
|
klass.command_options = options
|
|
klass.block = block
|
|
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 {Pry#run_command} as a hash of
|
|
# context and expanded in {#initialize}
|
|
attr_accessor :output
|
|
attr_accessor :target
|
|
attr_accessor :captures
|
|
attr_accessor :eval_string
|
|
attr_accessor :arg_string
|
|
attr_accessor :context
|
|
attr_accessor :command_set
|
|
attr_accessor :_pry_
|
|
|
|
# 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
|
|
# @example
|
|
# run "show-input"
|
|
# @example
|
|
# run ".ls"
|
|
# @example
|
|
# run "amend-line", "5", 'puts "hello world"'
|
|
def run(command_string, *args)
|
|
complete_string = "#{command_string} #{args.join(" ")}"
|
|
command_set.process_line(complete_string, context)
|
|
end
|
|
|
|
def commands
|
|
command_set.commands
|
|
end
|
|
|
|
def text
|
|
Pry::Helpers::Text
|
|
end
|
|
|
|
def void
|
|
VOID_VALUE
|
|
end
|
|
|
|
include Pry::Helpers::BaseHelpers
|
|
include Pry::Helpers::CommandHelpers
|
|
|
|
|
|
# Instantiate a command, in preparation for calling it.
|
|
#
|
|
# @param Hash context The runtime context to use with this command.
|
|
def initialize(context={})
|
|
self.context = context
|
|
self.target = context[:target]
|
|
self.output = context[:output]
|
|
self.eval_string = context[:eval_string]
|
|
self.command_set = context[:command_set]
|
|
self._pry_ = context[:pry_instance]
|
|
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.
|
|
#
|
|
# @param *[String] the arguments to pass to this command.
|
|
# @return Object the return value of the {#call} method, or Command::VOID_VALUE
|
|
def call_safely(*args)
|
|
unless dependencies_met?
|
|
gems_needed = Array(command_options[:requires_gem])
|
|
gems_not_installed = gems_needed.select { |g| !gem_installed?(g) }
|
|
output.puts "\nThe command '#{name}' is #{Helpers::Text.bold("unavailable")} because it requires the following gems to be installed: #{(gems_not_installed.join(", "))}"
|
|
output.puts "-"
|
|
output.puts "Type `install-command #{name}` to install the required gems and activate this command."
|
|
return void
|
|
end
|
|
|
|
if command_options[:argument_required] && args.empty?
|
|
raise CommandError, "The command '#{name}' requires an argument."
|
|
end
|
|
|
|
ret = call_with_hooks(*args)
|
|
command_options[:keep_retval] ? ret : void
|
|
end
|
|
|
|
# Are all the gems required to use this command installed?
|
|
#
|
|
# @return Boolean
|
|
def dependencies_met?
|
|
@dependencies_met ||= command_dependencies_met?(command_options)
|
|
end
|
|
|
|
private
|
|
|
|
# Run the {#call} method and all the registered hooks.
|
|
#
|
|
# @param *String the arguments to #{call}
|
|
# @return Object the return value from #{call}
|
|
def call_with_hooks(*args)
|
|
self.class.hooks[:before].each do |block|
|
|
instance_exec(*args, &block)
|
|
end
|
|
|
|
ret = call(*args)
|
|
|
|
self.class.hooks[:after].each do |block|
|
|
ret = instance_exec(*args, &block)
|
|
end
|
|
|
|
ret
|
|
end
|
|
end
|
|
|
|
# A super-class for Commands that are created with a single block.
|
|
#
|
|
# This class ensures that the block is called with the correct number of arguments
|
|
# and the right context.
|
|
#
|
|
# Create subclasses using {Pry::CommandSet#command}.
|
|
class BlockCommand < Command
|
|
# backwards compatibility
|
|
alias_method :opts, :context
|
|
|
|
# Call the block that was registered with this command.
|
|
#
|
|
# @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)
|
|
end
|
|
|
|
# Fix the number of arguments we pass to a block to avoid arity warnings.
|
|
#
|
|
# @param Number the arity of the block
|
|
# @param Array the arguments to pass
|
|
# @return Array a (possibly shorter) array of the arguments to pass
|
|
def correct_arg_arity(arity, args)
|
|
case
|
|
when arity < 0
|
|
args
|
|
when arity == 0
|
|
[]
|
|
when arity > 0
|
|
args.values_at(*(0..(arity - 1)).to_a)
|
|
end
|
|
end
|
|
|
|
def help; description; end
|
|
end
|
|
|
|
# A super-class ofr Commands with structure.
|
|
#
|
|
# This class implements the bare-minimum functionality that a command should have,
|
|
# namely a --help switch, and then delegates actual processing to its subclasses.
|
|
#
|
|
# Create subclasses using {Pry::CommandSet#command_class}, and override the {options(opt)} method
|
|
# to set up an instance of Slop, and the {process} method to actually run the command. If
|
|
# necessary, you can also override {setup} which will be called before {options}, for example to
|
|
# require any gems your command needs to run, or to set up state.
|
|
class ClassCommand < Command
|
|
|
|
attr_accessor :opts
|
|
attr_accessor :args
|
|
|
|
# Set up {opts} and {args}, and then call {process}
|
|
#
|
|
# This function will display help if necessary.
|
|
#
|
|
# @param *String the arguments passed
|
|
# @return Object the return value of {process} or VOID_VALUE
|
|
def call(*args)
|
|
setup
|
|
|
|
self.opts = slop
|
|
self.args = self.opts.parse!(args)
|
|
|
|
if opts.present?(:help)
|
|
output.puts slop.help
|
|
void
|
|
else
|
|
process
|
|
end
|
|
end
|
|
|
|
# Return the help generated by Slop for this command.
|
|
def help
|
|
slop.help
|
|
end
|
|
|
|
# Return an instance of Slop that can parse the options that this command accepts.
|
|
def slop
|
|
Slop.new do |opt|
|
|
opt.banner(unindent(self.class.banner))
|
|
options(opt)
|
|
opt.on(:h, :help, "Show this message.")
|
|
end
|
|
end
|
|
|
|
# A function called just before {options(opt)} as part of {call}.
|
|
#
|
|
# This function can be used to set up any context your command needs to run, for example
|
|
# requiring gems, or setting default values for options.
|
|
#
|
|
# @example
|
|
# def setup;
|
|
# require 'gist'
|
|
# @action = :method
|
|
# end
|
|
def setup; end
|
|
|
|
# A function to setup Slop so it can parse the options your command expects.
|
|
#
|
|
# NOTE: please don't do anything side-effecty in the main part of this method,
|
|
# as it may be called by Pry at any time for introspection reasons. If you need
|
|
# to set up default values, use {setup} instead.
|
|
#
|
|
# @example
|
|
# def options(opt)
|
|
# opt.banner "Gists methods or classes"
|
|
# opt.on(:c, :class, "gist a class") do
|
|
# @action = :class
|
|
# end
|
|
# end
|
|
def options(opt); end
|
|
|
|
# The actual body of your command should go here.
|
|
#
|
|
# The {opts} mehod can be called to get the options that Slop has passed,
|
|
# and {args} gives the remaining, unparsed arguments.
|
|
#
|
|
# The return value of this method is discarded unless the command was created
|
|
# with :keep_retval => true, in which case it is returned to the repl.
|
|
#
|
|
# @example
|
|
# def process
|
|
# if opts.present?(:class)
|
|
# gist_class
|
|
# else
|
|
# gist_method
|
|
# end
|
|
# end
|
|
def process; raise CommandError, "command '#{name}' not implemented" end
|
|
end
|
|
end
|