Skip to content

Commit 69ffa4c

Browse files
authored
Merge pull request #674 from dturn/repeatable-options
Allow Option to be repeatable
2 parents 44a75a9 + 36fea49 commit 69ffa4c

File tree

4 files changed

+70
-8
lines changed

4 files changed

+70
-8
lines changed

lib/thor/parser/option.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
class Thor
22
class Option < Argument #:nodoc:
3-
attr_reader :aliases, :group, :lazy_default, :hide
3+
attr_reader :aliases, :group, :lazy_default, :hide, :repeatable
44

55
VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
66

77
def initialize(name, options = {})
88
@check_default_type = options[:check_default_type]
99
options[:required] = false unless options.key?(:required)
10+
@repeatable = options.fetch(:repeatable, false)
1011
super
11-
@lazy_default = options[:lazy_default]
12-
@group = options[:group].to_s.capitalize if options[:group]
13-
@aliases = Array(options[:aliases])
14-
@hide = options[:hide]
12+
@lazy_default = options[:lazy_default]
13+
@group = options[:group].to_s.capitalize if options[:group]
14+
@aliases = Array(options[:aliases])
15+
@hide = options[:hide]
1516
end
1617

1718
# This parse quick options given as method_options. It makes several
@@ -128,7 +129,8 @@ def validate_default_type!
128129
@default.class.name.downcase.to_sym
129130
end
130131

131-
raise ArgumentError, "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == @type
132+
expected_type = (@repeatable && @type != :hash) ? :array : @type
133+
raise ArgumentError, "Expected #{expected_type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == expected_type
132134
end
133135

134136
def dasherized?

lib/thor/parser/options.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ def parse(args) # rubocop:disable MethodLength
9797

9898
switch = normalize_switch(switch)
9999
option = switch_option(switch)
100-
@assigns[option.human_name] = parse_peek(switch, option)
100+
result = parse_peek(switch, option)
101+
assign_result!(option, result)
101102
elsif @stop_on_unknown
102103
@parsing_options = false
103104
@extra << shifted
@@ -132,6 +133,15 @@ def check_unknown!
132133

133134
protected
134135

136+
def assign_result!(option, result)
137+
if option.repeatable && option.type == :hash
138+
(@assigns[option.human_name] ||= {}).merge!(result)
139+
elsif option.repeatable
140+
(@assigns[option.human_name] ||= []) << result
141+
else
142+
@assigns[option.human_name] = result
143+
end
144+
end
135145
# Check if the current value in peek is a registered switch.
136146
#
137147
# Two booleans are returned. The first is true if the current value

spec/parser/option_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,30 @@ def option(name, options = {})
140140
end.to raise_error(ArgumentError, 'Expected numeric default value for \'--foo-bar\'; got "baz" (string)')
141141
end
142142

143+
it "raises an error if repeatable and default is inconsistent with type and check_default_type is true" do
144+
expect do
145+
option("foo_bar", :type => :numeric, :repeatable => true, :default => "baz", :check_default_type => true)
146+
end.to raise_error(ArgumentError, 'Expected array default value for \'--foo-bar\'; got "baz" (string)')
147+
end
148+
149+
it "raises an error type hash is repeatable and default is inconsistent with type and check_default_type is true" do
150+
expect do
151+
option("foo_bar", :type => :hash, :repeatable => true, :default => "baz", :check_default_type => true)
152+
end.to raise_error(ArgumentError, 'Expected hash default value for \'--foo-bar\'; got "baz" (string)')
153+
end
154+
155+
it "does not raises an error if type hash is repeatable and default is consistent with type and check_default_type is true" do
156+
expect do
157+
option("foo_bar", :type => :hash, :repeatable => true, :default => {}, :check_default_type => true)
158+
end.not_to raise_error
159+
end
160+
161+
it "does not raises an error if repeatable and default is consistent with type and check_default_type is true" do
162+
expect do
163+
option("foo_bar", :type => :numeric, :repeatable => true, :default => [1], :check_default_type => true)
164+
end.not_to raise_error
165+
end
166+
143167
it "does not raises an error if default is an symbol and type string and check_default_type is true" do
144168
expect do
145169
option("foo", :type => :string, :default => :bar, :check_default_type => true)

spec/parser/options_spec.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ def create(opts, defaults = {}, stop_on_unknown = false)
66
opts.each do |key, value|
77
opts[key] = Thor::Option.parse(key, value) unless value.is_a?(Thor::Option)
88
end
9-
109
@opt = Thor::Options.new(opts, defaults, stop_on_unknown)
1110
end
1211

@@ -302,6 +301,13 @@ def remaining
302301
expect { parse("--fruit", "orange") }.to raise_error(Thor::MalformattedArgumentError,
303302
"Expected '--fruit' to be one of #{enum.join(', ')}; got orange")
304303
end
304+
305+
it "allows multiple values if repeatable is specified" do
306+
create :foo => Thor::Option.new("foo", :type => :string, :repeatable => true)
307+
308+
expect(parse("--foo=bar", "--foo", "12")["foo"]).to eq(["bar", "12"])
309+
expect(parse("--foo", "13", "--foo", "14")["foo"]).to eq(["bar", "12", "13", "14"])
310+
end
305311
end
306312

307313
describe "with :boolean type" do
@@ -362,6 +368,11 @@ def remaining
362368
expect(parse("--skip-foo", "bar")).to eq("foo" => false)
363369
expect(@opt.remaining).to eq(%w(bar))
364370
end
371+
372+
it "allows multiple values if repeatable is specified" do
373+
create :verbose => Thor::Option.new("verbose", :type => :boolean, :aliases => '-v', :repeatable => true)
374+
expect(parse("-v", "-v", "-v")["verbose"].count).to eq(3)
375+
end
365376
end
366377

367378
describe "with :hash type" do
@@ -384,6 +395,11 @@ def remaining
384395
it "must not allow the same hash key to be specified multiple times" do
385396
expect { parse("--attributes", "name:string", "name:integer") }.to raise_error(Thor::MalformattedArgumentError, "You can't specify 'name' more than once in option '--attributes'; got name:string and name:integer")
386397
end
398+
399+
it "allows multiple values if repeatable is specified" do
400+
create :attributes => Thor::Option.new("attributes", :type => :hash, :repeatable => true)
401+
expect(parse("--attributes", "name:one", "foo:1", "--attributes", "name:two", "bar:2")["attributes"]).to eq({"name"=>"two", "foo"=>"1", "bar" => "2"})
402+
end
387403
end
388404

389405
describe "with :array type" do
@@ -402,6 +418,11 @@ def remaining
402418
it "must not mix values with other switches" do
403419
expect(parse("--attributes", "a", "b", "c", "--baz", "cool")["attributes"]).to eq(%w(a b c))
404420
end
421+
422+
it "allows multiple values if repeatable is specified" do
423+
create :attributes => Thor::Option.new("attributes", :type => :array, :repeatable => true)
424+
expect(parse("--attributes", "1", "2", "--attributes", "3", "4")["attributes"]).to eq([["1", "2"], ["3", "4"]])
425+
end
405426
end
406427

407428
describe "with :numeric type" do
@@ -428,6 +449,11 @@ def remaining
428449
expect { parse("--limit", "3") }.to raise_error(Thor::MalformattedArgumentError,
429450
"Expected '--limit' to be one of #{enum.join(', ')}; got 3")
430451
end
452+
453+
it "allows multiple values if repeatable is specified" do
454+
create :run => Thor::Option.new("run", :type => :numeric, :repeatable => true)
455+
expect(parse("--run", "1", "--run", "2")["run"]).to eq([1, 2])
456+
end
431457
end
432458
end
433459
end

0 commit comments

Comments
 (0)