pry/lib/pry/command.rb

521 lines
15 KiB
Ruby

# frozen_string_literal: true
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