summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyrylo Silin <kyrylosilin@gmail.com>2012-08-08 07:08:30 +0300
committerKyrylo Silin <kyrylosilin@gmail.com>2012-11-21 04:54:43 +0200
commite2f95629e729a527a6ba5b41241c053ba2bb0803 (patch)
tree33f030051c8f61e987c2792e8161f4beb4a3b6f9
parent219b2ab79067ed311c8b9856171f59125407444c (diff)
downloadpry-e2f95629e729a527a6ba5b41241c053ba2bb0803.tar.gz
Extend ClassCommand so it can define sub commands
Create `ClassCommand::Options` class, which ties up sub commands and default options together. Let's consider the command `food make --tea`. `food` is a command, `make` is a sub command and `--tea` is an option of `make` sub command. We can access `--tea` via `opts[:make][:tea]. Also, we can check the freshness of our food like so: `food --freshness`. `--freshness` is a default option. We can access it like so: `opts.freshness?` or `opts[:freshness]`. Add unit tests for `ClassCommand::Option` and some other tests that reflect the additions. Finally, document everything and fix different typos in the existing documentation. Signed-off-by: Kyrylo Silin <kyrylosilin@gmail.com>
-rw-r--r--lib/pry/command.rb165
-rw-r--r--spec/command_spec.rb113
2 files changed, 256 insertions, 22 deletions
diff --git a/lib/pry/command.rb b/lib/pry/command.rb
index 63a141b4..95b6256a 100644
--- a/lib/pry/command.rb
+++ b/lib/pry/command.rb
@@ -1,3 +1,4 @@
+require 'delegate'
require 'pry/helpers/documentation_helpers'
class Pry
@@ -472,19 +473,141 @@ class Pry
# gems your command needs to run, or to set up state.
class ClassCommand < Command
+ # The class that couples together sub commands and top-level options (that
+ # are known as "default" options). The explicitly defined instance methods
+ # of this class provide the coupling with default options of a
+ # Slop::Commands instance. An instance of this class delegates all remaining
+ # methods to an instance of Slop::Commands class.
+ #
+ # @example
+ # # Define Slop commands.
+ # commands = Slop::Commands.new do |cmd|
+ # cmd.on :action do
+ # on :f, :force, "Use force"
+ # end
+ #
+ # cmd.default do
+ # on :v, :verbose, "Verbose mode"
+ # end
+ # end
+ #
+ # # Pass Slop commands as an argument to Options class.
+ # opts = Options.new(Slop::Commands.new)
+ # opts.default
+ # # => #<Slop ...>
+ #
+ # # Parse sub commands.
+ # opts.parse %'action --force'
+ # opts[:action].present?(:force)
+ # # => true
+ # opts.present?(:force)
+ # # => false
+ #
+ # # Parse default options.
+ # opts.parse %'--verbose'
+ # opts.verbose?
+ # # => true
+ # opts[:action].present?(:verbose)
+ # # => false
+ # opts.verbose
+ # # => NoMethodError
+ class Options < SimpleDelegator
+
+ # @param [Slop::Commands] opts The sub commands and options.
+ # @raise [ArgumentError] if the +opts+ isn't a kind of Slop::Commands.
+ # instance.
+ def initialize(opts)
+ unless opts.kind_of?(Slop::Commands)
+ raise ArgumentError, "Expected an instance of Slop::Command, not #{opts.class} one"
+ end
+ super
+ end
+
+ # Fetch the instance of Slop tied to a command or fetch an options
+ # argument value.
+ #
+ # If the +key+ doesn't correspond to any of the sub commands, the method
+ # tries to find the same +key+ in the list of default options.
+ #
+ # @example
+ # # A sub command example.
+ # opts = Options.new(commands)
+ # opts.parse %w'download video.ogv'
+ #
+ # opts[:download]
+ # # => #<Slop ...>
+ #
+ # # A default option example.
+ # opts = Options.new(commands)
+ # opts.parse %w'--host=localhost download video.ogv'
+ # opts[:host]
+ # # => true
+ #
+ # @param [String, Symbol] key The sub command name or the default option.
+ # @return [Slop, Boolean, nil] Either instance of Slop tied to the
+ # command, if any; or `true`, if the default option has the given +key+;
+ # or nil, if can't find the +key+.
+ # @note The method never returns `false`.
+ def [](key)
+ if command_key = self.get(key)
+ command_key
+ else
+ default.get(key)
+ end
+ end
+
+ # Check for a default options presence.
+ #
+ # @param [String, Symbol] keys The list of keys to check.
+ # @return [Boolean] Whether all of the +keys+ are present in the parsed
+ # arguments.
+ def present?(*keys)
+ default.present?(*keys)
+ end
+
+ # Convenience method for {#present?}.
+ #
+ # @example
+ # opts.parse %w'--verbose'
+ # opts.verbose?
+ # # => true
+ # opts.terse?
+ # # => false
+ #
+ # @return [Boolean, void] On condition of +method_name+ ends with a
+ # question mark returns `true`, if the _default option_ is present (and
+ # `false`, if not). Otherwise, calls `super`.
+ def method_missing(method_name, *args, &block)
+ name = method_name.to_s
+ if name.end_with?("?")
+ present?(name.chop)
+ else
+ super
+ end
+ end
+
+ private
+
+ # @return [Slop] The instance of Slop representing default options.
+ def default
+ __getobj__[:default]
+ end
+
+ end
+
attr_accessor :opts
attr_accessor :args
# Set up `opts` and `args`, and then call `process`.
#
- # This function will display help if necessary.
+ # 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.opts = Options.new(slop)
self.args = self.opts.parse!(args)
if opts.present?(:help)
@@ -500,13 +623,19 @@ class Pry
slop.help
end
- # Return an instance of Slop that can parse the options that this command accepts.
+ # Return an instance of Slop::Commands that can parse either sub commands
+ # or the options that this command accepts.
def slop
- Slop.new do |opt|
+ opts = proc do |opt|
opt.banner(unindent(self.class.banner))
options(opt)
opt.on(:h, :help, "Show this message.")
end
+
+ Slop::Commands.new do |cmd|
+ sub_commands(cmd)
+ cmd.default { |opt| opts.call(opt) }
+ end
end
# Generate shell completions
@@ -518,23 +647,35 @@ class Pry
end.flatten(1).compact + super
end
- # A function called just before `options(opt)` as part of `call`.
+ # A method called just before `options(opt)` as part of `call`.
#
- # This function can be used to set up any context your command needs to run, for example
- # requiring gems, or setting default values for options.
+ # 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;
+ # def setup
# require 'gist'
# @action = :method
# end
def setup; end
- # A function to setup Slop so it can parse the options your command expects.
+ # A method to setup Slop::Commands so it can parse the sub commands your
+ # command expects. If you need to set up default values, use `setup`
+ # instead.
+ #
+ # @example
+ # def sub_commands(cmd)
+ # cmd.on(:d, :download, "Download a content from a server.") do
+ # @action = :download
+ # end
+ # end
+ def sub_commands(cmd); end
+
+ # A method to setup Slop so it can parse the options your command expects.
#
- # NOTE: please don't do anything side-effecty in the main part of this method,
- # as it may be called by Pry at any time for introspection reasons. If you need
- # to set up default values, use `setup` instead.
+ # @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)
diff --git a/spec/command_spec.rb b/spec/command_spec.rb
index 7c037e74..ba127733 100644
--- a/spec/command_spec.rb
+++ b/spec/command_spec.rb
@@ -138,7 +138,6 @@ describe "Pry::Command" do
end
end
-
describe 'context' do
context = {
:target => binding,
@@ -168,14 +167,91 @@ describe "Pry::Command" do
end
end
+ describe Pry::ClassCommand::Options do
+ before do
+ Options = Pry::ClassCommand::Options
+
+ commands = Slop::Commands.new do |cmd|
+ cmd.on :boom do
+ on :v, :verbose, "Verbose boom!"
+ end
+
+ cmd.default do
+ on :n, :nothing, "Do nothing"
+ end
+ end
+
+ @opts = Options.new(commands)
+ end
+
+ describe '#new arguments' do
+ it 'should accept objects that are kind of Slop::Commands as an argument' do
+ class MyCommands < Slop::Commands
+ end
+
+ lambda { Options.new(MyCommands.new) }.should.not.raise ArgumentError
+ end
+
+ it 'should raise ArgumentError if the argument is not kind of Slop::Commands' do
+ lambda { Options.new(Array.new) }.should.raise ArgumentError
+ end
+ end
+
+ describe '#[] method' do
+ it 'should fetch commands' do
+ @opts[:boom].should.be.kind_of Slop
+ end
+
+ it 'should parse default options, if cannot fetch a command' do
+ @opts.parse %w'--nothing'
+
+ @opts[:nothing].should == true
+ @opts[:nothing].should == @opts[:default][:nothing]
+ end
+
+ it 'should return nil if cannot find neither a command nor a default option' do
+ @opts.parse %w'--something'
+
+ @opts[:something].should == nil
+ @opts[:something].should == @opts[:default][:something]
+ end
+ end
+
+ it 'should forward implicitly defined methods to Slop::Commands' do
+ opts = Options.new(Slop::Commands.new)
+ opts.global { on "--something" }
+ opts.parse %w'--something'
+
+ opts[:global][:something].should == true
+ end
+
+ it 'should check for a default options presence' do
+ @opts.parse %w'--nothing'
+
+ @opts.present?(:nothing).should == true
+ @opts.present?(:anything).should == false
+ end
+
+ it "should call #present? on NoMethodError, if the caller's name ends with '?'" do
+ @opts.parse %w'--nothing'
+
+ @opts.nothing?.should == true
+ @opts.anything?.should == false
+ end
+ end
+
describe 'classy api' do
- it 'should call setup, then options, then process' do
+ it 'should call setup, then sub_commands, then options, then process' do
cmd = @set.create_command 'rooster', "Has a tasty towel" do
def setup
output.puts "setup"
end
+ def sub_commands(cmd)
+ output.puts "sub_commands"
+ end
+
def options(opt)
output.puts "options"
end
@@ -185,7 +261,7 @@ describe "Pry::Command" do
end
end
- mock_command(cmd).output.should == "setup\noptions\nprocess\n"
+ mock_command(cmd).output.should == "setup\nsub_commands\noptions\nprocess\n"
end
it 'should raise a command error if process is not overridden' do
@@ -212,19 +288,36 @@ describe "Pry::Command" do
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
+ def options(opt)
+ opt.on :f, :four, "A numeric four", :as => Integer, :optional_argument => true
+ end
- def process
- args.should == ['four']
- opts[:f].should == 4
- end
+ def process
+ args.should == ['four']
+ opts[:f].should == 4
+ end
end
mock_command(cmd, %w(--four 4 four))
end
+ it 'should provide cmds and args as provided by slop' do
+ cmd = @set.create_command 'dichlorvos', 'Kill insects' do
+ def sub_commands(cmd)
+ cmd.on :kill do
+ on :i, :insect, "An insect."
+ end
+ end
+
+ def process
+ args.should == ["ant"]
+ opts[:kill][:insect].should == true
+ end
+ end
+
+ mock_command(cmd, %w(kill --insect ant))
+ end
+
it 'should allow overriding options after definition' do
cmd = @set.create_command /number-(one|two)/, "Lieutenants of the Golgafrinchan Captain", :shellwords => false do