summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyrylo Silin <silin@kyrylo.org>2019-04-09 03:02:54 +0300
committerGitHub <noreply@github.com>2019-04-09 03:02:54 +0300
commitf7f05fc703d6eab387dfbf484c33ae71fa70c308 (patch)
tree12f7629122fecefd17d8981033a71010fbe2e2f7
parentb7817c4015fcb0bf26852fd969921ca23ca97b48 (diff)
parentc6bc5eb6554d38c3c96fcc188944f8c1c3e460c1 (diff)
downloadpry-f7f05fc703d6eab387dfbf484c33ae71fa70c308.tar.gz
Merge pull request #2003 from pry/command-refactoring
Command class refactoring & adding tests
-rw-r--r--CHANGELOG.md2
-rw-r--r--lib/pry.rb2
-rw-r--r--lib/pry/block_command.rb20
-rw-r--r--lib/pry/class_command.rb192
-rw-r--r--lib/pry/command.rb360
-rw-r--r--spec/block_command_spec.rb63
-rw-r--r--spec/class_command_spec.rb264
-rw-r--r--spec/command_spec.rb1093
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
diff --git a/lib/pry.rb b/lib/pry.rb
index 251271c0..51227e59 100644
--- a/lib/pry.rb
+++ b/lib/pry.rb
@@ -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