pry--pry/lib/pry/command_set.rb

448 lines
15 KiB
Ruby
Raw Normal View History

class Pry
class NoCommandError < StandardError
def initialize(name, owner)
super "Command '#{name}' not found in command set #{owner}"
end
end
2011-05-30 04:46:44 +00:00
# This class is used to create sets of commands. Commands can be imported from
# different sets, aliased, removed, etc.
class CommandSet
2011-05-30 10:56:50 +00:00
include Enumerable
include Pry::Helpers::BaseHelpers
attr_reader :commands
2011-04-30 10:28:58 +00:00
attr_reader :helper_module
# @param [Array<CommandSet>] imported_sets Sets which will be imported
# automatically
# @yield Optional block run to define commands
2011-05-07 05:32:05 +00:00
def initialize(*imported_sets, &block)
2011-04-30 10:28:58 +00:00
@commands = {}
@helper_module = Module.new
define_default_commands
import(*imported_sets)
instance_eval(&block) if block
end
# Defines a new Pry command.
# @param [String, Regexp] name The name of the command. Can be
# Regexp as well as String.
# @param [String] description A description of the command.
# @param [Hash] options The optional configuration parameters.
# @option options [Boolean] :keep_retval Whether or not to use return value
# of the block for return of `command` or just to return `nil`
# (the default).
# @option options [Array<String>] :requires_gem Whether the command has
# any gem dependencies, if it does and dependencies not met then
# command is disabled and a stub proc giving instructions to
# install command is provided.
# @option options [Boolean] :interpolate Whether string #{} based
# interpolation is applied to the command arguments before
# executing the command. Defaults to true.
# @option options [String] :listing The listing name of the
# command. That is the name by which the command is looked up by
# help and by show-command. Necessary for regex based commands.
# @option options [Boolean] :use_prefix Whether the command uses
# `Pry.config.command_prefix` prefix (if one is defined). Defaults
# to true.
# @option options [Boolean] :shellwords Whether the command's arguments
# should be split using Shellwords instead of just split on spaces.
# Defaults to true.
# @yield The action to perform. The parameters in the block
# determines the parameters the command will receive. All
# parameters passed into the block will be strings. Successive
# command parameters are separated by whitespace at the Pry prompt.
# @example
2011-05-07 05:32:05 +00:00
# MyCommands = Pry::CommandSet.new do
# command "greet", "Greet somebody" do |name|
# puts "Good afternoon #{name.capitalize}!"
# end
# end
#
# # From pry:
# # pry(main)> _pry_.commands = MyCommands
# # pry(main)> greet john
# # Good afternoon John!
# # pry(main)> help greet
# # Greet somebody
# @example Regexp command
# MyCommands = Pry::CommandSet.new do
# command /number-(\d+)/, "number-N regex command", :listing => "number" do |num, name|
# puts "hello #{name}, nice number: #{num}"
# end
# end
#
# # From pry:
# # pry(main)> _pry_.commands = MyCommands
# # pry(main)> number-10 john
# # hello john, nice number: 10
# # pry(main)> help number
# # number-N regex command
2012-01-08 20:48:54 +00:00
def block_command(name, description="No description.", options={}, &block)
description, options = ["No description.", description] if description.is_a?(Hash)
2011-12-31 14:41:01 +00:00
options = default_options(name).merge!(options)
commands[name] = Pry::BlockCommand.subclass(name, description, options, helper_module, &block)
2011-12-31 00:55:22 +00:00
end
2012-01-08 20:48:54 +00:00
alias_method :command, :block_command
2011-12-31 00:55:22 +00:00
2011-12-31 14:41:01 +00:00
# Defines a new Pry command class.
#
# @param [String, Regexp] name The name of the command. Can be
# Regexp as well as String.
# @param [String] description A description of the command.
# @param [Hash] options The optional configuration parameters, see {#command}
# @param &Block The class body's definition.
#
# @example
2012-01-08 21:09:45 +00:00
# Pry::Commands.create_command "echo", "echo's the input", :shellwords => false do
2011-12-31 14:41:01 +00:00
# def options(opt)
# opt.banner "Usage: echo [-u | -d] <string to echo>"
# opt.on :u, :upcase, "ensure the output is all upper-case"
# opt.on :d, :downcase, "ensure the output is all lower-case"
# end
#
# def process
# raise Pry::CommandError, "-u and -d makes no sense" if opts.present?(:u) && opts.present?(:d)
# result = args.join(" ")
# result.downcase! if opts.present?(:downcase)
# result.upcase! if opts.present?(:upcase)
# output.puts result
# end
# end
#
2012-01-08 21:09:45 +00:00
def create_command(name, description="No description.", options={}, &block)
description, options = ["No description.", description] if description.is_a?(Hash)
2011-12-31 14:41:01 +00:00
options = default_options(name).merge!(options)
2011-12-31 00:55:22 +00:00
commands[name] = Pry::ClassCommand.subclass(name, description, options, helper_module, &block)
2011-12-31 11:22:38 +00:00
commands[name].class_eval(&block)
commands[name]
end
# Execute a block of code before a command is invoked. The block also
# gets access to parameters that will be passed to the command and
# is evaluated in the same context.
# @param [String, Regexp] name The name of the command.
# @yield The block to be run before the command.
# @example Display parameter before invoking command
# Pry.commands.before_command("whereami") do |n|
# output.puts "parameter passed was #{n}"
# end
def before_command(name, &block)
cmd = find_command_by_name_or_listing(name)
2011-12-31 11:10:23 +00:00
cmd.hooks[:before].unshift block
end
# Execute a block of code after a command is invoked. The block also
# gets access to parameters that will be passed to the command and
# is evaluated in the same context.
# @param [String, Regexp] name The name of the command.
# @yield The block to be run after the command.
# @example Display text 'command complete' after invoking command
# Pry.commands.after_command("whereami") do |n|
# output.puts "command complete!"
# end
def after_command(name, &block)
cmd = find_command_by_name_or_listing(name)
2011-12-31 11:10:23 +00:00
cmd.hooks[:after] << block
end
2011-05-30 10:56:50 +00:00
def each &block
@commands.each(&block)
2011-05-30 10:56:50 +00:00
end
# Removes some commands from the set
# @param [Array<String>] names name of the commands to remove
def delete(*names)
names.each do |name|
cmd = find_command_by_name_or_listing(name)
commands.delete cmd.name
end
end
# Imports all the commands from one or more sets.
# @param [Array<CommandSet>] sets Command sets, all of the commands of which
# will be imported.
def import(*sets)
2011-04-30 10:28:58 +00:00
sets.each do |set|
commands.merge! set.commands
helper_module.send :include, set.helper_module
end
end
# Imports some commands from a set
# @param [CommandSet] set Set to import commands from
# @param [Array<String>] names Commands to import
def import_from(set, *names)
helper_module.send :include, set.helper_module
names.each do |name|
cmd = set.find_command_by_name_or_listing(name)
commands[cmd.name] = cmd
end
end
# @param [String, Regexp] name_or_listing The name or listing name
# of the command to retrieve.
# @return [Command] The command object matched.
def find_command_by_name_or_listing(name_or_listing)
if commands[name_or_listing]
cmd = commands[name_or_listing]
else
_, cmd = commands.find { |name, command| command.options[:listing] == name_or_listing }
end
raise ArgumentError, "Cannot find a command with name: '#{name_or_listing}'!" if !cmd
cmd
end
protected :find_command_by_name_or_listing
# Aliases a command
# @param [String] new_name New name of the command.
# @param [String] old_name Old name of the command.
2011-05-06 14:18:23 +00:00
# @param [String, nil] desc New description of the command.
def alias_command(new_name, old_name, desc="")
orig_command = find_command_by_name_or_listing(old_name)
commands[new_name] = orig_command.dup
commands[new_name].name = new_name
commands[new_name].description = desc
end
# Rename a command. Accepts either actual name or listing name for
# the `old_name`.
# `new_name` must be the actual name of the new command.
# @param [String, Regexp] new_name The new name for the command.
# @param [String, Regexp] old_name The command's current name.
# @param [Hash] options The optional configuration parameters,
# accepts the same as the `command` method, but also allows the
# command description to be passed this way too.
# @example Renaming the `ls` command and changing its description.
# Pry.config.commands.rename "dir", "ls", :description => "DOS friendly ls"
def rename_command(new_name, old_name, options={})
cmd = find_command_by_name_or_listing(old_name)
options = {
:listing => new_name,
:description => cmd.description
}.merge!(options)
commands[new_name] = cmd.dup
commands[new_name].name = new_name
commands[new_name].description = options.delete(:description)
commands[new_name].options.merge!(options)
commands.delete(cmd.name)
end
# Sets or gets the description for a command (replacing the old
# description). Returns current description if no description
# parameter provided.
# @param [String, Regexp] name The command name.
# @param [String] description The command description.
# @example Setting
2011-05-07 05:32:05 +00:00
# MyCommands = Pry::CommandSet.new do
# desc "help", "help description"
# end
# @example Getting
# Pry.config.commands.desc "amend-line"
def desc(name, description=nil)
cmd = find_command_by_name_or_listing(name)
return cmd.description if !description
cmd.description = description
end
2011-04-30 10:28:58 +00:00
# Defines helpers methods for this command sets.
# Those helpers are only defined in this command set.
#
# @yield A block defining helper methods
# @example
# helpers do
# def hello
# puts "Hello!"
# end
#
# include OtherModule
# end
def helpers(&block)
helper_module.class_eval(&block)
end
# @return [Array] The list of commands provided by the command set.
def list_commands
commands.keys
end
# Find a command that matches the given line
#
# @param [String] the line that may be a command invocation
# @return [Pry::Command, nil]
def find_command(val)
commands.values.select{ |c| c.matches?(val) }.sort_by{ |c| c.match_score(val) }.last
end
# Is the given line a command invocation?
#
# @param [String]
# @return [Boolean]
def valid_command?(val)
!!find_command(val)
end
# Process the given line to see whether it needs executing as a command.
#
# @param String the line to execute
# @param Hash the context to execute the commands with
# @return CommandSet::Result
#
def process_line(val, context={})
if command = find_command(val)
context = context.merge(:command_set => self)
retval = command.new(context).process_line(val)
Result.new(true, retval)
else
Result.new(false)
end
end
2011-12-31 14:41:01 +00:00
# @nodoc used for testing
2011-12-31 11:10:23 +00:00
def run_command(context, name, *args)
command = commands[name] or raise NoCommandError.new(name, self)
2011-12-31 11:22:38 +00:00
command.new(context).call_safely(*args)
2011-12-31 11:10:23 +00:00
end
private
2011-12-31 14:41:01 +00:00
def default_options(name)
{
:requires_gem => [],
:keep_retval => false,
:argument_required => false,
:interpolate => true,
:shellwords => true,
:listing => name,
:use_prefix => true
}
end
def define_default_commands
2012-01-15 07:31:58 +00:00
create_command "help" do |cmd|
description "Show a list of commands, or help for one command"
banner <<-BANNER
Usage: help [ COMMAND ]
With no arguments, help lists all the available commands in the current
command-set along with their description.
When given a command name as an argument, shows the help for that command.
BANNER
2012-01-15 07:31:58 +00:00
def process
if cmd = args.first
if command = find_command(cmd)
output.puts command.new.help
else
output.puts "No info for command: #{cmd}"
end
else
2012-02-20 07:08:16 +00:00
grouped = commands.values.group_by(&:group)
2012-01-15 07:31:58 +00:00
2012-02-20 07:08:16 +00:00
help_text = []
grouped.keys.sort.each do |key|
commands = grouped[key].select do |command|
command.description && !command.description.empty?
end.sort_by do |command|
command.options[:listing].to_s
2012-01-15 07:31:58 +00:00
end
2012-02-20 07:08:16 +00:00
unless commands.empty?
help_text << "#{text.bold(key)}"
2012-02-20 07:08:16 +00:00
help_text += commands.map do |command|
" #{command.options[:listing].to_s.ljust(18)} #{command.description}"
2012-02-20 07:08:16 +00:00
end
end
end
stagger_output(help_text.join("\n"))
end
end
end
create_command "install-command", "Install a disabled command." do |name|
banner <<-BANNER
Usage: install-command COMMAND
Installs the gems necessary to run the given COMMAND. You will generally not
need to run this unless told to by an error message.
BANNER
def process(name)
require 'rubygems/dependency_installer' unless defined? Gem::DependencyInstaller
command = find_command(name)
if command_dependencies_met?(command.options)
output.puts "Dependencies for #{command.name} are met. Nothing to do."
return
end
output.puts "Attempting to install `#{name}` command..."
gems_to_install = Array(command.options[:requires_gem])
gems_to_install.each do |g|
next if gem_installed?(g)
output.puts "Installing `#{g}` gem..."
begin
Gem::DependencyInstaller.new.install(g)
rescue Gem::GemNotFoundException
raise CommandError, "Required Gem: `#{g}` not found. Aborting command installation."
end
end
Gem.refresh
gems_to_install.each do |g|
begin
require g
rescue LoadError
raise CommandError, "Required Gem: `#{g}` installed but not found?!. Aborting command installation."
end
end
output.puts "Installation of `#{name}` successful! Type `help #{name}` for information"
end
end
end
end
# Wraps the return result of process_commands, indicates if the
# result IS a command and what kind of command (e.g void)
class Result
attr_reader :retval
def initialize(is_command, retval = nil)
@is_command, @retval = is_command, retval
end
# Is the result a command?
# @return [Boolean]
def command?
@is_command
end
# Is the result a command and if it is, is it a void command?
# (one that does not return a value)
# @return [Boolean]
def void_command?
retval == Command::VOID_VALUE
end
end
end