From fd4506f88c793802abd54fbd0db514398003f8fc Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 00:35:08 +0000 Subject: [PATCH 01/12] objectify existing commands --- lib/pry/command_processor.rb | 17 ++-- lib/pry/command_set.rb | 99 +++++++++++------------ lib/pry/default_commands/introspection.rb | 2 +- 3 files changed, 55 insertions(+), 63 deletions(-) diff --git a/lib/pry/command_processor.rb b/lib/pry/command_processor.rb index cf07b41d..36b8074d 100644 --- a/lib/pry/command_processor.rb +++ b/lib/pry/command_processor.rb @@ -168,21 +168,18 @@ class Pry def execute_command(target, command, options, *args) ret = nil - if command.callable.is_a?(Proc) - context = CommandContext.new - else - - # in the case of non-procs the callable *is* the context - context = command.callable - end - - # set some useful methods to be used by the action blocks + # allocate, setup and then call initialize so that authors + # can do their own setup in initialize. + context = command.allocate setup_context(target, command, context, options) + context.extend commands.helper_module + context.send(:initialize) catch(:command_done) do - ret = commands.run_command(context, command.name, *args) + ret = context.call(*args) end + # FIXME: wtf? options[:val].replace("") ret diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index e44c6889..bf465503 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -1,3 +1,4 @@ +require 'pry/command_context' class Pry class NoCommandError < StandardError def initialize(name, owner) @@ -8,27 +9,52 @@ class Pry # This class is used to create sets of commands. Commands can be imported from # different sets, aliased, removed, etc. class CommandSet - class Command < Struct.new(:name, :description, :options, :callable) + class Command < Pry::CommandContext - def call(context, *args) + class << self + attr_accessor :name + attr_accessor :description + attr_accessor :options + attr_accessor :block + end - if stub_block = options[:stub_info] - context.instance_eval(&stub_block) - else - if callable.is_a?(Proc) - ret = context.instance_exec(*correct_arg_arity(callable.arity, args), &callable) - else + %w(name description options block).each do |attribute| + define_method(attribute) { self.class.send(attribute) } + end - # in the case of non-procs the callable *is* the context - ret = callable.call(*correct_arg_arity(callable.method(:call).arity, args)) - end - - if options[:keep_retval] - ret - else - Pry::CommandContext::VOID_VALUE - end + class << self + def inspect + "#" end + + def subclass(name, description, options, &block) + klass = Class.new(self) + klass.name = name + klass.description = description + klass.options = options + klass.block = block + klass + end + end + end + + class StubCommand < Command + def call(*args) + gems_needed = Array(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." + end + end + + class BlockCommand < Command + def call(*args) + if options[:argument_required] && args.empty? + raise CommandError, "The command '#{command.name}' requires an argument." + end + + instance_exec(*correct_arg_arity(block.arity, args), &block) end private @@ -118,7 +144,6 @@ class Pry # # pry(main)> help number # # number-N regex command def command(name, description="No description.", options={}, &block) - options = { :requires_gem => [], :keep_retval => false, @@ -129,18 +154,11 @@ class Pry :use_prefix => true }.merge!(options) - unless command_dependencies_met? options - gems_needed = Array(options[:requires_gem]) - gems_not_installed = gems_needed.select { |g| !gem_installed?(g) } - - options[:stub_info] = proc do - 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." - end + if command_dependencies_met? options + commands[name] = BlockCommand.subclass(name, description, options, &block) + else + commands[name] = StubCommand.subclass(name, description, options) end - - commands[name] = Command.new(name, description, options, options[:definition] ? options.delete(:definition) : block) end # Execute a block of code before a command is invoked. The block also @@ -278,29 +296,6 @@ class Pry commands.delete(cmd.name) end - # Runs a command. - # @param [Object] context Object which will be used as self during the - # command. - # @param [String] name Name of the command to be run - # @param [Array] args Arguments passed to the command - # @raise [NoCommandError] If the command is not defined in this set - def run_command(context, command_name, *args) - command = commands[command_name] - - - context.extend helper_module - - if command.nil? - raise NoCommandError.new(command_name, self) - end - - if command.options[:argument_required] && args.empty? - puts "The command '#{command.name}' requires an argument." - else - command.call context, *args - end - end - # Sets or gets the description for a command (replacing the old # description). Returns current description if no description # parameter provided. diff --git a/lib/pry/default_commands/introspection.rb b/lib/pry/default_commands/introspection.rb index 2a5f5064..b84585bc 100644 --- a/lib/pry/default_commands/introspection.rb +++ b/lib/pry/default_commands/introspection.rb @@ -69,7 +69,7 @@ class Pry end if find_command(command_name) - block = Pry::Method.new(find_command(command_name).callable) + block = Pry::Method.new(find_command(command_name).block) next unless block.source set_file_and_dir_locals(block.source_file) From ddfeb867df425a196ddb6b3ca4209a822296ba32 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 00:55:22 +0000 Subject: [PATCH 02/12] Add a command_class --- lib/pry/command_set.rb | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index bf465503..a5d421ec 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -70,6 +70,33 @@ class Pry end end + class ClassCommand < Command + attr_accessor :opts + attr_accessor :args + + def call(*args) + self.opts = slop + self.args = self.opts.parse!(args) + + if opts.present?(:help) + output.puts slop.help + else + run + end + end + + def options(opt); end + + def slop + Slop.new do |opt| + options(opt) + opt.on(:h, :help, "Show this message.") + end + end + + def run; raise CommandError, "command '#{name}' not implemented" end + end + include Enumerable include Pry::Helpers::BaseHelpers @@ -161,6 +188,25 @@ class Pry end end + def command_class(name, description="No description.", options={}, &block) + options = { + :requires_gem => [], + :keep_retval => false, + :argument_required => false, + :interpolate => true, + :shellwords => true, + :listing => name, + :use_prefix => true + }.merge!(options) + + if command_dependencies_met? options + commands[name] = ClassCommand.subclass(name, description, options) + commands[name].class_eval(&block) + else + commands[name] = StubCommand.subclass(name, description, options) + end + 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. From d797a209d9fecf4b6219b5b9ae405748029a8bd9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 00:55:38 +0000 Subject: [PATCH 03/12] Refactor ls to be a class --- lib/pry/default_commands/ls.rb | 187 ++++++++++++++++----------------- 1 file changed, 93 insertions(+), 94 deletions(-) diff --git a/lib/pry/default_commands/ls.rb b/lib/pry/default_commands/ls.rb index 0409342a..b3259e57 100644 --- a/lib/pry/default_commands/ls.rb +++ b/lib/pry/default_commands/ls.rb @@ -3,7 +3,96 @@ class Pry Ls = Pry::CommandSet.new do - helpers do + command_class "ls","Show the list of vars and methods in the current scope. Type `ls --help` for more info.", + :shellwords => false, :interpolate => false do + + def options(opt) + opt.banner unindent <<-USAGE + Usage: ls [-m|-M|-p|-pM] [-q|-v] [-c|-i] [Object] + ls [-g] [-l] + + ls shows you which methods, constants and variables are accessible to Pry. By default it shows you the local variables defined in the current shell, and any public methods or instance variables defined on the current object. + + The colours used are configurable using Pry.config.ls.*_color, and the separator is Pry.config.ls.separator. + + Pry.config.ls.ceiling is used to hide methods defined higher up in the inheritance chain, this is by default set to [Object, Module, Class] so that methods defined on all Objects are omitted. The -v flag can be used to ignore this setting and show all methods, while the -q can be used to set the ceiling much lower and show only methods defined on the object or its direct class. + USAGE + + opt.on :m, "methods", "Show public methods defined on the Object (default)" + opt.on :M, "instance-methods", "Show methods defined in a Module or Class" + + opt.on :p, "ppp", "Show public, protected (in yellow) and private (in green) methods" + opt.on :q, "quiet", "Show only methods defined on object.singleton_class and object.class" + opt.on :v, "verbose", "Show methods and constants on all super-classes (ignores Pry.config.ls.ceiling)" + + opt.on :g, "globals", "Show global variables, including those builtin to Ruby (in cyan)" + opt.on :l, "locals", "Show locals, including those provided by Pry (in red)" + + opt.on :c, "constants", "Show constants, highlighting classes (in blue), and exceptions (in purple)" + + opt.on :i, "ivars", "Show instance variables (in blue) and class variables (in bright blue)" + + opt.on :G, "grep", "Filter output by regular expression", :optional => false + + opt.on :h, "help", "Show help" + end + + def run + obj = args.empty? ? target_self : target.eval(args.join(" ")) + + # exclude -q, -v and --grep because they don't specify what the user wants to see. + has_opts = (opts.present?(:methods) || opts.present?(:'instance-methods') || opts.present?(:ppp) || + opts.present?(:globals) || opts.present?(:locals) || opts.present?(:constants) || + opts.present?(:ivars)) + + show_methods = opts.present?(:methods) || opts.present?(:'instance-methods') || opts.present?(:ppp) || !has_opts + show_constants = opts.present?(:constants) || (!has_opts && Module === obj) + show_ivars = opts.present?(:ivars) || !has_opts + show_locals = opts.present?(:locals) || (!has_opts && args.empty?) + + grep_regex, grep = [Regexp.new(opts[:G] || "."), lambda{ |x| x.grep(grep_regex) }] + + raise Pry::CommandError, "-l does not make sense with a specified Object" if opts.present?(:locals) && !args.empty? + raise Pry::CommandError, "-g does not make sense with a specified Object" if opts.present?(:globals) && !args.empty? + raise Pry::CommandError, "-q does not make sense with -v" if opts.present?(:quiet) && opts.present?(:verbose) + raise Pry::CommandError, "-M only makes sense with a Module or a Class" if opts.present?(:'instance-methods') && !(Module === obj) + raise Pry::CommandError, "-c only makes sense with a Module or a Class" if opts.present?(:constants) && !args.empty? && !(Module === obj) + + + if opts.present?(:globals) + output_section("global variables", grep[format_globals(target.eval("global_variables"))]) + end + + if show_constants + mod = Module === obj ? obj : Object + constants = mod.constants + constants -= (mod.ancestors - [mod]).map(&:constants).flatten unless opts.present?(:verbose) + output_section("constants", grep[format_constants(mod, constants)]) + end + + if show_methods + # methods is a hash {Module/Class => [Pry::Methods]} + methods = all_methods(obj).select{ |method| opts.present?(:ppp) || method.visibility == :public }.group_by(&:owner) + + # reverse the resolution order so that the most useful information appears right by the prompt + resolution_order(obj).take_while(&below_ceiling(obj)).reverse.each do |klass| + methods_here = format_methods((methods[klass] || []).select{ |m| m.name =~ grep_regex }) + output_section "#{Pry::WrappedModule.new(klass).method_prefix}methods", methods_here + end + end + + if show_ivars + klass = (Module === obj ? obj : obj.class) + output_section("instance variables", format_variables(:instance_var, grep[obj.__send__(:instance_variables)])) + output_section("class variables", format_variables(:class_var, grep[klass.__send__(:class_variables)])) + end + + if show_locals + output_section("locals", format_locals(grep[target.eval("local_variables")])) + end + end + + private # http://ruby.runpaint.org/globals, and running "puts global_variables.inspect". BUILTIN_GLOBALS = %w($" $$ $* $, $-0 $-F $-I $-K $-W $-a $-d $-i $-l $-p $-v $-w $. $/ $\\ @@ -20,17 +109,17 @@ class Pry $LAST_PAREN_MATCH $LAST_READ_LINE $MATCH $POSTMATCH $PREMATCH) # Get all the methods that we'll want to output - def all_methods(obj, opts) + def all_methods(obj) opts.present?(:'instance-methods') ? Pry::Method.all_from_class(obj) : Pry::Method.all_from_obj(obj) end - def resolution_order(obj, opts) + def resolution_order(obj) opts.present?(:'instance-methods') ? Pry::Method.instance_resolution_order(obj) : Pry::Method.resolution_order(obj) end # Get a lambda that can be used with .take_while to prevent over-eager # traversal of the Object's ancestry graph. - def below_ceiling(obj, opts) + def below_ceiling(obj) ceiling = if opts.present?(:quiet) [opts.present?(:'instance-methods') ? obj.ancestors[1] : obj.class.ancestors[1]] + Pry.config.ls.ceiling elsif opts.present?(:verbose) @@ -108,96 +197,6 @@ class Pry text.send(Pry.config.ls.send(:"#{type}_color"), str) end end - - command "ls", "Show the list of vars and methods in the current scope. Type `ls --help` for more info.", - :shellwords => false, :interpolate => false do |*args| - - opts = Slop.parse!(args, :strict => true) do |opt| - opt.banner unindent <<-USAGE - Usage: ls [-m|-M|-p|-pM] [-q|-v] [-c|-i] [Object] - ls [-g] [-l] - - ls shows you which methods, constants and variables are accessible to Pry. By default it shows you the local variables defined in the current shell, and any public methods or instance variables defined on the current object. - - The colours used are configurable using Pry.config.ls.*_color, and the separator is Pry.config.ls.separator. - - Pry.config.ls.ceiling is used to hide methods defined higher up in the inheritance chain, this is by default set to [Object, Module, Class] so that methods defined on all Objects are omitted. The -v flag can be used to ignore this setting and show all methods, while the -q can be used to set the ceiling much lower and show only methods defined on the object or its direct class. - USAGE - - opt.on :m, "methods", "Show public methods defined on the Object (default)" - opt.on :M, "instance-methods", "Show methods defined in a Module or Class" - - opt.on :p, "ppp", "Show public, protected (in yellow) and private (in green) methods" - opt.on :q, "quiet", "Show only methods defined on object.singleton_class and object.class" - opt.on :v, "verbose", "Show methods and constants on all super-classes (ignores Pry.config.ls.ceiling)" - - opt.on :g, "globals", "Show global variables, including those builtin to Ruby (in cyan)" - opt.on :l, "locals", "Show locals, including those provided by Pry (in red)" - - opt.on :c, "constants", "Show constants, highlighting classes (in blue), and exceptions (in purple)" - - opt.on :i, "ivars", "Show instance variables (in blue) and class variables (in bright blue)" - - opt.on :G, "grep", "Filter output by regular expression", :optional => false - - opt.on :h, "help", "Show help" - end - - next output.puts(opts.to_s) if opts.present?(:help) - - obj = args.empty? ? target_self : target.eval(args.join(" ")) - - # exclude -q, -v and --grep because they don't specify what the user wants to see. - has_opts = (opts.present?(:methods) || opts.present?(:'instance-methods') || opts.present?(:ppp) || - opts.present?(:globals) || opts.present?(:locals) || opts.present?(:constants) || - opts.present?(:ivars)) - - show_methods = opts.present?(:methods) || opts.present?(:'instance-methods') || opts.present?(:ppp) || !has_opts - show_constants = opts.present?(:constants) || (!has_opts && Module === obj) - show_ivars = opts.present?(:ivars) || !has_opts - show_locals = opts.present?(:locals) || (!has_opts && args.empty?) - - grep_regex, grep = [Regexp.new(opts[:G] || "."), lambda{ |x| x.grep(grep_regex) }] - - raise Pry::CommandError, "-l does not make sense with a specified Object" if opts.present?(:locals) && !args.empty? - raise Pry::CommandError, "-g does not make sense with a specified Object" if opts.present?(:globals) && !args.empty? - raise Pry::CommandError, "-q does not make sense with -v" if opts.present?(:quiet) && opts.present?(:verbose) - raise Pry::CommandError, "-M only makes sense with a Module or a Class" if opts.present?(:'instance-methods') && !(Module === obj) - raise Pry::CommandError, "-c only makes sense with a Module or a Class" if opts.present?(:constants) && !args.empty? && !(Module === obj) - - - if opts.present?(:globals) - output_section("global variables", grep[format_globals(target.eval("global_variables"))]) - end - - if show_constants - mod = Module === obj ? obj : Object - constants = mod.constants - constants -= (mod.ancestors - [mod]).map(&:constants).flatten unless opts.present?(:verbose) - output_section("constants", grep[format_constants(mod, constants)]) - end - - if show_methods - # methods is a hash {Module/Class => [Pry::Methods]} - methods = all_methods(obj, opts).select{ |method| opts.present?(:ppp) || method.visibility == :public }.group_by(&:owner) - - # reverse the resolution order so that the most useful information appears right by the prompt - resolution_order(obj, opts).take_while(&below_ceiling(obj, opts)).reverse.each do |klass| - methods_here = format_methods((methods[klass] || []).select{ |m| m.name =~ grep_regex }) - output_section "#{Pry::WrappedModule.new(klass).method_prefix}methods", methods_here - end - end - - if show_ivars - klass = (Module === obj ? obj : obj.class) - output_section("instance variables", format_variables(:instance_var, grep[obj.__send__(:instance_variables)])) - output_section("class variables", format_variables(:class_var, grep[klass.__send__(:class_variables)])) - end - - if show_locals - output_section("locals", format_locals(grep[target.eval("local_variables")])) - end - end end end end From 689e47899b2b4ded3cd4899dde125a79bec337ff Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 00:58:24 +0000 Subject: [PATCH 04/12] Refactor gist to new command-class API --- lib/pry/default_commands/documentation.rb | 264 +++++++++++----------- 1 file changed, 127 insertions(+), 137 deletions(-) diff --git a/lib/pry/default_commands/documentation.rb b/lib/pry/default_commands/documentation.rb index 9c6b3258..a96c1359 100644 --- a/lib/pry/default_commands/documentation.rb +++ b/lib/pry/default_commands/documentation.rb @@ -57,151 +57,141 @@ class Pry require 'pry/command_context' - command("gist", "Gist a method or expression history to github. Type `gist --help` for more info.",{:requires_gem => "gist", :shellwords => false, :definition => Pry::CommandContext.new{ + command_class "gist", "Gist a method or expression history to github. Type `gist --help` for more info.", :requires_gem => "gist", :shellwords => false do + attr_accessor :content + attr_accessor :code_type + attr_accessor :input_ranges - class << self - attr_accessor :opts - attr_accessor :content - attr_accessor :code_type - attr_accessor :input_ranges - end + def initialize + require 'gist' + end - def call(*args) - require 'gist' + def options(opt) + opt.banner unindent <<-USAGE + Usage: gist [OPTIONS] [METH] + Gist method (doc or source) or input expression to github. + Ensure the `gist` gem is properly working before use. http://github.com/defunkt/gist for instructions. + e.g: gist -m my_method + e.g: gist -d my_method + e.g: gist -i 1..10 +USAGE - gather_options(args) - process_options - perform_gist - end + opt.on :d, :doc, "Gist a method's documentation.", true + opt.on :m, :method, "Gist a method's source.", true + opt.on :f, :file, "Gist a file.", true + opt.on :p, :public, "Create a public gist (default: false)", :default => false + opt.on :l, :lines, "Only gist a subset of lines (only works with -m and -f)", :optional => true, :as => Range, :default => 1..-1 + opt.on :i, :in, "Gist entries from Pry's input expression history. Takes an index or range.", :optional => true, + :as => Range, :default => -5..-1 do |range| + input_ranges << absolute_index_range(range, _pry_.input_array.length) + end + end - def gather_options(args) - self.input_ranges = [] + def run + if opts.present?(:in) + in_option + elsif opts.present?(:file) + file_option + elsif opts.present?(:doc) + doc_option + elsif opts.present?(:method) + method_option + end - self.opts = parse_options!(args) do |opt| - opt.banner unindent <<-USAGE - Usage: gist [OPTIONS] [METH] - Gist method (doc or source) or input expression to github. - Ensure the `gist` gem is properly working before use. http://github.com/defunkt/gist for instructions. - e.g: gist -m my_method - e.g: gist -d my_method - e.g: gist -i 1..10 - USAGE + perform_gist + end - opt.on :d, :doc, "Gist a method's documentation.", true - opt.on :m, :method, "Gist a method's source.", true - opt.on :f, :file, "Gist a file.", true - opt.on :p, :public, "Create a public gist (default: false)", :default => false - opt.on :l, :lines, "Only gist a subset of lines (only works with -m and -f)", :optional => true, :as => Range, :default => 1..-1 - opt.on :i, :in, "Gist entries from Pry's input expression history. Takes an index or range.", :optional => true, - :as => Range, :default => -5..-1 do |range| - input_ranges << absolute_index_range(range, _pry_.input_array.length) - end - end - end + def in_option + self.code_type = :ruby + self.content = "" - def process_options - if opts.present?(:in) - in_option - elsif opts.present?(:file) - file_option - elsif opts.present?(:doc) - doc_option - elsif opts.present?(:method) - method_option - end - end - - def in_option - self.code_type = :ruby - self.content = "" - - input_ranges.each do |range| - input_expressions = _pry_.input_array[range] || [] - input_expressions.each_with_index.map do |code, index| - corrected_index = index + range.first - if code && code != "" - self.content << code - if code !~ /;\Z/ - self.content << "#{comment_expression_result_for_gist(Pry.config.gist.inspecter.call(_pry_.output_array[corrected_index]))}" - end - end - end - end - end - - def file_option - whole_file = File.read(File.expand_path(opts[:f])) - if opts.present?(:lines) - self.content = restrict_to_lines(whole_file, opts[:l]) - else - self.content = whole_file - end - end - - def doc_option - meth = get_method_or_raise(opts[:d], target, {}) - self.content = meth.doc - self.code_type = meth.source_type - - text.no_color do - self.content = process_comment_markup(self.content, self.code_type) - end - self.code_type = :plain - end - - def method_option - meth = get_method_or_raise(opts[:m], target, {}) - method_source = meth.source - if opts.present?(:lines) - self.content = restrict_to_lines(method_source, opts[:l]) - else - self.content = method_source - end - - self.code_type = meth.source_type - end - - def perform_gist - type_map = { :ruby => "rb", :c => "c", :plain => "plain" } - - # prevent Gist from exiting the session on error - begin - extname = opts.present?(:file) ? ".#{gist_file_extension(opts[:f])}" : ".#{type_map[self.code_type]}" - - link = Gist.write([:extension => extname, - :input => self.content], - !opts[:p]) - rescue SystemExit - end - - if link - Gist.copy(link) - output.puts "Gist created at #{link} and added to clipboard." - end - end - - def restrict_to_lines(content, lines) - line_range = one_index_range(lines) - content.lines.to_a[line_range].join - end - - def gist_file_extension(file_name) - file_name.split(".").last - end - - def comment_expression_result_for_gist(result) - content = "" - result.lines.each_with_index do |line, index| - if index == 0 - content << "# => #{line}" - else - content << "# #{line}" - end - end - content - end - }}) + input_ranges.each do |range| + input_expressions = _pry_.input_array[range] || [] + input_expressions.each_with_index.map do |code, index| + corrected_index = index + range.first + if code && code != "" + self.content << code + if code !~ /;\Z/ + self.content << "#{comment_expression_result_for_gist(Pry.config.gist.inspecter.call(_pry_.output_array[corrected_index]))}" end end end + end + end + + def file_option + whole_file = File.read(File.expand_path(opts[:f])) + if opts.present?(:lines) + self.content = restrict_to_lines(whole_file, opts[:l]) + else + self.content = whole_file + end + end + + def doc_option + meth = get_method_or_raise(opts[:d], target, {}) + self.content = meth.doc + self.code_type = meth.source_type + + text.no_color do + self.content = process_comment_markup(self.content, self.code_type) + end + self.code_type = :plain + end + + def method_option + meth = get_method_or_raise(opts[:m], target, {}) + method_source = meth.source + if opts.present?(:lines) + self.content = restrict_to_lines(method_source, opts[:l]) + else + self.content = method_source + end + + self.code_type = meth.source_type + end + + def perform_gist + type_map = { :ruby => "rb", :c => "c", :plain => "plain" } + + # prevent Gist from exiting the session on error + begin + extname = opts.present?(:file) ? ".#{gist_file_extension(opts[:f])}" : ".#{type_map[self.code_type]}" + + link = Gist.write([:extension => extname, + :input => self.content], + !opts[:p]) + rescue SystemExit + end + + if link + Gist.copy(link) + output.puts "Gist created at #{link} and added to clipboard." + end + end + + def restrict_to_lines(content, lines) + line_range = one_index_range(lines) + content.lines.to_a[line_range].join + end + + def gist_file_extension(file_name) + file_name.split(".").last + end + + def comment_expression_result_for_gist(result) + content = "" + result.lines.each_with_index do |line, index| + if index == 0 + content << "# => #{line}" + else + content << "# #{line}" + end + end + content + end + end + end + end +end From 2b6d0d2149b6e0123fbaf0933404bd750895c4eb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 11:10:23 +0000 Subject: [PATCH 05/12] Make all the tests pass again --- lib/pry/command_processor.rb | 40 ++----- lib/pry/command_set.rb | 86 +++++++++------ test/test_class_based_commands.rb | 61 ----------- test/test_command_set.rb | 176 +++--------------------------- test/test_pry.rb | 2 +- 5 files changed, 85 insertions(+), 280 deletions(-) delete mode 100644 test/test_class_based_commands.rb diff --git a/lib/pry/command_processor.rb b/lib/pry/command_processor.rb index 36b8074d..72369b5e 100644 --- a/lib/pry/command_processor.rb +++ b/lib/pry/command_processor.rb @@ -145,15 +145,20 @@ class Pry args = [] end - options = { + context = { :val => val, :arg_string => arg_string, :eval_string => eval_string, :commands => commands.commands, - :captures => captures + :captures => captures, + :pry_instance => @pry_instance, + :output => output, + :command_processor => self, + :command_set => commands, + :target => target } - ret = execute_command(target, command, options, *(captures + args)) + ret = execute_command(command, context, *(captures + args)) Result.new(true, command.options[:keep_retval], ret) end @@ -165,40 +170,19 @@ class Pry # @param [Hash] options The options to set on the Commands object. # @param [Array] args The command arguments. # @return [Object] The value returned by the command - def execute_command(target, command, options, *args) + def execute_command(command, context, *args) ret = nil - # allocate, setup and then call initialize so that authors - # can do their own setup in initialize. - context = command.allocate - setup_context(target, command, context, options) - context.extend commands.helper_module - context.send(:initialize) + instance = command.new(context) catch(:command_done) do - ret = context.call(*args) + ret = instance.call_with_hooks(*args) end # FIXME: wtf? - options[:val].replace("") + context[:val].replace("") ret end - - def setup_context(target, command, context, options) - context.opts = options - context.target = target - context.target_self = target.eval('self') - context.output = output - context.captures = options[:captures] - context.eval_string = options[:eval_string] - context.arg_string = options[:arg_string] - context.command_set = commands - context.command_name = command.options[:listing] - - context._pry_ = @pry_instance - - context.command_processor = self - end end end diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index a5d421ec..bd23889b 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -18,23 +18,58 @@ class Pry attr_accessor :block end + attr_accessor :context + %w(name description options block).each do |attribute| define_method(attribute) { self.class.send(attribute) } end class << self def inspect - "#" + "#" end - def subclass(name, description, options, &block) + def subclass(name, description, options, helpers, &block) klass = Class.new(self) + klass.send(:include, helpers) klass.name = name klass.description = description klass.options = options klass.block = block klass end + + def hooks + @hooks ||= {:before => [], :after => []} + end + end + + def initialize(context) + self.context = context + self.target = context[:target] + self.target_self = context[:target].eval('self') + self.output = context[:output] + self.captures = context[:captures] + self.eval_string = context[:eval_string] + self.arg_string = context[:arg_string] + self.command_set = context[:command_set] + self.command_name = self.class.options[:listing] + self._pry_ = context[:pry_instance] + self.command_processor = context[:command_processor] + end + + 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 + + self.class.options[:keep_retval] ? ret : CommandContext::VOID_VALUE end end @@ -49,6 +84,9 @@ class Pry end class BlockCommand < Command + # backwards compatibility + alias_method :opts, :context + def call(*args) if options[:argument_required] && args.empty? raise CommandError, "The command '#{command.name}' requires an argument." @@ -75,6 +113,8 @@ class Pry attr_accessor :args def call(*args) + setup + self.opts = slop self.args = self.opts.parse!(args) @@ -85,7 +125,9 @@ class Pry end end + def setup; end def options(opt); end + def run; raise CommandError, "command '#{name}' not implemented" end def slop Slop.new do |opt| @@ -94,7 +136,6 @@ class Pry end end - def run; raise CommandError, "command '#{name}' not implemented" end end include Enumerable @@ -182,9 +223,9 @@ class Pry }.merge!(options) if command_dependencies_met? options - commands[name] = BlockCommand.subclass(name, description, options, &block) + commands[name] = BlockCommand.subclass(name, description, options, helper_module, &block) else - commands[name] = StubCommand.subclass(name, description, options) + commands[name] = StubCommand.subclass(name, description, options, helper_module) end end @@ -200,10 +241,10 @@ class Pry }.merge!(options) if command_dependencies_met? options - commands[name] = ClassCommand.subclass(name, description, options) + commands[name] = ClassCommand.subclass(name, description, options, helper_module) commands[name].class_eval(&block) else - commands[name] = StubCommand.subclass(name, description, options) + commands[name] = StubCommand.subclass(name, description, options, helper_module) end end @@ -218,18 +259,7 @@ class Pry # end def before_command(name, &block) cmd = find_command_by_name_or_listing(name) - prev_callable = cmd.callable - - wrapper_block = proc do |*args| - instance_exec(*args, &block) - - if prev_callable.is_a?(Proc) - instance_exec(*args, &prev_callable) - else - prev_callable.call(*args) - end - end - cmd.callable = wrapper_block + cmd.hooks[:before].unshift block end # Execute a block of code after a command is invoked. The block also @@ -243,18 +273,7 @@ class Pry # end def after_command(name, &block) cmd = find_command_by_name_or_listing(name) - prev_callable = cmd.callable - - wrapper_block = proc do |*args| - if prev_callable.is_a?(Proc) - instance_exec(*args, &prev_callable) - else - prev_callable.call(*args) - end - - instance_exec(*args, &block) - end - cmd.callable = wrapper_block + cmd.hooks[:after] << block end def each &block @@ -382,6 +401,11 @@ class Pry commands.keys end + def run_command(context, name, *args) + command = commands[name] or raise NoCommandError.new(name, self) + command.new(context).call_with_hooks(*args) + end + private def define_default_commands diff --git a/test/test_class_based_commands.rb b/test/test_class_based_commands.rb deleted file mode 100644 index b93b6b91..00000000 --- a/test/test_class_based_commands.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'helper' - -# integration tests -describe "integration tests for class-based commands" do - before do - @set = Pry::CommandSet.new - end - - it 'should invoke a class-based command from the REPL' do - c = Class.new(Pry::CommandContext) do - def call - output.puts "yippee!" - end - end - - @set.command 'foo', "desc", :definition => c.new - @set.import_from Pry::Commands, "exit-all" - - redirect_pry_io(InputTester.new("foo", "exit-all"), out =StringIO.new) do - Pry.start binding, :commands => @set - end - - out.string.should =~ /yippee!/ - end - - it 'should return specified value with :keep_retval => true' do - c = Class.new(Pry::CommandContext) do - def call - :i_enjoyed_the_song_new_flame_by_simply_red_as_a_child_wandering_around_supermarkets - end - end - - @set.command 'foo', "desc", :keep_retval => true, :definition => c.new - @set.import_from Pry::Commands, "exit-all" - - redirect_pry_io(InputTester.new("foo", "exit-all"), out =StringIO.new) do - Pry.start binding, :commands => @set - end - - out.string.should =~ /i_enjoyed_the_song_new_flame_by_simply_red_as_a_child_wandering_around_supermarkets/ - end - - it 'should NOT return specified value with :keep_retval => false' do - c = Class.new(Pry::CommandContext) do - def call - :i_enjoyed_the_song_new_flame_by_simply_red_as_a_child_wandering_around_supermarkets - end - end - - @set.command 'foo', "desc", :keep_retval => false, :definition => c.new - @set.import_from Pry::Commands, "exit-all" - - redirect_pry_io(InputTester.new("foo", "exit-all"), out =StringIO.new) do - Pry.start binding, :commands => @set - end - - out.string.should.not =~ /i_enjoyed_the_song_new_flame_by_simply_red_as_a_child_wandering_around_supermarkets/ - end - - -end diff --git a/test/test_command_set.rb b/test/test_command_set.rb index 70f517a3..1a03e1d9 100644 --- a/test/test_command_set.rb +++ b/test/test_command_set.rb @@ -3,7 +3,10 @@ require 'helper' describe Pry::CommandSet do before do @set = Pry::CommandSet.new - @ctx = Pry::CommandContext.new + @ctx = { + :target => binding, + :command_set => @set + } end it 'should call the block used for the command when it is called' do @@ -24,11 +27,11 @@ describe Pry::CommandSet do @set.run_command @ctx, 'foo', 1, 2, 3 end - it 'should use the first argument as self' do + it 'should use the first argument as context' do ctx = @ctx @set.command 'foo' do - self.should == ctx + self.context.should == ctx end @set.run_command @ctx, 'foo' @@ -241,8 +244,8 @@ describe Pry::CommandSet do end it "should provide a 'help' command" do - @ctx.command_set = @set - @ctx.output = StringIO.new + @ctx[:command_set] = @set + @ctx[:output] = StringIO.new lambda { @set.run_command(@ctx, 'help') @@ -255,12 +258,12 @@ describe Pry::CommandSet do @set.command 'moo', "Mooerizes" do; end @set.command 'boo', "Booerizes" do; end - @ctx.command_set = @set - @ctx.output = StringIO.new + @ctx[:command_set] = @set + @ctx[:output] = StringIO.new @set.run_command(@ctx, 'help') - doc = @ctx.output.string + doc = @ctx[:output].string order = [doc.index("boo"), doc.index("foo"), @@ -331,15 +334,15 @@ describe Pry::CommandSet do end it 'should share the context with the original command' do - @ctx.target = "test target string" + @ctx[:target] = "test target string".__binding__ before_val = nil orig_val = nil @set.command('foo') { orig_val = target } @set.before_command('foo') { before_val = target } @set.run_command(@ctx, 'foo') - before_val.should == @ctx.target - orig_val.should == @ctx.target + before_val.should == @ctx[:target] + orig_val.should == @ctx[:target] end it 'should work when applied multiple times' do @@ -375,15 +378,15 @@ describe Pry::CommandSet do end it 'should share the context with the original command' do - @ctx.target = "test target string" + @ctx[:target] = "test target string".__binding__ after_val = nil orig_val = nil @set.command('foo') { orig_val = target } @set.after_command('foo') { after_val = target } @set.run_command(@ctx, 'foo') - after_val.should == @ctx.target - orig_val.should == @ctx.target + after_val.should == @ctx[:target] + orig_val.should == @ctx[:target] end it 'should determine the return value for the command' do @@ -418,149 +421,4 @@ describe Pry::CommandSet do end end - - describe "class-based commands" do - it 'should pass arguments to the command' do - c = Class.new(Pry::CommandContext) do - def call(*args) - args.should == [1, 2, 3] - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.run_command ctx, 'foo', 1, 2, 3 - end - - it 'should set unprovided arguments to nil' do - c = Class.new(Pry::CommandContext) do - def call(x, y, z) - x.should == 1 - y.should == nil - z.should == nil - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.run_command ctx, 'foo', 1 - end - - it 'should clip provided arguments to expected number' do - c = Class.new(Pry::CommandContext) do - def call(x, y, z) - x.should == 1 - y.should == 2 - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.run_command ctx, 'foo', 1, 2, 3, 4 - end - - it 'should return Pry::CommandContext::VOID by default' do - c = Class.new(Pry::CommandContext) do - def call - :i_have_done_thing_i_regret - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.run_command(ctx, 'foo').should == Pry::CommandContext::VOID_VALUE - end - - it 'should return specific value when :keep_retval => true' do - c = Class.new(Pry::CommandContext) do - def call - :i_have_a_dog_called_tobina - end - end - - @set.command 'foo', "desc", :keep_retval => true, :definition => c.new - - ctx = @set.commands['foo'].callable - @set.run_command(ctx, 'foo').should == :i_have_a_dog_called_tobina - end - - it 'should have access to helper methods' do - c = Class.new(Pry::CommandContext) do - def call - im_helping.should == "butterbum" - end - end - - @set.command 'foo', "desc", :definition => c.new - - @set.helpers do - def im_helping - "butterbum" - end - end - - ctx = @set.commands['foo'].callable - @set.run_command ctx, 'foo' - end - - it 'should persist state' do - c = Class.new(Pry::CommandContext) do - attr_accessor :state - def call - @state ||= 0 - @state += 1 - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.run_command ctx, 'foo' - @set.run_command ctx, 'foo' - ctx.state.should == 2 - end - - describe "before_command" do - it 'should be called before the original command' do - foo = [] - c = Class.new(Pry::CommandContext) do - define_method(:call) do - foo << 1 - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.before_command('foo') { foo << 2 } - @set.run_command(ctx, 'foo') - - foo.should == [2, 1] - end - end - - describe "after_command" do - it 'should be called before the original command' do - foo = [] - c = Class.new(Pry::CommandContext) do - define_method(:call) do - foo << 1 - end - end - - @set.command 'foo', "desc", :definition => c.new - - ctx = @set.commands['foo'].callable - @set.after_command('foo') { foo << 2 } - @set.run_command(ctx, 'foo') - - foo.should == [1, 2] - end - end - - end end diff --git a/test/test_pry.rb b/test/test_pry.rb index fcc9f2bf..b9bcb540 100644 --- a/test/test_pry.rb +++ b/test/test_pry.rb @@ -886,7 +886,7 @@ describe Pry do klass = Pry::CommandSet.new do alias_command "help2", "help" end - klass.commands["help2"].callable.should == klass.commands["help"].callable + klass.commands["help2"].block.should == klass.commands["help"].block end it 'should change description of a command using desc' do From b6c01c92c41b18b72c068fb316b8e3fef769cbd9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 11:22:38 +0000 Subject: [PATCH 06/12] Fix install-command --- lib/pry/command_processor.rb | 2 +- lib/pry/command_set.rb | 50 +++++++++++------------ lib/pry/default_commands/documentation.rb | 2 +- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/lib/pry/command_processor.rb b/lib/pry/command_processor.rb index 72369b5e..8b1dc7cf 100644 --- a/lib/pry/command_processor.rb +++ b/lib/pry/command_processor.rb @@ -176,7 +176,7 @@ class Pry instance = command.new(context) catch(:command_done) do - ret = instance.call_with_hooks(*args) + ret = instance.call_safely(*args) end # FIXME: wtf? diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index bd23889b..2d05d585 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -58,6 +58,18 @@ class Pry self.command_processor = context[:command_processor] end + def call_safely(*args) + if dependencies_met? + call_with_hooks(*args) + else + 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." + end + end + def call_with_hooks(*args) self.class.hooks[:before].each do |block| instance_exec(*args, &block) @@ -69,17 +81,13 @@ class Pry ret = instance_exec(*args, &block) end - self.class.options[:keep_retval] ? ret : CommandContext::VOID_VALUE + command_options[:keep_retval] ? ret : CommandContext::VOID_VALUE end - end - class StubCommand < Command - def call(*args) - gems_needed = Array(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." + def command_options; self.class.options; end + + def dependencies_met? + @dependencies_met ||= command_dependencies_met?(command_options) end end @@ -135,7 +143,6 @@ class Pry opt.on(:h, :help, "Show this message.") end end - end include Enumerable @@ -222,11 +229,7 @@ class Pry :use_prefix => true }.merge!(options) - if command_dependencies_met? options - commands[name] = BlockCommand.subclass(name, description, options, helper_module, &block) - else - commands[name] = StubCommand.subclass(name, description, options, helper_module) - end + commands[name] = BlockCommand.subclass(name, description, options, helper_module, &block) end def command_class(name, description="No description.", options={}, &block) @@ -240,12 +243,9 @@ class Pry :use_prefix => true }.merge!(options) - if command_dependencies_met? options - commands[name] = ClassCommand.subclass(name, description, options, helper_module) - commands[name].class_eval(&block) - else - commands[name] = StubCommand.subclass(name, description, options, helper_module) - end + commands[name] = ClassCommand.subclass(name, description, options, helper_module) + commands[name].class_eval(&block) + commands[name] end # Execute a block of code before a command is invoked. The block also @@ -403,7 +403,7 @@ class Pry def run_command(context, name, *args) command = commands[name] or raise NoCommandError.new(name, self) - command.new(context).call_with_hooks(*args) + command.new(context).call_safely(*args) end private @@ -433,10 +433,9 @@ class Pry command "install-command", "Install a disabled command." do |name| require 'rubygems/dependency_installer' unless defined? Gem::DependencyInstaller command = find_command(name) - stub_info = command.options[:stub_info] - if !stub_info - output.puts "Not a command stub. Nothing to do." + if command_dependencies_met?(command.options) + output.puts "Dependencies for #{command.name} are met. Nothing to do." next end @@ -469,7 +468,6 @@ class Pry end next if gem_install_failed - command.options.delete :stub_info output.puts "Installation of `#{name}` successful! Type `help #{name}` for information" end end diff --git a/lib/pry/default_commands/documentation.rb b/lib/pry/default_commands/documentation.rb index a96c1359..5773fb26 100644 --- a/lib/pry/default_commands/documentation.rb +++ b/lib/pry/default_commands/documentation.rb @@ -62,7 +62,7 @@ class Pry attr_accessor :code_type attr_accessor :input_ranges - def initialize + def setup require 'gist' end From d221a22d023bb6744139df786bfd4461fad321a6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 11:50:04 +0000 Subject: [PATCH 07/12] Merge Pry::CommandContext and Pry::CommandSet::Command into Pry::Command --- lib/pry.rb | 2 +- lib/pry/command.rb | 191 ++++++++++++++++++++++ lib/pry/command_context.rb | 57 ------- lib/pry/command_processor.rb | 2 +- lib/pry/command_set.rb | 141 +--------------- lib/pry/default_commands/documentation.rb | 2 - lib/pry/helpers/text.rb | 2 +- lib/pry/pry_instance.rb | 4 +- test/test_command_set.rb | 6 +- test/test_pry.rb | 2 +- 10 files changed, 202 insertions(+), 207 deletions(-) create mode 100644 lib/pry/command.rb delete mode 100644 lib/pry/command_context.rb diff --git a/lib/pry.rb b/lib/pry.rb index 71055308..bda51df0 100644 --- a/lib/pry.rb +++ b/lib/pry.rb @@ -184,9 +184,9 @@ require "pry/wrapped_module" require "pry/history_array" require "pry/helpers" require "pry/history" +require "pry/command" require "pry/command_set" require "pry/commands" -require "pry/command_context" require "pry/custom_completions" require "pry/completion" require "pry/plugins" diff --git a/lib/pry/command.rb b/lib/pry/command.rb new file mode 100644 index 00000000..37ea993f --- /dev/null +++ b/lib/pry/command.rb @@ -0,0 +1,191 @@ +class Pry + # Command contexts are the objects runing each command. + # Helper modules can be mixed into this class. + 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 + + class << self + attr_accessor :name + attr_accessor :description + attr_accessor :options + attr_accessor :block + end + + attr_accessor :command_name + attr_accessor :output + attr_accessor :target + attr_accessor :target_self + attr_accessor :captures + attr_accessor :eval_string + attr_accessor :arg_string + attr_accessor :opts + attr_accessor :command_set + attr_accessor :command_processor + attr_accessor :_pry_ + + def initialize(&block) + instance_exec(&block) if block + 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) + complete_string = "#{command_string} #{args.join(" ")}" + command_processor.process_commands(complete_string, eval_string, target) + 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 + + + attr_accessor :context + + %w(name description options block).each do |attribute| + define_method(attribute) { self.class.send(attribute) } + end + + class << self + def inspect + "#" + end + + def subclass(name, description, options, helpers, &block) + klass = Class.new(self) + klass.send(:include, helpers) + klass.name = name + klass.description = description + klass.options = options + klass.block = block + klass + end + + def hooks + @hooks ||= {:before => [], :after => []} + end + end + + def initialize(context) + self.context = context + self.target = context[:target] + self.target_self = context[:target].eval('self') + self.output = context[:output] + self.captures = context[:captures] + self.eval_string = context[:eval_string] + self.arg_string = context[:arg_string] + self.command_set = context[:command_set] + self.command_name = self.class.options[:listing] + self._pry_ = context[:pry_instance] + self.command_processor = context[:command_processor] + end + + def call_safely(*args) + if dependencies_met? + call_with_hooks(*args) + else + 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." + end + end + + 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 + + command_options[:keep_retval] ? ret : void + end + + def command_options; self.class.options; end + + def dependencies_met? + @dependencies_met ||= command_dependencies_met?(command_options) + end + end + + class BlockCommand < Command + # backwards compatibility + alias_method :opts, :context + + def call(*args) + if options[:argument_required] && args.empty? + raise CommandError, "The command '#{command.name}' requires an argument." + end + + instance_exec(*correct_arg_arity(block.arity, args), &block) + end + + private + 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 + end + + class ClassCommand < Command + attr_accessor :opts + attr_accessor :args + + def call(*args) + setup + + self.opts = slop + self.args = self.opts.parse!(args) + + if opts.present?(:help) + output.puts slop.help + else + run + end + end + + def setup; end + def options(opt); end + def run; raise CommandError, "command '#{name}' not implemented" end + + def slop + Slop.new do |opt| + options(opt) + opt.on(:h, :help, "Show this message.") + end + end + end +end diff --git a/lib/pry/command_context.rb b/lib/pry/command_context.rb deleted file mode 100644 index 0b656bb3..00000000 --- a/lib/pry/command_context.rb +++ /dev/null @@ -1,57 +0,0 @@ -class Pry - # Command contexts are the objects runing each command. - # Helper modules can be mixed into this class. - class CommandContext - - # represents a void return value for a command - VOID_VALUE = Object.new - - # give it a nice inspect - def VOID_VALUE.inspect() "void" end - - attr_accessor :command_name - attr_accessor :output - attr_accessor :target - attr_accessor :target_self - attr_accessor :captures - attr_accessor :eval_string - attr_accessor :arg_string - attr_accessor :opts - attr_accessor :command_set - attr_accessor :command_processor - attr_accessor :_pry_ - - def initialize(&block) - instance_exec(&block) if block - 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) - complete_string = "#{command_string} #{args.join(" ")}" - command_processor.process_commands(complete_string, eval_string, target) - 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 - end -end diff --git a/lib/pry/command_processor.rb b/lib/pry/command_processor.rb index 8b1dc7cf..09b332ed 100644 --- a/lib/pry/command_processor.rb +++ b/lib/pry/command_processor.rb @@ -22,7 +22,7 @@ class Pry # (one that does not return a value) # @return [Boolean] def void_command? - (command? && !keep_retval?) || retval == CommandContext::VOID_VALUE + (command? && !keep_retval?) || retval == Command::VOID_VALUE end # Is the return value kept for this command? (i.e :keep_retval => true) diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index 2d05d585..74001571 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -1,4 +1,3 @@ -require 'pry/command_context' class Pry class NoCommandError < StandardError def initialize(name, owner) @@ -9,142 +8,6 @@ class Pry # This class is used to create sets of commands. Commands can be imported from # different sets, aliased, removed, etc. class CommandSet - class Command < Pry::CommandContext - - class << self - attr_accessor :name - attr_accessor :description - attr_accessor :options - attr_accessor :block - end - - attr_accessor :context - - %w(name description options block).each do |attribute| - define_method(attribute) { self.class.send(attribute) } - end - - class << self - def inspect - "#" - end - - def subclass(name, description, options, helpers, &block) - klass = Class.new(self) - klass.send(:include, helpers) - klass.name = name - klass.description = description - klass.options = options - klass.block = block - klass - end - - def hooks - @hooks ||= {:before => [], :after => []} - end - end - - def initialize(context) - self.context = context - self.target = context[:target] - self.target_self = context[:target].eval('self') - self.output = context[:output] - self.captures = context[:captures] - self.eval_string = context[:eval_string] - self.arg_string = context[:arg_string] - self.command_set = context[:command_set] - self.command_name = self.class.options[:listing] - self._pry_ = context[:pry_instance] - self.command_processor = context[:command_processor] - end - - def call_safely(*args) - if dependencies_met? - call_with_hooks(*args) - else - 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." - end - end - - 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 - - command_options[:keep_retval] ? ret : CommandContext::VOID_VALUE - end - - def command_options; self.class.options; end - - def dependencies_met? - @dependencies_met ||= command_dependencies_met?(command_options) - end - end - - class BlockCommand < Command - # backwards compatibility - alias_method :opts, :context - - def call(*args) - if options[:argument_required] && args.empty? - raise CommandError, "The command '#{command.name}' requires an argument." - end - - instance_exec(*correct_arg_arity(block.arity, args), &block) - end - - private - 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 - end - - class ClassCommand < Command - attr_accessor :opts - attr_accessor :args - - def call(*args) - setup - - self.opts = slop - self.args = self.opts.parse!(args) - - if opts.present?(:help) - output.puts slop.help - else - run - end - end - - def setup; end - def options(opt); end - def run; raise CommandError, "command '#{name}' not implemented" end - - def slop - Slop.new do |opt| - options(opt) - opt.on(:h, :help, "Show this message.") - end - end - end - include Enumerable include Pry::Helpers::BaseHelpers @@ -229,7 +92,7 @@ class Pry :use_prefix => true }.merge!(options) - commands[name] = BlockCommand.subclass(name, description, options, helper_module, &block) + commands[name] = Pry::BlockCommand.subclass(name, description, options, helper_module, &block) end def command_class(name, description="No description.", options={}, &block) @@ -243,7 +106,7 @@ class Pry :use_prefix => true }.merge!(options) - commands[name] = ClassCommand.subclass(name, description, options, helper_module) + commands[name] = Pry::ClassCommand.subclass(name, description, options, helper_module) commands[name].class_eval(&block) commands[name] end diff --git a/lib/pry/default_commands/documentation.rb b/lib/pry/default_commands/documentation.rb index 5773fb26..da3b964d 100644 --- a/lib/pry/default_commands/documentation.rb +++ b/lib/pry/default_commands/documentation.rb @@ -55,8 +55,6 @@ class Pry EOS end - require 'pry/command_context' - command_class "gist", "Gist a method or expression history to github. Type `gist --help` for more info.", :requires_gem => "gist", :shellwords => false do attr_accessor :content attr_accessor :code_type diff --git a/lib/pry/helpers/text.rb b/lib/pry/helpers/text.rb index ea27d327..502a5fc0 100644 --- a/lib/pry/helpers/text.rb +++ b/lib/pry/helpers/text.rb @@ -1,7 +1,7 @@ class Pry module Helpers - # The methods defined on {Text} are available to custom commands via {Pry::CommandContext#text}. + # The methods defined on {Text} are available to custom commands via {Pry::Command#text}. module Text COLORS = diff --git a/lib/pry/pry_instance.rb b/lib/pry/pry_instance.rb index 57d9f30c..4028ae0a 100644 --- a/lib/pry/pry_instance.rb +++ b/lib/pry/pry_instance.rb @@ -378,12 +378,12 @@ class Pry # @param [String] val The command (and its params) to execute. # @param [String] eval_string The current input buffer. # @param [Binding] target The binding to use.. - # @return [Pry::CommandContext::VOID_VALUE] + # @return [Pry::Command::VOID_VALUE] # @example # pry_instance.run_command("ls -m") def run_command(val, eval_string = "", target = binding_stack.last) @command_processor.process_commands(val, eval_string, target) - Pry::CommandContext::VOID_VALUE + Pry::Command::VOID_VALUE end # Set the last result of an eval. diff --git a/test/test_command_set.rb b/test/test_command_set.rb index 1a03e1d9..aedc8bb0 100644 --- a/test/test_command_set.rb +++ b/test/test_command_set.rb @@ -162,9 +162,9 @@ describe Pry::CommandSet do @set.desc('foo').should == 'bar' end - it 'should return Pry::CommandContext::VOID_VALUE for commands by default' do + it 'should return Pry::Command::VOID_VALUE for commands by default' do @set.command('foo') { 3 } - @set.run_command(@ctx, 'foo').should == Pry::CommandContext::VOID_VALUE + @set.run_command(@ctx, 'foo').should == Pry::Command::VOID_VALUE end it 'should be able to keep return values' do @@ -187,7 +187,7 @@ describe Pry::CommandSet do end @set.run_command(@ctx, 'foo') - Pry::CommandContext.new.should.not.respond_to :my_helper + Pry::Command.subclass('foo', '', {}, Module.new).new({:target => binding}).should.not.respond_to :my_helper end it 'should not recreate a new helper module when helpers is called' do diff --git a/test/test_pry.rb b/test/test_pry.rb index b9bcb540..64eb3312 100644 --- a/test/test_pry.rb +++ b/test/test_pry.rb @@ -43,7 +43,7 @@ describe Pry do # bug fix for https://github.com/banister/pry/issues/93 it 'should not leak pry constants into Object namespace' do - input_string = "CommandContext" + input_string = "Command" str_output = StringIO.new o = Object.new pry_tester = Pry.new(:input => StringIO.new(input_string), From 7b0a5a346c7269193c1010a026895d2e96ede874 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 12:40:01 +0000 Subject: [PATCH 08/12] Document Pry::Command --- lib/pry/command.rb | 204 ++++++++++++++++++++++++--------- lib/pry/default_commands/ls.rb | 2 +- 2 files changed, 152 insertions(+), 54 deletions(-) diff --git a/lib/pry/command.rb b/lib/pry/command.rb index 37ea993f..4db49f03 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -1,6 +1,8 @@ class Pry - # Command contexts are the objects runing each command. - # Helper modules can be mixed into this class. + + # 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 @@ -9,6 +11,8 @@ class Pry # 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 :name attr_accessor :description @@ -16,6 +20,48 @@ class Pry attr_accessor :block 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 + + class << self + def 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.options = options + klass.block = block + klass + 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 + end + + + # Properties of one execution of a command (passed by the {Pry::CommandProcessor} as a hash of + # context and expanded in {#initialize} attr_accessor :command_name attr_accessor :output attr_accessor :target @@ -23,15 +69,11 @@ class Pry attr_accessor :captures attr_accessor :eval_string attr_accessor :arg_string - attr_accessor :opts + attr_accessor :context attr_accessor :command_set attr_accessor :command_processor attr_accessor :_pry_ - def initialize(&block) - instance_exec(&block) if block - 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 @@ -62,32 +104,9 @@ class Pry include Pry::Helpers::CommandHelpers - attr_accessor :context - - %w(name description options block).each do |attribute| - define_method(attribute) { self.class.send(attribute) } - end - - class << self - def inspect - "#" - end - - def subclass(name, description, options, helpers, &block) - klass = Class.new(self) - klass.send(:include, helpers) - klass.name = name - klass.description = description - klass.options = options - klass.block = block - klass - end - - def hooks - @hooks ||= {:before => [], :after => []} - end - end - + # 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] @@ -102,18 +121,43 @@ class Pry self.command_processor = context[:command_processor] 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) - if dependencies_met? - call_with_hooks(*args) - else + 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) @@ -125,29 +169,24 @@ class Pry ret = instance_exec(*args, &block) end - command_options[:keep_retval] ? ret : void - end - - def command_options; self.class.options; end - - def dependencies_met? - @dependencies_met ||= command_dependencies_met?(command_options) + 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 def call(*args) - if options[:argument_required] && args.empty? - raise CommandError, "The command '#{command.name}' requires an argument." - end - instance_exec(*correct_arg_arity(block.arity, args), &block) end - private def correct_arg_arity(arity, args) case when arity < 0 @@ -160,10 +199,25 @@ class Pry 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 @@ -172,20 +226,64 @@ class Pry if opts.present?(:help) output.puts slop.help + void else - run + process end end - def setup; end - def options(opt); end - def run; raise CommandError, "command '#{name}' not implemented" end - + # Return an instance of Slop that can parse the options that this command accepts. def slop Slop.new do |opt| 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 diff --git a/lib/pry/default_commands/ls.rb b/lib/pry/default_commands/ls.rb index b3259e57..94b4e8e3 100644 --- a/lib/pry/default_commands/ls.rb +++ b/lib/pry/default_commands/ls.rb @@ -37,7 +37,7 @@ class Pry opt.on :h, "help", "Show help" end - def run + def process obj = args.empty? ? target_self : target.eval(args.join(" ")) # exclude -q, -v and --grep because they don't specify what the user wants to see. From 367b353bca9718a322b231246e145b78ac5d3332 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 14:13:59 +0000 Subject: [PATCH 09/12] Make target_self and command_name methods, not attributes --- lib/pry/command.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pry/command.rb b/lib/pry/command.rb index 4db49f03..95d986b4 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -25,6 +25,7 @@ class Pry 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 @@ -62,10 +63,8 @@ class Pry # Properties of one execution of a command (passed by the {Pry::CommandProcessor} as a hash of # context and expanded in {#initialize} - attr_accessor :command_name attr_accessor :output attr_accessor :target - attr_accessor :target_self attr_accessor :captures attr_accessor :eval_string attr_accessor :arg_string @@ -107,20 +106,21 @@ class Pry # Instantiate a command, in preparation for calling it. # # @param Hash context The runtime context to use with this command. - def initialize(context) + def initialize(context={}) self.context = context self.target = context[:target] - self.target_self = context[:target].eval('self') self.output = context[:output] self.captures = context[:captures] self.eval_string = context[:eval_string] self.arg_string = context[:arg_string] self.command_set = context[:command_set] - self.command_name = self.class.options[:listing] self._pry_ = context[:pry_instance] self.command_processor = context[:command_processor] end + # The value of {self} inside the {target} binding. + def target_self; target.eval('self'); end + # Run the command with the given {args}. # # This is a public wrapper around {#call} which ensures all preconditions are met. From 5539f3bebb8df76ad1347065b91ec42de7bc4b78 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 14:40:44 +0000 Subject: [PATCH 10/12] Make help use Slop where possible --- lib/pry/command.rb | 7 +++++++ lib/pry/command_set.rb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/pry/command.rb b/lib/pry/command.rb index 95d986b4..0a202757 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -197,6 +197,8 @@ class Pry args.values_at *(0..(arity - 1)).to_a end end + + def help; description; end end # A super-class ofr Commands with structure. @@ -232,6 +234,11 @@ class Pry 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| diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index 74001571..a56e766e 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -286,7 +286,7 @@ class Pry stagger_output(help_text) else if command = find_command(cmd) - output.puts command.description + output.puts command.new.help else output.puts "No info for command: #{cmd}" end From c34ae2871138fb8d617eac6a062fd592d0bb547c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 14:41:01 +0000 Subject: [PATCH 11/12] Share default options --- lib/pry/command_set.rb | 59 ++++++++++++++++------- lib/pry/default_commands/documentation.rb | 2 +- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/lib/pry/command_set.rb b/lib/pry/command_set.rb index a56e766e..819c7e19 100644 --- a/lib/pry/command_set.rb +++ b/lib/pry/command_set.rb @@ -82,29 +82,38 @@ class Pry # # pry(main)> help number # # number-N regex command def command(name, description="No description.", options={}, &block) - options = { - :requires_gem => [], - :keep_retval => false, - :argument_required => false, - :interpolate => true, - :shellwords => true, - :listing => name, - :use_prefix => true - }.merge!(options) + options = default_options(name).merge!(options) commands[name] = Pry::BlockCommand.subclass(name, description, options, helper_module, &block) end + # 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 + # Pry::Commands.command_class "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 + # 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 + # def command_class(name, description="No description.", options={}, &block) - options = { - :requires_gem => [], - :keep_retval => false, - :argument_required => false, - :interpolate => true, - :shellwords => true, - :listing => name, - :use_prefix => true - }.merge!(options) + options = default_options(name).merge!(options) commands[name] = Pry::ClassCommand.subclass(name, description, options, helper_module) commands[name].class_eval(&block) @@ -264,12 +273,26 @@ class Pry commands.keys end + # @nodoc used for testing def run_command(context, name, *args) command = commands[name] or raise NoCommandError.new(name, self) command.new(context).call_safely(*args) end private + + 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 command "help", "This menu." do |cmd| diff --git a/lib/pry/default_commands/documentation.rb b/lib/pry/default_commands/documentation.rb index da3b964d..a47fc2c1 100644 --- a/lib/pry/default_commands/documentation.rb +++ b/lib/pry/default_commands/documentation.rb @@ -85,7 +85,7 @@ USAGE end end - def run + def process if opts.present?(:in) in_option elsif opts.present?(:file) From f8c2d04dcea5c7772dae14a6f3568fdfa1f2afa0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 31 Dec 2011 15:21:58 +0000 Subject: [PATCH 12/12] Test new command api --- test/helper.rb | 6 + test/test_command.rb | 223 ++++++++++++++++++++++++++++++++++ test/test_default_commands.rb | 3 +- 3 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 test/test_command.rb diff --git a/test/helper.rb b/test/helper.rb index db384074..b30d216c 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -98,6 +98,12 @@ def mock_pry(*args) output.string end +def mock_command(cmd, args=[], opts={}) + output = StringIO.new + ret = cmd.new(opts.merge(:output => output)).call_safely(*args) + Struct.new(:output, :return).new(output.string, ret) +end + def redirect_global_pry_input(new_io) old_io = Pry.input Pry.input = new_io diff --git a/test/test_command.rb b/test/test_command.rb new file mode 100644 index 00000000..cfa88efe --- /dev/null +++ b/test/test_command.rb @@ -0,0 +1,223 @@ +require 'helper' + +describe "Pry::Command" do + + before do + @set = Pry::CommandSet.new + end + + describe 'call_safely' do + + it 'should display a message if gems are missing' do + cmd = @set.command_class "ford-prefect", "From a planet near Beetlegeuse", :requires_gem => %w(ghijkl) do + # + end + + mock_command(cmd, %w(hello world)).output.should =~ /install-command ford-prefect/ + end + + it 'should abort early if arguments are required' do + cmd = @set.command_class 'arthur-dent', "Doesn't understand Thursdays", :argument_required => true do + # + end + + lambda { + mock_command(cmd, %w()) + }.should.raise(Pry::CommandError) + end + + it 'should return VOID without keep_retval' do + cmd = @set.command_class 'zaphod-beeblebrox', "Likes pan-Galactic Gargle Blasters" do + def process + 3 + end + end + + mock_command(cmd).return.should == Pry::Command::VOID_VALUE + end + + it 'should return the return value with keep_retval' do + cmd = @set.command_class 'tricia-mcmillian', "a.k.a Trillian", :keep_retval => true do + def process + 5 + end + end + + mock_command(cmd).return.should == 5 + end + + it 'should call hooks in the right order' do + cmd = @set.command_class 'marvin', "Pained by the diodes in his left side" do + def process + output.puts 3 + args[0].to_i + end + end + + @set.before_command 'marvin' do |i| + output.puts 2 + i.to_i + end + @set.before_command 'marvin' do |i| + output.puts 1 + i.to_i + end + + @set.after_command 'marvin' do |i| + output.puts 4 + i.to_i + end + + @set.after_command 'marvin' do |i| + output.puts 5 + i.to_i + end + + mock_command(cmd, %w(2)).output.should == "3\n4\n5\n6\n7\n" + end + + # TODO: This strikes me as rather silly... + it 'should return the value from the last hook with keep_retval' do + cmd = @set.command_class 'slartibartfast', "Designs Fjords", :keep_retval => true do + def process + 22 + end + end + + @set.after_command 'slartibartfast' do + 10 + end + + mock_command(cmd).return.should == 10 + end + end + + describe 'help' do + it 'should default to the description for blocky commands' do + @set.command 'oolon-colluphid', "Raving Atheist" do + # + end + + mock_command(@set.commands['help'], %w(oolon-colluphid), :command_set => @set).output.should =~ /Raving Atheist/ + end + + it 'should use slop to generate the help for classy commands' do + @set.command_class 'eddie', "The ship-board computer" do + def options(opt) + opt.banner "Over-cheerful, and makes a ticking noise." + end + end + + mock_command(@set.commands['help'], %w(eddie), :command_set => @set).output.should =~ /Over-cheerful/ + end + + it 'should provide --help for classy commands' do + cmd = @set.command_class 'agrajag', "Killed many times by Arthur" do + def options(opt) + opt.on :r, :retaliate, "Try to get Arthur back" + end + end + + mock_command(cmd, %w(--help)).output.should =~ /--retaliate/ + end + + it 'should provide a -h for classy commands' do + cmd = @set.command_class 'zarniwoop', "On an intergalactic cruise, in his office." do + def options(opt) + opt.on :e, :escape, "Help zaphod escape the Total Perspective Vortex" + end + end + + mock_command(cmd, %w(--help)).output.should =~ /Total Perspective Vortex/ + end + end + + + describe 'context' do + context = { + :target => binding, + :output => StringIO.new, + :captures => [], + :eval_string => "eval-string", + :arg_string => "arg-string", + :command_set => @set, + :pry_instance => Object.new, + :command_processor => Object.new + } + + it 'should capture lots of stuff from the hash passed to new before setup' do + cmd = @set.command_class 'fenchurch', "Floats slightly off the ground" do + define_method(:setup) do + self.context.should == context + target.should == context[:target] + target_self.should == context[:target].eval('self') + output.should == context[:output] + end + + define_method(:process) do + captures.should.equal?(context[:captures]) + eval_string.should == "eval-string" + arg_string.should == "arg-string" + command_set.should == @set + _pry_.should == context[:pry_instance] + command_processor.should == context[:command_processor] + end + end + + cmd.new(context).call + end + end + + describe 'classy api' do + + it 'should call setup, then options, then process' do + cmd = @set.command_class 'rooster', "Has a tasty towel" do + def setup + output.puts "setup" + end + + def options(opt) + output.puts "options" + end + + def process + output.puts "process" + end + end + + mock_command(cmd).output.should == "setup\noptions\nprocess\n" + end + + it 'should raise a command error if process is not overridden' do + cmd = @set.command_class 'jeltz', "Commander of a Vogon constructor fleet" do + def proccces + # + end + end + + lambda { + mock_command(cmd) + }.should.raise(Pry::CommandError) + end + + it 'should work if neither options, nor setup is overridden' do + cmd = @set.command_class 'wowbagger', "Immortal, insulting.", :keep_retval => true do + def process + 5 + end + end + + mock_command(cmd).return.should == 5 + end + + it 'should provide opts and args as provided by slop' do + cmd = @set.command_class 'lintilla', "One of 800,000,000 clones" do + def options(opt) + opt.on :f, :four, "A numeric four", :as => Integer, :optional => true + end + + def process + args.should == ['four'] + opts[:f].should == 4 + end + end + + mock_command(cmd, %w(--four 4 four)) + end + end +end diff --git a/test/test_default_commands.rb b/test/test_default_commands.rb index 1321b84d..64af4ed7 100644 --- a/test/test_default_commands.rb +++ b/test/test_default_commands.rb @@ -7,8 +7,7 @@ describe "Pry::Commands" do redirect_pry_io(InputTester.new("help ls", "exit-all"), str_output) do pry end - str_output.string.each_line.count.should == 1 - str_output.string.should =~ /ls --help/ + str_output.string.should =~ /Usage: ls/ end it 'should display help for a regex command with a "listing"' do