From cf786881bb91977a26d754c99580a6ab742f6b26 Mon Sep 17 00:00:00 2001 From: Kyrylo Silin Date: Wed, 8 Aug 2012 07:08:30 +0300 Subject: [PATCH] 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 --- lib/pry/command.rb | 165 +++++++++++++++++++++++++++++++++++++++---- spec/command_spec.rb | 113 ++++++++++++++++++++++++++--- 2 files changed, 256 insertions(+), 22 deletions(-) diff --git a/lib/pry/command.rb b/lib/pry/command.rb index b10b6147..c938d3fb 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -1,3 +1,4 @@ +require 'delegate' require 'pry/helpers/documentation_helpers' class Pry @@ -471,19 +472,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 + # # => # + # + # # 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] + # # => # + # + # # 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] 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) @@ -499,13 +622,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 @@ -517,23 +646,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. # - # 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 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. # # @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