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:
parent
765515eb29
commit
cf786881bb
2 changed files with 256 additions and 22 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue