1
0
Fork 0
mirror of https://github.com/pry/pry.git synced 2022-11-09 12:35:05 -05:00

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>
This commit is contained in:
Kyrylo Silin 2012-08-08 07:08:30 +03:00
parent 765515eb29
commit cf786881bb
2 changed files with 256 additions and 22 deletions

View file

@ -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
# # => #<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)
@ -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)

View file

@ -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