Permalink
Browse files

Changes in semantics:

* unknown options are no longer ignored; instead, they are
  included in the arguments Array. This does not change the
  behavior if check_unknown_options! is on (an Exception is
  still raised).
* boolean, string, and number option types can now be
  interspersed with arguments. For instance, if --foo is a
  boolean argument, `script task --foo bar` will pass "bar"
  as an argument to the task.
* Thor::Group receives undeclared arguments and unknown
  options in the `args` accessor
* TODO: Significantly more unit test coverage of the option
  parsing classes.
  • Loading branch information...
1 parent 1d5b217 commit 0cfe4afaec9217eac864885c939b4996bafdc0c6 @wycats wycats committed Jul 3, 2011
View
@@ -259,8 +259,10 @@ def dispatch(meth, given_args, given_opts, config) #:nodoc:
opts = given_opts || opts || []
config.merge!(:current_task => task, :task_options => task.options)
+ instance = new(args, opts, config)
+ args = instance.args
trailing = args[Range.new(arguments.size, -1)]
- new(args, opts, config).invoke_task(task, trailing || [])
+ instance.invoke_task(task, trailing || [])
end
# The banner for this class. You can customize it if you are invoking the
View
@@ -19,7 +19,7 @@ class Thor
action add_file create_file in_root inside run run_ruby_script)
module Base
- attr_accessor :options
+ attr_accessor :options, :args
# It receives arguments in an Array and two hashes, one for options and
# other for configuration.
@@ -38,22 +38,43 @@ module Base
# config<Hash>:: Configuration for this Thor class.
#
def initialize(args=[], options={}, config={})
- args = Thor::Arguments.parse(self.class.arguments, args)
- args.each { |key, value| send("#{key}=", value) }
-
parse_options = self.class.class_options
+ # The start method splits inbound arguments at the first argument
+ # that looks like an option (starts with - or --). It then calls
+ # new, passing in the two halves of the arguments Array as the
+ # first two parameters.
+
if options.is_a?(Array)
task_options = config.delete(:task_options) # hook for start
parse_options = parse_options.merge(task_options) if task_options
array_options, hash_options = options, {}
else
+ # Handle the case where the class was explicitly instantiated
+ # with pre-parsed options.
array_options, hash_options = [], options
end
+ # Let Thor::Options parse the options first, so it can remove
+ # declared options from the array. This will leave us with
+ # a list of arguments that weren't declared.
opts = Thor::Options.new(parse_options, hash_options)
self.options = opts.parse(array_options)
+
+ # If unknown options are disallowed, make sure that none of the
+ # remaining arguments looks like an option.
opts.check_unknown! if self.class.check_unknown_options?(config)
+
+ # Add the remaining arguments from the options parser to the
+ # arguments passed in to initialize. Then remove any positional
+ # arguments declared using #argument (this is primarily used
+ # by Thor::Group). Tis will leave us with the remaining
+ # positional arguments.
+ thor_args = Thor::Arguments.new(self.class.arguments)
+ thor_args.parse(args + opts.remaining).each { |k,v| send("#{k}=", v) }
+ args = thor_args.remaining
+
+ @args = args
end
class << self
View
@@ -220,10 +220,13 @@ def dispatch(task, given_args, given_opts, config) #:nodoc:
args, opts = Thor::Options.split(given_args)
opts = given_opts || opts
+ instance = new(args, opts, config)
+ args = instance.args
+
if task
- new(args, opts, config).invoke_task(all_tasks[task])
+ instance.invoke_task(all_tasks[task])
else
- new(args, opts, config).invoke_all
+ instance.invoke_all
end
end
@@ -49,6 +49,10 @@ def parse(args)
@assigns
end
+ def remaining
+ @pile
+ end
+
private
def no_or_skip?(arg)
View
@@ -38,7 +38,7 @@ def initialize(hash_options={}, defaults={})
@non_assigned_required.delete(hash_options[key])
end
- @shorts, @switches, @unknown = {}, {}, []
+ @shorts, @switches, @extra = {}, {}, []
options.each do |option|
@switches[option.switch_name] = option
@@ -49,14 +49,19 @@ def initialize(hash_options={}, defaults={})
end
end
+ def remaining
+ @extra
+ end
+
def parse(args)
@pile = args.dup
while peek
match, is_switch = current_is_switch?
+ shifted = shift
if is_switch
- case shift
+ case shifted
when SHORT_SQ_RE
unshift($1.split('').map { |f| "-#{f}" })
next
@@ -71,9 +76,10 @@ def parse(args)
option = switch_option(switch)
@assigns[option.human_name] = parse_peek(switch, option)
elsif match
- @unknown << shift
+ @extra << shifted
+ @extra << shift while peek && peek !~ /^-/
else
- shift
+ @extra << shifted
end
end
@@ -85,7 +91,9 @@ def parse(args)
end
def check_unknown!
- raise UnknownArgumentError, "Unknown switches '#{@unknown.join(', ')}'" unless @unknown.empty?
+ # an unknown option starts with - or -- and has no more -'s afterward.
+ unknown = @extra.select { |str| str =~ /^--?[^-]*$/ }
+ raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
end
protected
View
@@ -252,7 +252,7 @@ def hello
it "checks unknown options except specified" do
capture(:stderr) {
- MyScript.start(["with_optional", "NAME", "--omg", "--invalid"]).should == ["NAME", {}]
+ MyScript.start(["with_optional", "NAME", "--omg", "--invalid"]).should == ["NAME", {}, ["--omg", "--invalid"]]
}.strip.should be_empty
end
end
@@ -77,8 +77,8 @@ END
method_option :lazy_array, :type => :array, :lazy_default => %w[eat at joes]
method_option :lazy_hash, :type => :hash, :lazy_default => {'swedish' => 'meatballs'}
desc "with_optional NAME", "invoke with optional name"
- def with_optional(name=nil)
- [ name, options ]
+ def with_optional(name=nil, *args)
+ [ name, options, args ]
end
class AnotherScript < Thor
View
@@ -175,4 +175,42 @@
end
end
end
+
+ describe "edge-cases" do
+ it "can handle boolean options followed by arguments" do
+ klass = Class.new(Thor::Group) do
+ desc "say hi to name"
+ argument :name, :type => :string
+ class_option :loud, :type => :boolean
+
+ def hi
+ name.upcase! if options[:loud]
+ "Hi #{name}"
+ end
+ end
+
+ klass.start(["jose"]).should == ["Hi jose"]
+ klass.start(["jose", "--loud"]).should == ["Hi JOSE"]
+ klass.start(["--loud", "jose"]).should == ["Hi JOSE"]
+ end
+
+ it "provides extra args as `args`" do
+ klass = Class.new(Thor::Group) do
+ desc "say hi to name"
+ argument :name, :type => :string
+ class_option :loud, :type => :boolean
+
+ def hi
+ name.upcase! if options[:loud]
+ out = "Hi #{name}"
+ out << ": " << args.join(", ") unless args.empty?
+ out
+ end
+ end
+
+ klass.start(["jose"]).should == ["Hi jose"]
+ klass.start(["jose", "--loud"]).should == ["Hi JOSE"]
+ klass.start(["--loud", "jose"]).should == ["Hi JOSE"]
+ end
+ end
end
@@ -262,6 +262,12 @@ def check_unknown!
parse("--no-foo-bar")["foo_bar"].should == false
parse("--skip-foo-bar")["foo_bar"].should == false
end
+
+ it "doesn't eat the next part of the param" do
+ create :foo => :boolean
+ parse("--foo", "bar").should == {"foo" => true}
+ @opt.remaining.should == ["bar"]
+ end
end
describe "with :hash type" do
View
@@ -168,7 +168,7 @@
end
it "does not set options in attributes" do
- MyScript.start(["with_optional", "--all"]).should == [nil, { "all" => true }]
+ MyScript.start(["with_optional", "--all"]).should == [nil, { "all" => true }, []]
end
it "raises an error if a required param is not provided" do
@@ -331,4 +331,32 @@ def shell
}.should be_empty
end
end
+
+ describe "edge-cases" do
+ it "can handle boolean options followed by arguments" do
+ klass = Class.new(Thor) do
+ method_option :loud, :type => :boolean
+ desc "hi NAME", "say hi to name"
+ def hi(name)
+ name.upcase! if options[:loud]
+ "Hi #{name}"
+ end
+ end
+
+ klass.start(["hi", "jose"]).should == "Hi jose"
+ klass.start(["hi", "jose", "--loud"]).should == "Hi JOSE"
+ klass.start(["hi", "--loud", "jose"]).should == "Hi JOSE"
+ end
+
+ it "passes through unknown options" do
+ klass = Class.new(Thor) do
+ desc "unknown", "passing unknown options"
+ def unknown(*args)
+ args
+ end
+ end
+
+ klass.start(["unknown", "foo", "--bar", "baz", "bat", "--bam"]).should == ["foo", "--bar", "baz", "bat", "--bam"]
+ end
+ end
end

0 comments on commit 0cfe4af

Please sign in to comment.