# frozen_string_literal: true class Pry class NoCommandError < StandardError def initialize(match, owner) super "Command '#{match}' not found in command set #{owner}" end end # This class is used to create sets of commands. Commands can be imported from # different sets, aliased, removed, etc. class CommandSet include Enumerable include Pry::Helpers::BaseHelpers attr_reader :helper_module # @param [Array] imported_sets # Sets which will be imported automatically # @yield Optional block run to define commands def initialize(*imported_sets, &block) @commands = {} @helper_module = Module.new import(*imported_sets) instance_eval(&block) if block end # Defines a new Pry command. # @param [String, Regexp] match The start of invocations of this command. # @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 [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-source. Necessary for commands with regex matches. # @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 # MyCommands = Pry::CommandSet.new do # command "greet", "Greet somebody" do |name| # puts "Good afternoon #{name.capitalize}!" # end # end # # # From pry: # # pry(main)> pry_instance.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_instance.commands = MyCommands # # pry(main)> number-10 john # # hello john, nice number: 10 # # pry(main)> help number # # number-N regex command def block_command(match, description = "No description.", options = {}, &block) if description.is_a?(Hash) options = description description = "No description." end options = Pry::Command.default_options(match).merge!(options) @commands[match] = Pry::BlockCommand.subclass( match, description, options, helper_module, &block ) end alias command block_command # Defines a new Pry command class. # # @param [String, Regexp] match The start of invocations of this command. # @param [String] description A description of the command. # @param [Hash] options The optional configuration parameters, see {#command} # @yield The class body's definition. # # @example # Pry::Commands.create_command "echo", "echo's the input", :shellwords => false do # def options(opt) # opt.banner "Usage: echo [-u | -d] " # 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 # if opts.present?(:u) && opts.present?(:d) # raise Pry::CommandError, "-u and -d makes no sense" # end # result = args.join(" ") # result.downcase! if opts.present?(:downcase) # result.upcase! if opts.present?(:upcase) # output.puts result # end # end # def create_command(match, description = "No description.", options = {}, &block) if description.is_a?(Hash) options = description description = "No description." end options = Pry::Command.default_options(match).merge!(options) @commands[match] = Pry::ClassCommand.subclass( match, description, options, helper_module, &block ) @commands[match].class_eval(&block) @commands[match] end def each(&block) @commands.each(&block) end # Removes some commands from the set # @param [Array] searches the matches or listings of the commands # to remove def delete(*searches) searches.each do |search| cmd = find_command_by_match_or_listing(search) @commands.delete cmd.match end end # Imports all the commands from one or more sets. # @param [Array] sets Command sets, all of the commands of which # will be imported. # @return [Pry::CommandSet] Returns the reciever (a command set). def import(*sets) sets.each do |set| @commands.merge! set.to_hash helper_module.send :include, set.helper_module end self end # Imports some commands from a set # @param [CommandSet] set Set to import commands from # @param [Array] matches Commands to import # @return [Pry::CommandSet] Returns the reciever (a command set). def import_from(set, *matches) helper_module.send :include, set.helper_module matches.each do |match| cmd = set.find_command_by_match_or_listing(match) @commands[cmd.match] = cmd end self end # @param [String, Regexp] match_or_listing The match or listing of a command. # of the command to retrieve. # @return [Command] The command object matched. def find_command_by_match_or_listing(match_or_listing) cmd = (@commands[match_or_listing] || Pry::Helpers::BaseHelpers.find_command(match_or_listing, @commands)) cmd || raise(ArgumentError, "cannot find a command: '#{match_or_listing}'") end # Aliases a command # @param [String, Regex] match The match of the alias (can be a regex). # @param [String] action The action to be performed (typically # another command). # @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 as `:desc` # @example Creating an alias for `ls -M` # Pry.config.commands.alias_command "lM", "ls -M" # @example Pass explicit description (overriding default). # Pry.config.commands.alias_command "lM", "ls -M", :desc => "cutiepie" def alias_command(match, action, options = {}) (cmd = find_command(action)) || raise("command: '#{action}' not found") original_options = cmd.options.dup options = original_options.merge!( desc: "Alias for `#{action}`", listing: match.is_a?(String) ? match : match.inspect ).merge!(options) # ensure default description is used if desc is nil desc = options.delete(:desc).to_s c = block_command match, desc, options do |*args| run action, *args end # TODO: untested. What's this about? c.class_eval do define_method(:complete) do |input| cmd.new(context).complete(input) end end c.group "Aliases" c end # Rename a command. Accepts either match or listing for the search. # # @param [String, Regexp] new_match The new match for the command. # @param [String, Regexp] search The command's current match or listing. # @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_match, search, options = {}) cmd = find_command_by_match_or_listing(search) options = { listing: new_match, description: cmd.description }.merge!(options) @commands[new_match] = cmd.dup @commands[new_match].match = new_match @commands[new_match].description = options.delete(:description) @commands[new_match].options.merge!(options) @commands.delete(cmd.match) end # Sets or gets the description for a command (replacing the old # description). Returns current description if no description # parameter provided. # @param [String, Regexp] search The command match. # @param [String?] description (nil) The command description. # @example Setting # MyCommands = Pry::CommandSet.new do # desc "help", "help description" # end # @example Getting # Pry.config.commands.desc "amend-line" def desc(search, description = nil) cmd = find_command_by_match_or_listing(search) return cmd.description unless description cmd.description = description end # @return [Array] # The list of commands provided by the command set. def list_commands @commands.keys end alias keys list_commands def to_hash @commands.dup end alias to_h to_hash # Find a command that matches the given line # @param [String] pattern The line that might be a command invocation # @return [Pry::Command, nil] def [](pattern) commands = @commands.values.select do |command| command.matches?(pattern) end commands.max_by { |command| command.match_score(pattern) } end alias find_command [] # # Re-assign the command found at _pattern_ with _command_. # # @param [Regexp, String] pattern # The command to add or replace(found at _pattern_). # # @param [Pry::Command] command # The command to add. # # @return [Pry::Command] # Returns the new command (matched with "pattern".) # # @example # Pry.config.commands["help"] = MyHelpCommand # def []=(pattern, command) if command.equal?(nil) @commands.delete(pattern) return end unless command.is_a?(Class) && command < Pry::Command raise TypeError, "command is not a subclass of Pry::Command" end bind_command_to_pattern = pattern != command.match if bind_command_to_pattern command_copy = command.dup command_copy.match = pattern @commands[pattern] = command_copy else @commands[pattern] = command end end # # Add a command to set. # # @param [Command] command # a subclass of Pry::Command. # def add_command(command) self[command.match] = command end # Find the command that the user might be trying to refer to. # @param [String] search The user's search. # @return [Pry::Command?] def find_command_for_help(search) find_command(search) || (begin find_command_by_match_or_listing(search) rescue ArgumentError nil end) end # Is the given line a command invocation? # @param [String] val # @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] val The line to execute # @param [Hash] context 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 # Generate completions for the user's search. # @param [String] search The line to search for # @param [Hash] context The context to create the command with # @return [Array] def complete(search, context = {}) if (command = find_command(search)) command.new(context).complete(search) else keys = @commands.keys.select do |key| key.is_a?(String) && key.start_with?(search) end keys.map { |key| key + " " } end end private # 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 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 = is_command @retval = 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