diff options
author | Kyrylo Silin <silin@kyrylo.org> | 2019-04-09 03:02:54 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-09 03:02:54 +0300 |
commit | f7f05fc703d6eab387dfbf484c33ae71fa70c308 (patch) | |
tree | 12f7629122fecefd17d8981033a71010fbe2e2f7 | |
parent | b7817c4015fcb0bf26852fd969921ca23ca97b48 (diff) | |
parent | c6bc5eb6554d38c3c96fcc188944f8c1c3e460c1 (diff) | |
download | pry-f7f05fc703d6eab387dfbf484c33ae71fa70c308.tar.gz |
Merge pull request #2003 from pry/command-refactoring
Command class refactoring & adding tests
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | lib/pry.rb | 2 | ||||
-rw-r--r-- | lib/pry/block_command.rb | 20 | ||||
-rw-r--r-- | lib/pry/class_command.rb | 192 | ||||
-rw-r--r-- | lib/pry/command.rb | 360 | ||||
-rw-r--r-- | spec/block_command_spec.rb | 63 | ||||
-rw-r--r-- | spec/class_command_spec.rb | 264 | ||||
-rw-r--r-- | spec/command_spec.rb | 1093 |
8 files changed, 1102 insertions, 894 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a443510e..e7c238ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ ([#2001](https://github.com/pry/pry/pull/2001)) * Deleted `Pry::Command#disabled_commands` ([#2001](https://github.com/pry/pry/pull/2001)) +* Deleted `Pry::BlockCommand#opts` (use `#context` instead) + ([#2003](https://github.com/pry/pry/pull/2003)) #### Bug fixes @@ -14,6 +14,8 @@ require 'pry/exceptions' require 'pry/hooks' require 'pry/input_completer' require 'pry/command' +require 'pry/class_command' +require 'pry/block_command' require 'pry/command_set' require 'pry/syntax_highlighter' diff --git a/lib/pry/block_command.rb b/lib/pry/block_command.rb new file mode 100644 index 00000000..947f8e7d --- /dev/null +++ b/lib/pry/block_command.rb @@ -0,0 +1,20 @@ +class Pry + # 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 + # Call the block that was registered with this command. + # @param [Array<String>] args The arguments passed + # @return [Object] The return value of the block + def call(*args) + instance_exec(*normalize_method_args(block, args), &block) + end + + def help + "#{command_options[:listing].to_s.ljust(18)} #{description}" + end + end +end diff --git a/lib/pry/class_command.rb b/lib/pry/class_command.rb new file mode 100644 index 00000000..dde4781a --- /dev/null +++ b/lib/pry/class_command.rb @@ -0,0 +1,192 @@ +class Pry + # A super-class of 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#create_command}, and override the + # `options(opt)` method to set up an instance of Pry::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 + class << self + # Ensure that subclasses inherit the options, description and + # match from a ClassCommand super class. + def inherited(klass) + klass.match match + klass.description description + klass.command_options options + end + + def source + source_object.source + end + + def doc + new.help + end + + def source_location + source_object.source_location + end + + def source_file + source_object.source_file + end + alias file source_file + + def source_line + source_object.source_line + end + alias line source_line + + private + + # The object used to extract the source for the command. + # + # This should be a `Pry::Method(block)` for a command made with `create_command` + # and a `Pry::WrappedModule(self)` for a command that's a standard class. + # @return [Pry::WrappedModule, Pry::Method] + def source_object + @source_object ||= if name =~ /^[A-Z]/ + Pry::WrappedModule(self) + else + Pry::Method(block) + end + end + end + + attr_accessor :opts + attr_accessor :args + + # Set up `opts` and `args`, and then call `process`. + # + # This method will display help if necessary. + # + # @param [Array<String>] args The arguments passed + # @return [Object] The return value of `process` or VOID_VALUE + def call(*args) + setup + + self.opts = slop + self.args = opts.parse!(args) + + if opts.present?(:help) + output.puts slop.help + void + else + process(*normalize_method_args(method(:process), args)) + end + end + + # Return the help generated by Pry::Slop for this command. + def help + slop.help + end + + # Return an instance of Pry::Slop that can parse either subcommands or the + # options that this command accepts. + def slop + Pry::Slop.new do |opt| + opt.banner(unindent(self.class.banner)) + subcommands(opt) + options(opt) + opt.on :h, :help, 'Show this message.' + end + end + + # Generate shell completions + # @param [String] search The line typed so far + # @return [Array<String>] the words to complete + def complete(search) + slop.flat_map do |opt| + [opt.long && "--#{opt.long} " || opt.short && "-#{opt.short}"] + end.compact + super + end + + # A method called just before `options(opt)` as part of `call`. + # + # This method 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 method to setup Pry::Slop commands so it can parse the subcommands your + # command expects. If you need to set up default values, use `setup` + # instead. + # + # @example A minimal example + # def subcommands(cmd) + # cmd.command :download do |opt| + # description 'Downloads a content from a server' + # + # opt.on :verbose, 'Use verbose output' + # + # run do |options, arguments| + # ContentDownloader.download(options, arguments) + # end + # end + # end + # + # @example Define the invokation block anywhere you want + # def subcommands(cmd) + # cmd.command :download do |opt| + # description 'Downloads a content from a server' + # + # opt.on :verbose, 'Use verbose output' + # end + # end + # + # def process + # # Perform calculations... + # opts.fetch_command(:download).run do |options, arguments| + # ContentDownloader.download(options, arguments) + # end + # # More calculations... + # end + def subcommands(cmd); end + + # A method to setup Pry::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 Pry::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 '#{command_name}' not implemented" + end + end +end diff --git a/lib/pry/command.rb b/lib/pry/command.rb index 86f1c924..81cded01 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -10,6 +10,10 @@ class Pry extend Helpers::DocumentationHelpers extend CodeObject::Helpers + include Pry::Helpers::BaseHelpers + include Pry::Helpers::CommandHelpers + include Pry::Helpers::Text + # represents a void return value for a command VOID_VALUE = Object.new @@ -91,38 +95,7 @@ class Pry takes_block: false } end - end - - # Make those properties accessible to instances - def name - self.class.name - end - - def match - self.class.match - end - - def description - self.class.description - end - - def block - self.class.block - end - - def command_options - self.class.options - end - def command_name - self.class.command_name - end - - def source - self.class.source - end - - class << self def name super.to_s == "" ? "#<class(Pry::Command #{match.inspect})>" : super end @@ -188,8 +161,7 @@ class Pry end def command_regex - pr = Pry.respond_to?(:config) ? Pry.config.command_prefix : "" - prefix = convert_to_regex(pr) + prefix = convert_to_regex(Pry.config.command_prefix) prefix = "(?:#{prefix})?" unless options[:use_prefix] /^#{prefix}#{convert_to_regex(match)}(?!\S)/ @@ -236,17 +208,8 @@ class Pry attr_accessor :context attr_accessor :command_set attr_accessor :hooks - attr_accessor :pry_instance alias _pry_= pry_instance= - def _pry_ - loc = caller_locations(1..1).first - warn( - "#{loc.path}:#{loc.lineno}: warning: _pry_ is deprecated, use " \ - "pry_instance instead" - ) - pry_instance - end # The block we pass *into* a command so long as `:takes_block` is # not equal to `false` @@ -256,6 +219,47 @@ class Pry # end attr_accessor :command_block + # Instantiate a command, in preparation for calling it. + # @param [Hash] context The runtime context to use with this command. + def initialize(context = {}) + self.context = context + self.target = context[:target] + self.output = context[:output] + self.eval_string = context[:eval_string] + self.command_set = context[:command_set] + self.hooks = context[:hooks] + self.pry_instance = context[:pry_instance] + end + + # Make those properties accessible to instances + def name + self.class.name + end + + def match + self.class.match + end + + def description + self.class.description + end + + def block + self.class.block + end + + def command_options + self.class.options + end + + def command_name + self.class.command_name + end + + def source + self.class.source + end + # Run a command from another command. # @param [String] command_string The string that invokes the command # @param [Array] args Further arguments to pass to the command @@ -279,20 +283,13 @@ class Pry VOID_VALUE end - include Pry::Helpers::BaseHelpers - include Pry::Helpers::CommandHelpers - include Pry::Helpers::Text - - # Instantiate a command, in preparation for calling it. - # @param [Hash] context The runtime context to use with this command. - def initialize(context = {}) - self.context = context - self.target = context[:target] - self.output = context[:output] - self.eval_string = context[:eval_string] - self.command_set = context[:command_set] - self.hooks = context[:hooks] - self.pry_instance = context[:pry_instance] + def _pry_ + loc = caller_locations(1..1).first + warn( + "#{loc.path}:#{loc.lineno}: warning: _pry_ is deprecated, use " \ + "pry_instance instead" + ) + pry_instance end # @return [Object] The value of `self` inside the `target` binding. @@ -405,6 +402,16 @@ class Pry call_safely(*(captures + args)) end + # Generate completions for this command + # + # @param [String] _search The line typed so far + # @return [Array<String>] Completion words + def complete(_search) + [] + end + + private + # Run the command with the given `args`. # # This is a public wrapper around `#call` which ensures all preconditions @@ -432,16 +439,6 @@ class Pry Symbol.instance_eval { define_method(:call, call_method) } if call_method end - # Generate completions for this command - # - # @param [String] _search The line typed so far - # @return [Array<String>] Completion words - def complete(_search) - [] - end - - private - # Pass a block argument to a command. # @param [String] arg_string The arguments (as a string) passed to the command. # We inspect these for a '| do' or a '| {' and if we find it we use it @@ -502,231 +499,20 @@ class Pry ret end - # Fix the number of arguments we pass to a block to avoid arity warnings. - # @param [Fixnum] arity The arity of the block - # @param [Array] args The arguments to pass - # @return [Array] A (possibly shorter) array of the arguments to pass - def correct_arg_arity(arity, args) - if arity < 0 + # Normalize method arguments according to its arity. + # + # @param [Integer] method + # @param [Array] args + # @return [Array] a (possibly shorter) array of the arguments to pass + def normalize_method_args(method, args) + case method.arity + when -1 args - elsif arity == 0 + when 0 [] - elsif arity > 0 - args.values_at(*(0..(arity - 1)).to_a) - end - 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 opts context - - # Call the block that was registered with this command. - # @param [Array<String>] args The arguments passed - # @return [Object] The return value of the block - def call(*args) - instance_exec(*correct_arg_arity(block.arity, args), &block) - end - - def help - "#{command_options[:listing].to_s.ljust(18)} #{description}" - end - end - - # A super-class of 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#create_command}, and override the - # `options(opt)` method to set up an instance of Pry::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 - class << self - # Ensure that subclasses inherit the options, description and - # match from a ClassCommand super class. - def inherited(klass) - klass.match match - klass.description description - klass.command_options options - end - - def source - source_object.source - end - - def doc - new.help - end - - def source_location - source_object.source_location - end - - def source_file - source_object.source_file - end - alias file source_file - - def source_line - source_object.source_line - end - alias line source_line - - private - - # The object used to extract the source for the command. - # - # This should be a `Pry::Method(block)` for a command made with `create_command` - # and a `Pry::WrappedModule(self)` for a command that's a standard class. - # @return [Pry::WrappedModule, Pry::Method] - def source_object - @source_object ||= if name =~ /^[A-Z]/ - Pry::WrappedModule(self) - else - Pry::Method(block) - end - end - end - - attr_accessor :opts - attr_accessor :args - - # Set up `opts` and `args`, and then call `process`. - # - # This method will display help if necessary. - # - # @param [Array<String>] args The arguments passed - # @return [Object] The return value of `process` or VOID_VALUE - def call(*args) - setup - - self.opts = slop - self.args = opts.parse!(args) - - if opts.present?(:help) - output.puts slop.help - void else - process(*correct_arg_arity(method(:process).arity, args)) - end - end - - # Return the help generated by Pry::Slop for this command. - def help - slop.help - end - - # Return an instance of Pry::Slop that can parse either subcommands or the - # options that this command accepts. - def slop - Pry::Slop.new do |opt| - opt.banner(unindent(self.class.banner)) - subcommands(opt) - options(opt) - opt.on :h, :help, 'Show this message.' + args.values_at(*(0..(method.arity - 1)).to_a) end end - - # Generate shell completions - # @param [String] search The line typed so far - # @return [Array<String>] the words to complete - def complete(search) - slop.flat_map do |opt| - [opt.long && "--#{opt.long} " || opt.short && "-#{opt.short}"] - end.compact + super - end - - # A method called just before `options(opt)` as part of `call`. - # - # This method 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 method to setup Pry::Slop commands so it can parse the subcommands your - # command expects. If you need to set up default values, use `setup` - # instead. - # - # @example A minimal example - # def subcommands(cmd) - # cmd.command :download do |opt| - # description 'Downloads a content from a server' - # - # opt.on :verbose, 'Use verbose output' - # - # run do |options, arguments| - # ContentDownloader.download(options, arguments) - # end - # end - # end - # - # @example Define the invokation block anywhere you want - # def subcommands(cmd) - # cmd.command :download do |opt| - # description 'Downloads a content from a server' - # - # opt.on :verbose, 'Use verbose output' - # end - # end - # - # def process - # # Perform calculations... - # opts.fetch_command(:download).run do |options, arguments| - # ContentDownloader.download(options, arguments) - # end - # # More calculations... - # end - def subcommands(cmd); end - - # A method to setup Pry::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 Pry::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 '#{command_name}' not implemented" - end end end diff --git a/spec/block_command_spec.rb b/spec/block_command_spec.rb new file mode 100644 index 00000000..1a57e96a --- /dev/null +++ b/spec/block_command_spec.rb @@ -0,0 +1,63 @@ +RSpec.describe Pry::BlockCommand do + subject { Class.new(described_class).new } + + describe "#call" do + context "when #process accepts no arguments" do + let(:block) do + def process; end + method(:process) + end + + before { subject.class.block = block } + + it "calls the block despite passed arguments" do + expect { subject.call(1, 2) }.not_to raise_error + end + end + + context "when #process accepts some arguments" do + let(:block) do + def process(arg, other); end + method(:process) + end + + before { subject.class.block = block } + + it "calls the block even if there's not enough arguments" do + expect { subject.call(1) }.not_to raise_error + end + + it "calls the block even if there are more arguments than needed" do + expect { subject.call(1, 2, 3) }.not_to raise_error + end + end + + context "when passed a variable-length array" do + let(:block) do + def process(*args); end + method(:process) + end + + before { subject.class.block = block } + + it "calls the block without arguments" do + expect { subject.call }.not_to raise_error + end + + it "calls the block with some arguments" do + expect { subject.call(1, 2, 3) }.not_to raise_error + end + end + end + + describe "#help" do + before do + subject.class.description = 'desc' + subject.class.command_options(listing: 'listing') + end + + it "returns help output" do + expect(subject.help).to eq('listing desc') + end + end +end diff --git a/spec/class_command_spec.rb b/spec/class_command_spec.rb new file mode 100644 index 00000000..8d7d7484 --- /dev/null +++ b/spec/class_command_spec.rb @@ -0,0 +1,264 @@ +RSpec.describe Pry::ClassCommand do + describe ".inherited" do + context "when match is defined" do + subject do + Class.new(described_class) do + match('match') + end + end + + it "sets match on the subclass" do + subclass = Class.new(subject) + expect(subclass.match).to eq('match') + end + end + + context "when description is defined" do + subject do + Class.new(described_class) do + description('description') + end + end + + it "sets description on the subclass" do + subclass = Class.new(subject) + expect(subclass.description).to eq('description') + end + end + + context "when command_options is defined" do + subject do + Class.new(described_class) do + command_options(listing: 'listing') + end + end + + it "sets command_options on the subclass" do + subclass = Class.new(subject) + expect(subclass.command_options) + .to match(hash_including(listing: 'listing')) + end + end + end + + describe ".source" do + subject { Class.new(described_class) } + + it "returns source code for the process method" do + expect(subject.source).to match(/\Adef process\n.+\nend\n\z/) + end + end + + describe ".doc" do + subject do + Class.new(described_class) { banner('banner') } + end + + it "returns source code for the process method" do + expect(subject.doc).to eq("banner\n -h, --help Show this message.") + end + end + + describe ".source_location" do + subject { Class.new(described_class) } + + it "returns source location" do + expect(subject.source_location) + .to match([/class_command.rb/, be_kind_of(Integer)]) + end + end + + describe ".source_file" do + subject { Class.new(described_class) } + + it "returns source file" do + expect(subject.source_file).to match(/class_command.rb/) + end + end + + describe ".source_line" do + subject { Class.new(described_class) } + + it "returns source file" do + expect(subject.source_line).to be_kind_of(Integer) + end + end + + describe "#call" do + subject do + command = Class.new(described_class) do + def process; end + end + command.new + end + + before { subject.class.banner('banner') } + + it "invokes setup" do + expect(subject).to receive(:setup) + expect(subject.call) + end + + it "sets command's opts" do + expect { subject.call }.to change { subject.opts } + .from(nil).to(an_instance_of(Pry::Slop)) + end + + it "sets command's args" do + expect { subject.call('foo', 'bar') }.to change { subject.args } + .from(nil).to(%w[foo bar]) + end + + context "when help is invoked" do + let(:output) { StringIO.new } + + before { subject.output = output } + + it "outputs help info" do + subject.call('--help') + expect(subject.output.string) + .to eq("banner\n -h, --help Show this message.\n") + end + + it "returns void value" do + expect(subject.call('--help')).to eql(Pry::Command::VOID_VALUE) + end + end + + context "when help is not invloved" do + context "when #process accepts no arguments" do + subject do + command = Class.new(described_class) do + def process; end + end + command.new + end + + it "calls the command despite passed arguments" do + expect { subject.call('foo') }.not_to raise_error + end + end + + context "when #process accepts some arguments" do + subject do + command = Class.new(described_class) do + def process(arg, other); end + end + command.new + end + + it "calls the command even if there's not enough arguments" do + expect { subject.call('foo') }.not_to raise_error + end + + it "calls the command even if there are more arguments than needed" do + expect { subject.call('1', '2', '3') }.not_to raise_error + end + end + + context "when passed a variable-length array" do + subject do + command = Class.new(described_class) do + def process(arg, other); end + end + command.new + end + + it "calls the command without arguments" do + expect { subject.call }.not_to raise_error + end + + it "calls the command with some arguments" do + expect { subject.call('1', '2', '3') }.not_to raise_error + end + end + end + end + + describe "#help" do + subject { Class.new(described_class).new } + + before { subject.class.banner('banner') } + + it "returns help output" do + expect(subject.help) + .to eq("banner\n -h, --help Show this message.") + end + end + + describe "#slop" do + subject { Class.new(described_class).new } + + before { subject.class.banner(' banner') } + + it "returns a Slop instance" do + expect(subject.slop).to be_a(Pry::Slop) + end + + it "makes Slop's banner unindented" do + slop = subject.slop + expect(slop.banner).to eq('banner') + end + + it "defines the help option" do + expect(subject.slop.fetch_option(:help)).not_to be_nil + end + + context "when there are subcommands" do + subject do + command = Class.new(described_class) do + def subcommands(cmd) + cmd.command(:download) + end + end + command.new + end + + it "adds subcommands to Slop" do + expect(subject.slop.fetch_command(:download)).not_to be_nil + end + end + + context "when there are options" do + subject do + command = Class.new(described_class) do + def options(opt) + opt.on(:test) + end + end + command.new + end + + it "adds subcommands to Slop" do + expect(subject.slop.fetch_option(:test)).not_to be_nil + end + end + end + + describe "#complete" do + subject do + command = Class.new(described_class) do + def options(opt) + opt.on(:d, :download) + opt.on(:u, :upload) + opt.on(:x) + end + end + command.new + end + + before { subject.class.banner('') } + + it "generates option completions" do + expect(subject.complete('')) + .to match(array_including('--download ', '--upload ', '-x')) + end + end + + describe "#process" do + it "raises CommandError" do + expect { subject.process } + .to raise_error(Pry::CommandError, /not implemented/) + end + end +end diff --git a/spec/command_spec.rb b/spec/command_spec.rb index 85c5bdca..f56d92db 100644 --- a/spec/command_spec.rb +++ b/spec/command_spec.rb @@ -1,831 +1,710 @@ -describe "Pry::Command" do - before do - @set = Pry::CommandSet.new - @set.import Pry::Commands +require 'stringio' + +RSpec.describe Pry::Command do + subject do + Class.new(described_class) do + def process; end + end end - describe 'call_safely' do - it 'should abort early if arguments are required' do - cmd = @set.create_command( - 'arthur-dent', "Doesn't understand Thursdays", argument_required: true - ) do - end + let(:default_options) do + { + argument_required: false, + interpolate: true, + keep_retval: false, + shellwords: true, + takes_block: false, + use_prefix: true, + listing: 'nil' + } + end - expect { mock_command(cmd, %w[]) }.to raise_error Pry::CommandError - end + describe ".match" do + context "when no argument is given" do + context "and when match was defined previously" do + before { subject.match('old-match') } - it 'should return VOID without keep_retval' do - cmd = @set.create_command( - 'zaphod-beeblebrox', "Likes pan-Galactic Gargle Blasters" - ) do - def process - 3 + it "doesn't overwrite match" do + expect(subject.match).to eq('old-match') end end - expect(mock_command(cmd).return).to eq Pry::Command::VOID_VALUE - end - - it 'should return the return value with keep_retval' do - cmd = @set.create_command 'tricia-mcmillian', "a.k.a Trillian", keep_retval: true do - def process - 5 + context "and when match was not defined previously" do + it "sets match to nil" do + subject.match + expect(subject.match).to be_nil end end - - expect(mock_command(cmd).return).to eq 5 end - context "hooks API" do - before do - @set.create_command 'jamaica', 'Out of Many, One People' do - def process - output.puts 1 + args[0].to_i - end + context "when given an argument" do + context "and when match is a string" do + it "sets command options with listing as match" do + subject.match('match') # rubocop:disable Performance/RedundantMatch + expect(subject.command_options).to include(listing: 'match') end end - let(:hooks) do - h = Pry::Hooks.new - h.add_hook('before_jamaica', 'name1') do |i| - output.puts 3 - i.to_i - end - - h.add_hook('before_jamaica', 'name2') do |i| - output.puts 4 - i.to_i - end - - h.add_hook('after_jamaica', 'name3') do |i| - output.puts 2 + i.to_i + context "and when match is an object" do + let(:object) do + obj = Object.new + def obj.inspect + 'inspect' + end + obj end - h.add_hook('after_jamaica', 'name4') do |i| - output.puts 3 + i.to_i + it "sets command options with listing as object's inspect" do + subject.match(object) + expect(subject.command_options).to include(listing: 'inspect') end end - - it "should call hooks in the right order" do - out = pry_tester(hooks: hooks, commands: @set).process_command('jamaica 2') - expect(out).to eq("1\n2\n3\n4\n5\n") - end end end - describe 'help' do - it 'should default to the description for blocky commands' do - @set.command 'oolon-colluphid', "Raving Atheist" do - end + describe ".description" do + context "and when description was defined previously" do + before { subject.description('old description') } - expect(mock_command(@set['help'], %w[oolon-colluphid], command_set: @set).output) - .to match(/Raving Atheist/) + it "doesn't overwrite match" do + subject.description + expect(subject.description).to eq('old description') + end end - it 'should use slop to generate the help for classy commands' do - @set.create_command 'eddie', "The ship-board computer" do - def options(opt) - opt.banner "Over-cheerful, and makes a ticking noise." - end + context "and when description was not defined previously" do + it "sets description to nil" do + expect(subject.description).to be_nil end - - expect(mock_command(@set['help'], %w[eddie], command_set: @set).output) - .to match(/Over-cheerful/) end - it 'should provide --help for classy commands' do - cmd = @set.create_command 'agrajag', "Killed many times by Arthur" do - def options(opt) - opt.on :r, :retaliate, "Try to get Arthur back" - end + context "when given an argument" do + it "sets description" do + subject.description('description') + expect(subject.description).to eq('description') end - - expect(mock_command(cmd, %w[--help]).output).to match(/--retaliate/) end + end - it 'should provide a -h for classy commands' do - cmd = @set.create_command( - 'zarniwoop', "On an intergalactic cruise, in his office." - ) do - def options(opt) - opt.on :e, :escape, "Help zaphod escape the Total Perspective Vortex" + describe ".command_options" do + context "when no argument is given" do + context "and when command options were defined previously" do + before { subject.command_options(foo: :bar) } + + it "returns memoized command options" do + expect(subject.command_options).to eq(default_options.merge(foo: :bar)) end end - expect(mock_command(cmd, %w[--help]).output).to match(/Total Perspective Vortex/) + context "and when command options were not defined previously" do + it "sets command options to default options" do + subject.command_options + expect(subject.command_options).to eq(default_options) + end + end end - it 'should use the banner provided' do - cmd = @set.create_command 'deep-thought', "The second-best computer ever" do - banner <<-BANNER - Who's merest operational parameters, I am not worthy to compute. - BANNER - end + context "when given an argument" do + let(:new_option) { { new_option: 'value' } } - expect(mock_command(cmd, %w[--help]).output).to match(/Who\'s merest/) + it "merges the argument with command options" do + expect(subject.command_options(new_option)) + .to eq(default_options.merge(new_option)) + end end end - describe 'context' do - let(:context) do - { - target: binding, - output: StringIO.new, - eval_string: "eval-string", - command_set: @set, - pry_instance: Pry.new - } - end - - describe '#setup' do - it 'should capture lots of stuff from the hash passed to new before setup' do - inside = inner_scope do |probe| - cmd = @set.create_command('fenchurch', "Floats slightly off the ground") do - define_method(:setup, &probe) - end + describe ".banner" do + context "when no argument is given" do + context "and when banner was defined previously" do + before { subject.banner('banner') } - cmd.new(context).call + it "returns the memoized banner" do + expect(subject.banner).to eq('banner') end - - expect(inside.context).to eq(context) - expect(inside.target).to eq(context[:target]) - expect(inside.target_self).to eq(context[:target].eval('self')) - expect(inside.output).to eq(context[:output]) end - end - - describe '#process' do - it 'should capture lots of stuff from the hash passed to new before setup' do - inside = inner_scope do |probe| - cmd = @set.create_command('fenchurch', "Floats slightly off the ground") do - define_method(:process, &probe) - end - cmd.new(context).call + context "and when banner was not defined previously" do + it "return nil" do + subject.banner + expect(subject.banner).to be_nil end + end + end - expect(inside.eval_string).to eq("eval-string") - expect(inside.__send__(:command_set)).to eq(@set) - expect(inside.pry_instance).to eq(context[:pry_instance]) + context "when given an argument" do + it "merges the argument with command options" do + expect(subject.banner('banner')).to eq('banner') end end end - describe 'classy api' do - it 'should call setup, then subcommands, then options, then process' do - cmd = @set.create_command 'rooster', "Has a tasty towel" do - def setup - output.puts "setup" - end - - def subcommands(_cmd) - output.puts "subcommands" - end - - def options(_opt) - output.puts "options" - end + describe ".block" do + context "when block exists" do + let(:block) { proc {} } - def process - output.puts "process" - end + it "returns the block" do + subject.block = block + expect(subject.block).to eql(block) end - - expect(mock_command(cmd).output).to eq "setup\nsubcommands\noptions\nprocess\n" end - it 'should raise a command error if process is not overridden' do - cmd = @set.create_command 'jeltz', "Commander of a Vogon constructor fleet" do - def proccces; end + context "when block doesn't exist" do + it "uses #process method" do + expect(subject.block.name).to eq(:process) end + end + end - expect { mock_command(cmd) }.to raise_error Pry::CommandError + describe ".source" do + it "returns source code of the method" do + expect(subject.source).to eq("def process; end\n") end + end - it 'should work if neither options, nor setup is overridden' do - cmd = @set.create_command 'wowbagger', "Immortal, insulting.", keep_retval: true do - def process - 5 + describe ".doc" do + subject do + Class.new(described_class) do + def help + 'help' end end - - expect(mock_command(cmd).return).to eq 5 end - it 'should provide opts and args as provided by slop' do - cmd = @set.create_command 'lintilla', "One of 800,000,000 clones" do - def options(opt) - opt.on :f, :four, "A numeric four", as: Integer, optional_argument: true - end + it "returns help output" do + expect(subject.doc).to eq('help') + end + end - def process - output.puts args.inspect - output.puts opts[:f] - end - end + describe ".source_file" do + it "returns source file" do + expect(subject.source_file).to match(__FILE__) + end + end - result = mock_command(cmd, %w[--four 4 four]) - expect(result.output.split).to eq ['["four"]', '4'] + describe ".source_line" do + it "returns source line" do + expect(subject.source_line).to be_kind_of(Integer) end + end - it 'should allow overriding options after definition' do - cmd = @set.create_command( - /number-(one|two)/, "Lieutenants of the Golgafrinchan Captain", shellwords: false - ) do - command_options listing: 'number-one' + describe ".default_options" do + context "when given a String argument" do + it "returns default options with string listing" do + expect(subject.default_options('listing')) + .to eq(default_options.merge(listing: 'listing')) end - - expect(cmd.command_options[:shellwords]).to eq false - expect(cmd.command_options[:listing]).to eq 'number-one' end - it "should create subcommands" do - cmd = @set.create_command 'mum', 'Your mum' do - def subcommands(cmd) - cmd.command :yell + context "when given an Object argument" do + let(:object) do + obj = Object.new + def obj.inspect + 'inspect' end + obj + end - def process - output.puts opts.fetch_command(:blahblah).inspect - output.puts opts.fetch_command(:yell).present? - end + it "returns default options with object's inspect as listing" do + expect(subject.default_options(object)) + .to eq(default_options.merge(listing: 'inspect')) end + end + end - result = mock_command(cmd, ['yell']) - expect(result.output.split).to eq %w[nil true] + describe ".name" do + it "returns the name of the command" do + expect(subject.name).to eq('#<class(Pry::Command nil)>') end - it "should create subcommand options" do - cmd = @set.create_command 'mum', 'Your mum' do - def subcommands(cmd) - cmd.command :yell do - on :p, :person + context "when super command name exists" do + subject do + parent = Class.new(described_class) do + def name + 'parent name' end end - def process - output.puts args.inspect - output.puts opts.fetch_command(:yell).present? - output.puts opts.fetch_command(:yell).person? - end + Class.new(parent) end - result = mock_command(cmd, %w[yell --person papa]) - expect(result.output.split).to eq ['["papa"]', 'true', 'true'] - end - - it "should accept top-level arguments" do - cmd = @set.create_command 'mum', 'Your mum' do - def subcommands(cmd) - cmd.on :yell - end - - def process - args.should == %w[yell papa sonny daughter] - end + it "returns the name of the parent command" do + expect(subject.name).to eq('#<class(Pry::Command nil)>') end - - mock_command(cmd, %w[yell papa sonny daughter]) end + end - describe "explicit classes" do - before do - @x = Class.new(Pry::ClassCommand) do - options baby: :pig - match(/goat/) - description "waaaninngggiiigygygygygy" + describe ".inspect" do + subject do + Class.new(described_class) do + def self.name + 'name' end end + end - it 'subclasses should inherit options, match and description from superclass' do - k = Class.new(@x) - expect(k.options).to eq @x.options - expect(k.match).to eq @x.match - expect(k.description).to eq @x.description - end + it "returns command name" do + expect(subject.inspect).to eq('name') end end - describe 'tokenize' do - it "should interpolate string with \#{} in them" do - expect do |probe| - cmd = @set.command('random-dent', &probe) + describe ".command_name" do + before { subject.match('foo') } - _foo = 5 - # rubocop:disable Lint/InterpolationCheck - cmd.new(target: binding).process_line('random-dent #{1 + 2} #{3 + _foo}') - # rubocop:enable Lint/InterpolationCheck - end.to yield_with_args('3', '8') + it "returns listing" do + expect(subject.command_name).to eq('foo') end + end - it 'should not fail if interpolation is not needed and target is not set' do - expect do |probe| - cmd = @set.command('the-book', &probe) + describe ".subclass" do + it "returns a new class" do + klass = subject.subclass('match', 'desc', {}, Module.new) + expect(klass).to be_a(Class) + expect(klass).not_to eql(subject) + end - cmd.new.process_line 'the-book --help' - end.to yield_with_args('--help') + it "includes helpers to the new class" do + mod = Module.new { def foo; end } + klass = subject.subclass('match', 'desc', {}, mod) + expect(klass.new).to respond_to(:foo) end - it 'should not interpolate commands with :interpolate => false' do - # rubocop:disable Lint/InterpolationCheck - expect do |probe| - cmd = @set.command('thor', 'norse god', interpolate: false, &probe) + it "sets match on the new class" do + klass = subject.subclass('match', 'desc', {}, Module.new) + expect(klass.match).to eq('match') + end - cmd.new.process_line 'thor %(#{foo})' - end.to yield_with_args('%(#{foo})') - # rubocop:enable Lint/InterpolationCheck + it "sets description on the new class" do + klass = subject.subclass('match', 'desc', {}, Module.new) + expect(klass.description).to eq('desc') end - it 'should use shell-words to split strings' do - expect do |probe| - cmd = @set.command('eccentrica', &probe) + it "sets command options on the new class" do + klass = subject.subclass('match', 'desc', { foo: :bar }, Module.new) + expect(klass.command_options).to include(foo: :bar) + end - cmd.new.process_line %(eccentrica "gallumbits" 'erot''icon' 6) - end.to yield_with_args('gallumbits', 'eroticon', '6') + it "sets block on the new class" do + block = proc {} + klass = subject.subclass('match', 'desc', { foo: :bar }, Module.new, &block) + expect(klass.block).to eql(block) end + end - it 'should split on spaces if shellwords is not used' do - expect do |probe| - cmd = @set.command( - 'bugblatter-beast', 'would eat its grandmother', shellwords: false, &probe - ) + describe ".matches?" do + context "when given value matches command regex" do + before { subject.match('test-command') } - cmd.new.process_line %(bugblatter-beast "of traal") - end.to yield_with_args('"of', 'traal"') + it "returns true" do + expect(subject.matches?('test-command')).to be_truthy + end end - it 'should add captures to arguments for regex commands' do - expect do |probe| - cmd = @set.command(/perfectly (normal)( beast)?/i, &probe) - - cmd.new.process_line %(Perfectly Normal Beast (honest!)) - end.to yield_with_args('Normal', ' Beast', '(honest!)') + context "when given value doesn't match command regex" do + it "returns false" do + expect(subject.matches?('test-command')).to be_falsey + end end end - describe 'process_line' do - it 'should check for command name collisions if configured' do - old = Pry.config.collision_warning - Pry.config.collision_warning = true + describe ".match_score" do + context "when command regex matches given value" do + context "and when the size of last match is more than 1" do + before { subject.match(/\.(.*)/) } - cmd = @set.command '_frankie' do + it "returns the length of the first match" do + expect(subject.match_score('.||')).to eq(1) + end end - _frankie = 'boyle' - output = StringIO.new - cmd.new(target: binding, output: output).process_line %(_frankie mouse) + context "and when the size of last match is 1 or 0" do + before { subject.match('hi') } - expect(output.string).to match(/command .* conflicts/) - - Pry.config.collision_warning = old + it "returns the length of the last match" do + expect(subject.match_score('hi there')).to eq(2) + end + end end - it 'should spot collision warnings on assignment if configured' do - old = Pry.config.collision_warning - Pry.config.collision_warning = true - - cmd = @set.command 'frankie' do + context "when command regex doesn't match given value" do + it "returns -1" do + expect(subject.match_score('test')).to eq(-1) end + end + end - output = StringIO.new - cmd.new(target: binding, output: output).process_line %(frankie = mouse) + describe ".command_regex" do + before { subject.match('test-command') } - expect(output.string).to match(/command .* conflicts/) + context "when use_prefix is true" do + before { subject.command_options(use_prefix: true) } - Pry.config.collision_warning = old + it "returns a Regexp without a prefix" do + expect(subject.command_regex).to eq(/^test\-command(?!\S)/) + end end - it "should set the commands' arg_string and captures" do - inside = inner_scope do |probe| - cmd = @set.command(/benj(ie|ei)/, &probe) + context "when use_prefix is false" do + before { subject.command_options(use_prefix: false) } - cmd.new.process_line %(benjie mouse) + it "returns a Regexp with a prefix" do + expect(subject.command_regex).to eq(/^(?:)?test\-command(?!\S)/) end + end + end - expect(inside.arg_string).to eq("mouse") - expect(inside.captures).to eq(['ie']) + describe ".convert_to_regex" do + context "when given object is a String" do + it "escapes the string as a Regexp" do + expect(subject.convert_to_regex('foo.+')).to eq('foo\\.\\+') + end end - it "should raise an error if the line doesn't match the command" do - cmd = @set.command 'grunthos', 'the flatulent' - expect { cmd.new.process_line %(grumpos) }.to raise_error Pry::CommandError + context "when given object is an Object" do + let(:obj) { Object.new } + + it "returns the given object" do + expect(subject.convert_to_regex(obj)).to eql(obj) + end end end - describe "block parameters" do - before do - @context = Object.new - @set.command "walking-spanish", "down the hall", takes_block: true do - insert_variable(:@x, command_block.call, target) + describe ".group" do + context "when name is given" do + it "sets group to that name" do + expect(subject.group('Test Group')).to eq('Test Group') end - @set.import Pry::Commands - - @t = pry_tester(@context, commands: @set) end - it 'should accept multiline blocks' do - @t.eval <<-COMMAND - walking-spanish | do - :jesus - end - COMMAND + context "when source file matches a pry command" do + before do + expect_any_instance_of(Pry::Method).to receive(:source_file) + .and_return('/pry/test_commands/test_command.rb') + end - expect(@context.instance_variable_get(:@x)).to eq :jesus + it "sets group name to command name" do + expect(subject.group).to eq('Test command') + end end - it 'should accept normal parameters along with block' do - @set.block_command "walking-spanish", - "litella's been screeching for a blind pig.", - takes_block: true do |x, y| - insert_variable(:@x, x, target) - insert_variable(:@y, y, target) - insert_variable(:@block_var, command_block.call, target) + context "when source file matches a pry plugin" do + before do + expect_any_instance_of(Pry::Method).to receive(:source_file) + .and_return('pry-test-1.2.3') end - @t.eval 'walking-spanish john carl| { :jesus }' - - expect(@context.instance_variable_get(:@x)).to eq "john" - expect(@context.instance_variable_get(:@y)).to eq "carl" - expect(@context.instance_variable_get(:@block_var)).to eq :jesus + it "sets group name to plugin name" do + expect(subject.group).to eq('pry-test (v1.2.3)') + end end - describe "single line blocks" do - it 'should accept blocks with do ; end' do - @t.eval 'walking-spanish | do ; :jesus; end' - expect(@context.instance_variable_get(:@x)).to eq :jesus + context "when source file matches 'pryrc'" do + before do + expect_any_instance_of(Pry::Method).to receive(:source_file) + .and_return('pryrc') end - it 'should accept blocks with do; end' do - @t.eval 'walking-spanish | do; :jesus; end' - expect(@context.instance_variable_get(:@x)).to eq :jesus + it "sets group name to pryrc" do + expect(subject.group).to eq('pryrc') end + end - it 'should accept blocks with { }' do - @t.eval 'walking-spanish | { :jesus }' - expect(@context.instance_variable_get(:@x)).to eq :jesus + context "when source file doesn't match anything" do + it "returns '(other)'" do + expect(subject.group).to eq('(other)') end end + end - describe "block-related content removed from arguments" do - describe "arg_string" do - it 'should remove block-related content from arg_string (with one normal arg)' do - @set.block_command( - "walking-spanish", "down the hall", takes_block: true - ) do |x, _y| - insert_variable(:@arg_string, arg_string, target) - insert_variable(:@x, x, target) - end - - @t.eval 'walking-spanish john| { :jesus }' - - expect(@context.instance_variable_get(:@arg_string)) - .to eq(@context.instance_variable_get(:@x)) - end - - it 'should remove block-related content from arg_string (with no normal args)' do - @set.block_command "walking-spanish", "down the hall", takes_block: true do - insert_variable(:@arg_string, arg_string, target) - end - - @t.eval 'walking-spanish | { :jesus }' - - expect(@context.instance_variable_get(:@arg_string)).to eq "" - end + describe "#run" do + let(:command_set) do + set = Pry::CommandSet.new + set.command('test') {} + set + end - it( - "doesn't remove block-related content from arg_string " \ - "when :takes_block => false" - ) do - block_string = "| { :jesus }" - @set.block_command "walking-spanish", "homemade special", takes_block: false do - insert_variable(:@arg_string, arg_string, target) - end + subject do + command = Class.new(described_class) + command.new(command_set: command_set, pry_instance: Pry.new) + end - @t.eval "walking-spanish #{block_string}" + it "runs a command from another command" do + result = subject.run('test') + expect(result).to be_command + end + end - expect(@context.instance_variable_get(:@arg_string)).to eq block_string - end + describe "#commands" do + let(:command_set) do + set = Pry::CommandSet.new + set.command('test') do + def process; end end + set + end - describe "args" do - describe "block_command" do - it "should remove block-related content from arguments" do - @set.block_command( - "walking-spanish", "glass is full of sand", takes_block: true - ) do |x, y| - insert_variable(:@x, x, target) - insert_variable(:@y, y, target) - end - - @t.eval 'walking-spanish | { :jesus }' + subject do + command = Class.new(described_class) + command.new(command_set: command_set, pry_instance: Pry.new) + end - expect(@context.instance_variable_get(:@x)).to eq nil - expect(@context.instance_variable_get(:@y)).to eq nil - end + it "returns command set as a hash" do + expect(subject.commands).to eq('test' => command_set['test']) + end + end - it( - "doesn't remove block-related content from arguments if :takes_block => false" - ) do - @set.block_command( - "walking-spanish", "litella screeching for a blind pig", takes_block: false - ) do |x, y| - insert_variable(:@x, x, target) - insert_variable(:@y, y, target) - end + describe "#void" do + it "returns void value" do + expect(subject.new.void).to eq(Pry::Command::VOID_VALUE) + end + end - @t.eval 'walking-spanish | { :jesus }' + describe "#target_self" do + let(:target) { binding } - expect(@context.instance_variable_get(:@x)).to eq "|" - expect(@context.instance_variable_get(:@y)).to eq "{" - end - end + subject { Class.new(described_class).new(target: target) } - describe "create_command" do - it "should remove block-related content from arguments" do - @set.create_command( - "walking-spanish", "punk sanders carved one out of wood", takes_block: true - ) do - def process(x, y) # rubocop:disable Naming/UncommunicativeMethodParamName - insert_variable(:@x, x, target) - insert_variable(:@y, y, target) - end - end - - @t.eval 'walking-spanish | { :jesus }' - - expect(@context.instance_variable_get(:@x)).to eq nil - expect(@context.instance_variable_get(:@y)).to eq nil - end + it "returns the value of self inside the target binding" do + expect(subject.target_self).to eq(target.eval('self')) + end + end - it( - "doesn't remove block-related content from arguments if :takes_block => false" - ) do - @set.create_command "walking-spanish", "down the hall", takes_block: false do - def process(x, y) # rubocop:disable Naming/UncommunicativeMethodParamName - insert_variable(:@x, x, target) - insert_variable(:@y, y, target) - end - end + describe "#state" do + let(:target) { binding } - @t.eval 'walking-spanish | { :jesus }' + subject { Class.new(described_class).new(pry_instance: Pry.new) } - expect(@context.instance_variable_get(:@x)).to eq "|" - expect(@context.instance_variable_get(:@y)).to eq "{" - end - end - end + it "returns a state hash" do + expect(subject.state).to be_a(Pry::Config) end - describe "blocks can take parameters" do - describe "{} style blocks" do - it 'should accept multiple parameters' do - @set.block_command "walking-spanish", "down the hall", takes_block: true do - insert_variable(:@x, command_block.call(1, 2), target) - end - - @t.eval 'walking-spanish | { |x, y| [x, y] }' + it "remembers the state" do + subject.state[:foo] = :bar + expect(subject.state[:foo]).to eq(:bar) + end + end - expect(@context.instance_variable_get(:@x)).to eq [1, 2] - end + describe "#interpolate_string" do + context "when given string contains \#{" do + let(:target) do + foo = 'bar' + binding end - describe "do/end style blocks" do - it 'should accept multiple parameters' do - @set.create_command "walking-spanish", "litella", takes_block: true do - def process - insert_variable(:@x, command_block.call(1, 2), target) - end - end + subject { Class.new(described_class).new(target: target) } - @t.eval <<-COMMAND - walking-spanish | do |x, y| - [x, y] - end - COMMAND + it "returns the result of eval within target" do + # rubocop:disable Lint/InterpolationCheck + expect(subject.interpolate_string('#{foo}')).to eq('bar') + # rubocop:enable Lint/InterpolationCheck + end + end - expect(@context.instance_variable_get(:@x)).to eq [1, 2] - end + context "when given string doesn't contain \#{" do + it "returns the given string" do + expect(subject.new.interpolate_string('foo')).to eq('foo') end end + end - describe "closure behaviour" do - it 'should close over locals in the definition context' do - @t.eval 'var = :hello', 'walking-spanish | { var }' - expect(@context.instance_variable_get(:@x)).to eq :hello + describe "#check_for_command_collision" do + let(:command_set) do + set = Pry::CommandSet.new + set.command('test') do + def process; end end + set end - describe "exposing block parameter" do - describe "block_command" do - it "should expose block in command_block method" do - @set.block_command "walking-spanish", "glass full of sand", takes_block: true do - insert_variable(:@x, command_block.call, target) - end + let(:output) { StringIO.new } - @t.eval 'walking-spanish | { :jesus }' + subject do + command = Class.new(described_class) + command.new(command_set: command_set, target: target, output: output) + end - expect(@context.instance_variable_get(:@x)).to eq :jesus - end + context "when a command collides with a local variable" do + let(:target) do + test = 'foo' + binding end - describe "create_command" do - it "should NOT expose &block in create_command's process method" do - @set.create_command "walking-spanish", "down the hall", takes_block: true do - def process(&block) - block.call # rubocop:disable Performance/RedundantBlockCall - end - end - @out = StringIO.new + it "displays a warning" do + subject.check_for_command_collision('test', '') + expect(output.string) + .to match("'test', which conflicts with a local-variable") + end + end - expect { @t.eval 'walking-spanish | { :jesus }' }.to raise_error(NoMethodError) - end + context "when a command collides with a method" do + let(:target) do + def test; end + binding + end - it "should expose block in command_block method" do - @set.create_command "walking-spanish", "homemade special", takes_block: true do - def process - insert_variable(:@x, command_block.call, target) - end - end + it "displays a warning" do + subject.check_for_command_collision('test', '') + expect(output.string).to match("'test', which conflicts with a method") + end + end - @t.eval 'walking-spanish | { :jesus }' + context "when a command doesn't collide" do + let(:target) do + def test; end + binding + end - expect(@context.instance_variable_get(:@x)).to eq :jesus - end + it "doesn't display a warning" do + subject.check_for_command_collision('nothing', '') + expect(output.string).to be_empty end end end - describe "a command made with a custom sub-class" do - before do - class MyTestCommand < Pry::ClassCommand - match(/my-*test/) - description 'So just how many sound technicians does it take to' \ - 'change a lightbulb? 1? 2? 3? 1-2-3? Testing?' - options shellwords: false, listing: 'my-test' + describe "#tokenize" do + let(:target) { binding } + let(:klass) { Class.new(described_class) } + let(:target) { binding } - undef process if method_defined? :process + subject { klass.new(target: target) } - def process - output.puts command_name * 2 - end + before { klass.match('test') } + + context "when given string uses interpolation" do + let(:target) do + foo = 4 + binding end - Pry.config.commands.add_command MyTestCommand - end + before { klass.command_options(interpolate: true) } - after do - Pry.config.commands.delete 'my-test' - end + it "interpolates the string in the target's context" do + # rubocop:disable Lint/InterpolationCheck + expect(subject.tokenize('test #{1 + 2} #{3 + foo}')) + .to eq(['test', '3 7', [], %w[3 7]]) + # rubocop:enable Lint/InterpolationCheck + end - it "allows creation of custom subclasses of Pry::Command" do - expect(pry_eval('my---test')).to match(/my-testmy-test/) - end + context "and when interpolation is disabled" do + before { klass.command_options(interpolate: false) } - it "shows the source of the process method" do - expect(pry_eval('show-source my-test')).to match(/output.puts command_name/) + it "doesn't interpolate the string" do + # rubocop:disable Lint/InterpolationCheck + expect(subject.tokenize('test #{3 + foo}')) + .to eq(['test', '#{3 + foo}', [], %w[#{3 + foo}]]) + # rubocop:enable Lint/InterpolationCheck + end + end end - describe "command options hash" do - it "is always present" do - options_hash = { - keep_retval: false, - argument_required: false, - interpolate: true, - shellwords: false, - listing: 'my-test', - use_prefix: true, - takes_block: false - } - expect(MyTestCommand.options).to eq options_hash + context "when given string doesn't match a command" do + it "raises CommandError" do + expect { subject.tokenize('boom') } + .to raise_error(Pry::CommandError, /command which didn't match/) end + end - describe ":listing option" do - it "defaults to :match if not set explicitly" do - class HappyNewYear < Pry::ClassCommand - match 'happy-new-year' - description 'Happy New Year 2013' - end - Pry.config.commands.add_command HappyNewYear + context "when target is not set" do + subject { klass.new } - expect(HappyNewYear.options[:listing]).to eq 'happy-new-year' + it "still returns tokens" do + expect(subject.tokenize('test --help')) + .to eq(['test', '--help', [], ['--help']]) + end + end - Pry.config.commands.delete 'happy-new-year' - end + context "when shellwords is enabled" do + before { klass.command_options(shellwords: true) } - it "can be set explicitly" do - class MerryChristmas < Pry::ClassCommand - match 'merry-christmas' - description 'Merry Christmas!' - command_options listing: 'happy-holidays' - end - Pry.config.commands.add_command MerryChristmas - - expect(MerryChristmas.options[:listing]).to eq 'happy-holidays' + it "strips quotes from the arguments" do + expect(subject.tokenize(%(test "foo" 'bar' 1))) + .to eq(['test', %("foo" 'bar' 1), [], %w[foo bar 1]]) + end + end - Pry.config.commands.delete 'merry-christmas' - end + context "when shellwords is disabled" do + before { klass.command_options(shellwords: false) } - it "equals to :match option's inspect, if :match is Regexp" do - class CoolWinter < Pry::ClassCommand - match(/.*winter/) - description 'Is winter cool or cool?' - end - Pry.config.commands.add_command CoolWinter + it "doesn't split quotes from the arguments" do + # rubocop:disable Lint/PercentStringArray + expect(subject.tokenize(%(test "foo" 'bar' 1))) + .to eq(['test', %("foo" 'bar' 1), [], %w["foo" 'bar' 1]]) + # rubocop:enable Lint/PercentStringArray + end + end - expect(CoolWinter.options[:listing]).to eq '/.*winter/' + context "when command regex has captures" do + before { klass.match(/perfectly (normal)( beast)/i) } - Pry.config.commands.delete(/.*winter/) - end + it "returns the captures" do + expect(subject.tokenize('Perfectly Normal Beast (honest!)')).to eq( + [ + 'Perfectly Normal Beast', + '(honest!)', + ['Normal', ' Beast'], + ['(honest!)'] + ] + ) end end end - describe "commands can save state" do - before do - @set = Pry::CommandSet.new do - create_command "litella", "desc" do - def process - state.my_state ||= 0 - state.my_state += 1 - end - end - - create_command "sanders", "desc" do - def process - state.my_state = "wood" - end - end - - create_command(/[Hh]ello-world/, "desc") do - def process - state.my_state ||= 0 - state.my_state += 2 - end - end - end.import Pry::Commands - - @t = pry_tester(commands: @set) + describe "#process_line" do + let(:klass) do + Class.new(described_class) do + def call(*args); end + end end - it 'should save state for the command on the Pry#command_state hash' do - @t.eval 'litella' - expect(@t.pry.command_state["litella"].my_state).to eq 1 + let(:target) do + test = 4 + binding end - it 'should ensure state is maintained between multiple invocations of command' do - @t.eval 'litella' - @t.eval 'litella' - expect(@t.pry.command_state["litella"].my_state).to eq 2 - end + let(:output) { StringIO.new } + + subject { klass.new(target: target, output: output) } - it 'should ensure state with same name stored seperately for each command' do - @t.eval 'litella', 'sanders' + before { klass.match(/test(y)?/) } - expect(@t.pry.command_state["litella"].my_state).to eq 1 - expect(@t.pry.command_state["sanders"].my_state).to eq("wood") + it "sets arg_string" do + subject.process_line('test -v') + expect(subject.arg_string).to eq('-v') end - it 'should ensure state is properly saved for regex commands' do - @t.eval 'hello-world', 'Hello-world' - expect(@t.pry.command_state[/[Hh]ello-world/].my_state).to eq 4 + it "sets captures" do + subject.process_line('testy') + expect(subject.captures).to eq(['y']) end - end - if defined?(Bond) - describe 'complete' do - it 'should return the arguments that are defined' do - @set.create_command "torrid" do - def options(opt) - opt.on :test - opt.on :lest - opt.on :pests - end + describe "collision warnings" do + context "when collision warnings are configured" do + before do + expect(Pry.config).to receive(:collision_warning).and_return(true) end - expect(@set.complete('torrid ')).to.include('--test ') + it "prints a warning when there's a collision" do + subject.process_line('test') + expect(output.string).to match(/conflicts with a local-variable/) + end end - end - end - describe 'group' do - before do - @set.import( - Pry::CommandSet.new do - create_command("magic") { group("Not for a public use") } + context "when collision warnings are not set" do + before do + expect(Pry.config).to receive(:collision_warning).and_return(false) end - ) - end - - it 'should be correct for default commands' do - expect(@set["help"].group).to eq "Help" - end - it 'should not change once it is initialized' do - @set["magic"].group("-==CD COMMAND==-") - expect(@set["magic"].group).to eq "Not for a public use" + it "prints a warning when there's a collision" do + subject.process_line('test') + expect(output.string).to be_empty + end + end end + end - it 'should not disappear after the call without parameters' do - @set["magic"].group - expect(@set["magic"].group).to eq "Not for a public use" + describe "#complete" do + it "returns empty array" do + expect(subject.new.complete('')).to eq([]) end end end |