mirror of
https://github.com/pry/pry.git
synced 2022-11-09 12:35:05 -05:00
473bc9f2d6
Quite often, when we deprecate APIs, we want to print a warning. Such a warning would be hard to track without a file and line location. We are trying to be a good citizen and print warnings like Ruby does it.
518 lines
15 KiB
Ruby
518 lines
15 KiB
Ruby
require 'delegate'
|
|
require 'shellwords'
|
|
|
|
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#create_command} which creates a ClassCommand. Please don't
|
|
# use this class directly.
|
|
class Command
|
|
extend Helpers::DocumentationHelpers
|
|
extend CodeObject::Helpers
|
|
|
|
include Pry::Helpers::BaseHelpers
|
|
include Pry::Helpers::CommandHelpers
|
|
include Pry::Helpers::Text
|
|
|
|
# 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#create_command}).
|
|
class << self
|
|
attr_writer :block
|
|
attr_writer :description
|
|
attr_writer :command_options
|
|
attr_writer :match
|
|
|
|
def match(arg = nil)
|
|
if arg
|
|
@command_options ||= default_options(arg)
|
|
@command_options[:listing] = arg.is_a?(String) ? arg : arg.inspect
|
|
@match = arg
|
|
end
|
|
@match ||= nil
|
|
end
|
|
|
|
# Define or get the command's description
|
|
def description(arg = nil)
|
|
@description = arg if arg
|
|
@description ||= nil
|
|
end
|
|
|
|
# Define or get the command's options
|
|
def command_options(arg = nil)
|
|
@command_options ||= default_options(match)
|
|
@command_options.merge!(arg) if arg
|
|
@command_options
|
|
end
|
|
# backward compatibility
|
|
alias options command_options
|
|
alias options= command_options=
|
|
|
|
# Define or get the command's banner
|
|
def banner(arg = nil)
|
|
@banner = arg if arg
|
|
@banner ||= description
|
|
end
|
|
|
|
def block
|
|
@block || instance_method(:process)
|
|
end
|
|
|
|
def source
|
|
file, line = block.source_location
|
|
strip_leading_whitespace(Pry::Code.from_file(file).expression_at(line))
|
|
end
|
|
|
|
def doc
|
|
new.help
|
|
end
|
|
|
|
def source_file
|
|
Array(block.source_location).first
|
|
end
|
|
alias file source_file
|
|
|
|
def source_line
|
|
Array(block.source_location).last
|
|
end
|
|
alias line source_line
|
|
|
|
def default_options(match)
|
|
{
|
|
keep_retval: false,
|
|
argument_required: false,
|
|
interpolate: true,
|
|
shellwords: true,
|
|
listing: (match.is_a?(String) ? match : match.inspect),
|
|
use_prefix: true,
|
|
takes_block: false
|
|
}
|
|
end
|
|
|
|
def name
|
|
super.to_s == "" ? "#<class(Pry::Command #{match.inspect})>" : super
|
|
end
|
|
|
|
def inspect
|
|
name
|
|
end
|
|
|
|
def command_name
|
|
options[:listing]
|
|
end
|
|
|
|
# Create a new command with the given properties.
|
|
# @param [String, Regex] match The thing that triggers this command
|
|
# @param [String] description The description to appear in `help`
|
|
# @param [Hash] options Behavioral options (see {Pry::CommandSet#command})
|
|
# @param [Module] helpers A module of helper functions to be included.
|
|
# @yield optional, used for BlockCommands
|
|
# @return [Class] (a subclass of {Pry::Command})
|
|
def subclass(match, description, options, helpers, &block)
|
|
klass = Class.new(self)
|
|
klass.send(:include, helpers)
|
|
klass.match = match
|
|
klass.description = description
|
|
klass.command_options = options
|
|
klass.block = block
|
|
klass
|
|
end
|
|
|
|
# Should this command be called for the given line?
|
|
# @param [String] val A line input at the REPL
|
|
# @return [Boolean]
|
|
def matches?(val)
|
|
command_regex =~ val
|
|
end
|
|
|
|
# How well does this command match the given line?
|
|
#
|
|
# Higher scores are better because they imply that this command matches
|
|
# the line more closely.
|
|
#
|
|
# The score is calculated by taking the number of characters at the start
|
|
# of the string that are used only to identify the command, not as part of
|
|
# the arguments.
|
|
#
|
|
# @example
|
|
# /\.(.*)/.match_score(".foo") #=> 1
|
|
# /\.*(.*)/.match_score("...foo") #=> 3
|
|
# 'hi'.match_score("hi there") #=> 2
|
|
#
|
|
# @param [String] val A line input at the REPL
|
|
# @return [Fixnum]
|
|
def match_score(val)
|
|
if command_regex =~ val
|
|
if Regexp.last_match.size > 1
|
|
Regexp.last_match.begin(1)
|
|
else
|
|
Regexp.last_match.end(0)
|
|
end
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
def command_regex
|
|
prefix = convert_to_regex(Pry.config.command_prefix)
|
|
prefix = "(?:#{prefix})?" unless options[:use_prefix]
|
|
|
|
/^#{prefix}#{convert_to_regex(match)}(?!\S)/
|
|
end
|
|
|
|
def convert_to_regex(obj)
|
|
case obj
|
|
when String
|
|
Regexp.escape(obj)
|
|
else
|
|
obj
|
|
end
|
|
end
|
|
|
|
# The group in which the command should be displayed in "help" output.
|
|
# This is usually auto-generated from directory naming, but it can be
|
|
# manually overridden if necessary.
|
|
# Group should not be changed once it is initialized.
|
|
def group(name = nil)
|
|
@group ||= begin
|
|
name || case Pry::Method(block).source_file
|
|
when %r{/pry/.*_commands/(.*).rb}
|
|
Regexp.last_match(1).capitalize.tr('_', " ")
|
|
when /(pry-\w+)-([\d\.]+([\w\.]+)?)/
|
|
name = Regexp.last_match(1)
|
|
version = Regexp.last_match(2)
|
|
"#{name} (v#{version})"
|
|
when /pryrc/
|
|
"pryrc"
|
|
else
|
|
"(other)"
|
|
end
|
|
end
|
|
end
|
|
|
|
def state
|
|
Pry::CommandState.default.state_for(match)
|
|
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 :hooks
|
|
attr_accessor :pry_instance
|
|
alias _pry_= pry_instance=
|
|
|
|
# The block we pass *into* a command so long as `:takes_block` is
|
|
# not equal to `false`
|
|
# @example
|
|
# my-command | do
|
|
# puts "block content"
|
|
# end
|
|
attr_accessor :command_block
|
|
|
|
# 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.hooks = context[:hooks]
|
|
self.pry_instance = context[:pry_instance]
|
|
end
|
|
|
|
# Make those properties accessible to instances
|
|
def name
|
|
self.class.name
|
|
end
|
|
|
|
def match
|
|
self.class.match
|
|
end
|
|
|
|
def description
|
|
self.class.description
|
|
end
|
|
|
|
def block
|
|
self.class.block
|
|
end
|
|
|
|
def command_options
|
|
self.class.options
|
|
end
|
|
|
|
def command_name
|
|
self.class.command_name
|
|
end
|
|
|
|
def source
|
|
self.class.source
|
|
end
|
|
|
|
# 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)
|
|
command_string = pry_instance.config.command_prefix.to_s + command_string
|
|
complete_string = "#{command_string} #{args.join(' ')}".rstrip
|
|
command_set.process_line(complete_string, context)
|
|
end
|
|
|
|
def commands
|
|
command_set.to_hash
|
|
end
|
|
|
|
def void
|
|
VOID_VALUE
|
|
end
|
|
|
|
def _pry_
|
|
Pry::Warning.warn('_pry_ is deprecated, use pry_instance instead')
|
|
pry_instance
|
|
end
|
|
|
|
# @return [Object] The value of `self` inside the `target` binding.
|
|
def target_self
|
|
target.eval('self')
|
|
end
|
|
|
|
# @return [Hash] Pry commands can store arbitrary state
|
|
# here. This state persists between subsequent command invocations.
|
|
# All state saved here is unique to the command, it does not
|
|
# need to be namespaced.
|
|
# @example
|
|
# state.my_state = "my state" # this will not conflict with any
|
|
# # `state.my_state` used in another command.
|
|
def state
|
|
self.class.state
|
|
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.
|
|
def check_for_command_collision(command_match, arg_string)
|
|
collision_type = target.eval("defined?(#{command_match})")
|
|
collision_type ||= 'local-variable' if arg_string =~ %r{\A\s*[-+*/%&|^]*=}
|
|
|
|
if collision_type
|
|
output.puts(
|
|
"#{Helpers::Text.bold('WARNING:')} Calling Pry command '#{command_match}', " \
|
|
"which conflicts with a #{collision_type}.\n\n"
|
|
)
|
|
end
|
|
rescue Pry::RescuableException # rubocop:disable Lint/HandleExceptions
|
|
end
|
|
|
|
# Extract necessary information from a line that Command.matches? this
|
|
# command.
|
|
#
|
|
# Returns an array of four elements:
|
|
#
|
|
# ```
|
|
# [String] the portion of the line that matched with the Command match
|
|
# [String] a string of all the arguments (i.e. everything but the match)
|
|
# [Array] the captures caught by the command_regex
|
|
# [Array] the arguments obtained by splitting the arg_string
|
|
# ```
|
|
#
|
|
# @param [String] val The line of input
|
|
# @return [Array]
|
|
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
|
|
unless Regexp.last_match
|
|
raise CommandError, "fatal: called a command which didn't match?!"
|
|
end
|
|
|
|
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?(" ")
|
|
|
|
# process and pass a block if one is found
|
|
pass_block(arg_string) if command_options[:takes_block]
|
|
|
|
args =
|
|
if arg_string
|
|
if command_options[:shellwords]
|
|
Shellwords.shellwords(arg_string)
|
|
else
|
|
arg_string.split(" ")
|
|
end
|
|
else
|
|
[]
|
|
end
|
|
|
|
[val[0..pos].rstrip, arg_string, captures, args]
|
|
end
|
|
|
|
# Process a line that Command.matches? this command.
|
|
# @param [String] line The line to process
|
|
# @return [Object, Command::VOID_VALUE]
|
|
def process_line(line)
|
|
command_match, arg_string, captures, args = tokenize(line)
|
|
|
|
if Pry.config.collision_warning
|
|
check_for_command_collision(command_match, arg_string)
|
|
end
|
|
|
|
self.arg_string = arg_string
|
|
self.captures = captures
|
|
|
|
call_safely(*(captures + args))
|
|
end
|
|
|
|
# Generate completions for this command
|
|
#
|
|
# @param [String] _search The line typed so far
|
|
# @return [Array<String>] Completion words
|
|
def complete(_search)
|
|
[]
|
|
end
|
|
|
|
private
|
|
|
|
# Run the command with the given `args`.
|
|
#
|
|
# This is a public wrapper around `#call` which ensures all preconditions
|
|
# are met.
|
|
#
|
|
# @param [Array<String>] args The arguments to pass to this command.
|
|
# @return [Object] The return value of the `#call` method, or
|
|
# {Command::VOID_VALUE}.
|
|
def call_safely(*args)
|
|
if command_options[:argument_required] && args.empty?
|
|
raise CommandError, "The command '#{command_name}' requires an argument."
|
|
end
|
|
|
|
ret = use_unpatched_symbol do
|
|
call_with_hooks(*args)
|
|
end
|
|
command_options[:keep_retval] ? ret : void
|
|
end
|
|
|
|
def use_unpatched_symbol
|
|
call_method = Symbol.method_defined?(:call) && Symbol.instance_method(:call)
|
|
Symbol.class_eval { undef :call } if call_method
|
|
yield
|
|
ensure
|
|
Symbol.instance_eval { define_method(:call, call_method) } if call_method
|
|
end
|
|
|
|
# Pass a block argument to a command.
|
|
# @param [String] arg_string The arguments (as a string) 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 `arg_string`.
|
|
def pass_block(arg_string)
|
|
# Workaround for weird JRuby bug where rindex in this case can return nil
|
|
# even when there's a match.
|
|
arg_string.scan(/\| *(?:do|\{)/)
|
|
block_index = $LAST_MATCH_INFO && $LAST_MATCH_INFO.offset(0)[0]
|
|
|
|
return unless block_index
|
|
|
|
block_init_string = arg_string.slice!(block_index..-1)[1..-1]
|
|
prime_string = "proc #{block_init_string}\n"
|
|
|
|
block_string =
|
|
if !Pry::Code.complete_expression?(prime_string)
|
|
pry_instance.r(target, prime_string)
|
|
else
|
|
prime_string
|
|
end
|
|
|
|
begin
|
|
self.command_block = target.eval(block_string)
|
|
rescue Pry::RescuableException
|
|
raise CommandError, "Incomplete block definition."
|
|
end
|
|
end
|
|
|
|
def find_hooks(event)
|
|
event_name = "#{event}_#{command_name}"
|
|
(hooks || Pry.hooks || self.class.hooks).get_hooks(event_name).values
|
|
end
|
|
|
|
def before_hooks
|
|
find_hooks('before')
|
|
end
|
|
|
|
def after_hooks
|
|
find_hooks('after')
|
|
end
|
|
|
|
# Run the `#call` method and all the registered hooks.
|
|
# @param [Array<String>] args The arguments to `#call`
|
|
# @return [Object] The return value from `#call`
|
|
def call_with_hooks(*args)
|
|
before_hooks.each { |block| instance_exec(*args, &block) }
|
|
|
|
ret = call(*args)
|
|
|
|
after_hooks.each do |block|
|
|
ret = instance_exec(*args, &block)
|
|
end
|
|
|
|
ret
|
|
end
|
|
|
|
# Normalize method arguments according to its arity.
|
|
#
|
|
# @param [Integer] method
|
|
# @param [Array] args
|
|
# @return [Array] a (possibly shorter) array of the arguments to pass
|
|
def normalize_method_args(method, args)
|
|
case method.arity
|
|
when -1
|
|
args
|
|
when 0
|
|
[]
|
|
else
|
|
args.values_at(*(0..(method.arity - 1)).to_a)
|
|
end
|
|
end
|
|
end
|
|
end
|