Skip to content

Commit 291d93d

Browse files
committed
Merge PR #483
2 parents f3d5d19 + e9128ad commit 291d93d

File tree

9 files changed

+545
-10
lines changed

9 files changed

+545
-10
lines changed

lib/thor.rb

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,81 @@ def method_option(name, options = {})
163163
end
164164
alias_method :option, :method_option
165165

166+
# Adds and declares option group for exclusive options in the
167+
# block and arguments. You can declare options as the outside of the block.
168+
#
169+
# If :for is given as option, it allows you to change the options from
170+
# a previous defined command.
171+
#
172+
# ==== Parameters
173+
# Array[Thor::Option.name]
174+
# options<Hash>:: :for is applied for previous defined command.
175+
#
176+
# ==== Examples
177+
#
178+
# exclusive do
179+
# option :one
180+
# option :two
181+
# end
182+
#
183+
# Or
184+
#
185+
# option :one
186+
# option :two
187+
# exclusive :one, :two
188+
#
189+
# If you give "--one" and "--two" at the same time ExclusiveArgumentsError
190+
# will be raised.
191+
#
192+
def method_exclusive(*args, &block)
193+
register_options_relation_for(:method_options,
194+
:method_exclusive_option_names, *args, &block)
195+
end
196+
alias_method :exclusive, :method_exclusive
197+
198+
# Adds and declares option group for required at least one of options in the
199+
# block of arguments. You can declare options as the outside of the block.
200+
#
201+
# If :for is given as option, it allows you to change the options from
202+
# a previous defined command.
203+
#
204+
# ==== Parameters
205+
# Array[Thor::Option.name]
206+
# options<Hash>:: :for is applied for previous defined command.
207+
#
208+
# ==== Examples
209+
#
210+
# at_least_one do
211+
# option :one
212+
# option :two
213+
# end
214+
#
215+
# Or
216+
#
217+
# option :one
218+
# option :two
219+
# at_least_one :one, :two
220+
#
221+
# If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
222+
# will be raised.
223+
#
224+
# You can use at_least_one and exclusive at the same time.
225+
#
226+
# exclusive do
227+
# at_least_one do
228+
# option :one
229+
# option :two
230+
# end
231+
# end
232+
#
233+
# Then it is required either only one of "--one" or "--two".
234+
#
235+
def method_at_least_one(*args, &block)
236+
register_options_relation_for(:method_options,
237+
:method_at_least_one_option_names, *args, &block)
238+
end
239+
alias_method :at_least_one, :method_at_least_one
240+
166241
# Prints help information for the given command.
167242
#
168243
# ==== Parameters
@@ -178,6 +253,9 @@ def command_help(shell, command_name)
178253
shell.say " #{banner(command).split("\n").join("\n ")}"
179254
shell.say
180255
class_options_help(shell, nil => command.options.values)
256+
print_exclusive_options(shell, command)
257+
print_at_least_one_required_options(shell, command)
258+
181259
if command.long_description
182260
shell.say "Description:"
183261
shell.print_wrapped(command.long_description, :indent => 2)
@@ -208,6 +286,8 @@ def help(shell, subcommand = false)
208286
shell.print_table(list, :indent => 2, :truncate => true)
209287
shell.say
210288
class_options_help(shell)
289+
print_exclusive_options(shell)
290+
print_at_least_one_required_options(shell)
211291
end
212292

213293
# Returns commands ready to be printed.
@@ -346,6 +426,24 @@ def disable_required_check?(command) #:nodoc:
346426

347427
protected
348428

429+
# Returns this class exclusive options array set.
430+
#
431+
# ==== Returns
432+
# Array[Array[Thor::Option.name]]
433+
#
434+
def method_exclusive_option_names #:nodoc:
435+
@method_exclusive_option_names ||= []
436+
end
437+
438+
# Returns this class at least one of required options array set.
439+
#
440+
# ==== Returns
441+
# Array[Array[Thor::Option.name]]
442+
#
443+
def method_at_least_one_option_names #:nodoc:
444+
@method_at_least_one_option_names ||= []
445+
end
446+
349447
def stop_on_unknown_option #:nodoc:
350448
@stop_on_unknown_option ||= []
351449
end
@@ -355,6 +453,28 @@ def disable_required_check #:nodoc:
355453
@disable_required_check ||= [:help]
356454
end
357455

456+
def print_exclusive_options(shell, command = nil) # :nodoc:
457+
opts = []
458+
opts = command.method_exclusive_option_names unless command.nil?
459+
opts += class_exclusive_option_names
460+
unless opts.empty?
461+
shell.say "Exclusive Options:"
462+
shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, :indent => 2 )
463+
shell.say
464+
end
465+
end
466+
467+
def print_at_least_one_required_options(shell, command = nil) # :nodoc:
468+
opts = []
469+
opts = command.method_at_least_one_option_names unless command.nil?
470+
opts += class_at_least_one_option_names
471+
unless opts.empty?
472+
shell.say "Required At Least One:"
473+
shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, :indent => 2 )
474+
shell.say
475+
end
476+
end
477+
358478
# The method responsible for dispatching given the args.
359479
def dispatch(meth, given_args, given_opts, config) #:nodoc:
360480
meth ||= retrieve_command_name(given_args)
@@ -419,8 +539,11 @@ def create_command(meth) #:nodoc:
419539

420540
if @usage && @desc
421541
base_class = @hide ? Thor::HiddenCommand : Thor::Command
422-
commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
542+
relations = {:exclusive_option_names => method_exclusive_option_names,
543+
:at_least_one_option_names => method_at_least_one_option_names}
544+
commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options, relations)
423545
@usage, @desc, @long_desc, @method_options, @hide = nil
546+
@method_exclusive_option_names, @method_at_least_one_option_names = nil
424547
true
425548
elsif all_commands[meth] || meth == "method_missing"
426549
true

lib/thor/base.rb

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,23 @@ def initialize(args = [], local_options = {}, config = {})
7373
# Let Thor::Options parse the options first, so it can remove
7474
# declared options from the array. This will leave us with
7575
# a list of arguments that weren't declared.
76-
stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command]
76+
current_command = config[:current_command]
77+
stop_on_unknown = self.class.stop_on_unknown_option? current_command
78+
79+
# Give a relation of options.
80+
# After parsing, Thor::Options check whether right relations are kept
81+
relations = if current_command.nil?
82+
{:exclusive_option_names => [], :at_least_one_option_names => []}
83+
else
84+
current_command.options_relation
85+
end
86+
87+
self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n }
7788
disable_required_check = self.class.disable_required_check? config[:current_command]
78-
opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check)
89+
self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n }
90+
91+
opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)
92+
7993
self.options = opts.parse(array_options)
8094
self.options = config[:class_options].merge(options) if config[:class_options]
8195

@@ -313,6 +327,86 @@ def class_option(name, options = {})
313327
build_option(name, options, class_options)
314328
end
315329

330+
# Adds and declares option group for exclusive options in the
331+
# block and arguments. You can declare options as the outside of the block.
332+
#
333+
# ==== Parameters
334+
# Array[Thor::Option.name]
335+
#
336+
# ==== Examples
337+
#
338+
# class_exclusive do
339+
# class_option :one
340+
# class_option :two
341+
# end
342+
#
343+
# Or
344+
#
345+
# class_option :one
346+
# class_option :two
347+
# class_exclusive :one, :two
348+
#
349+
# If you give "--one" and "--two" at the same time ExclusiveArgumentsError
350+
# will be raised.
351+
#
352+
def class_exclusive(*args, &block)
353+
register_options_relation_for(:class_options,
354+
:class_exclusive_option_names, *args, &block)
355+
end
356+
357+
# Adds and declares option group for required at least one of options in the
358+
# block and arguments. You can declare options as the outside of the block.
359+
#
360+
# ==== Examples
361+
#
362+
# class_at_least_one do
363+
# class_option :one
364+
# class_option :two
365+
# end
366+
#
367+
# Or
368+
#
369+
# class_option :one
370+
# class_option :two
371+
# class_at_least_one :one, :two
372+
#
373+
# If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
374+
# will be raised.
375+
#
376+
# You can use class_at_least_one and class_exclusive at the same time.
377+
#
378+
# class_exclusive do
379+
# class_at_least_one do
380+
# class_option :one
381+
# class_option :two
382+
# end
383+
# end
384+
#
385+
# Then it is required either only one of "--one" or "--two".
386+
#
387+
def class_at_least_one(*args, &block)
388+
register_options_relation_for(:class_options,
389+
:class_at_least_one_option_names, *args, &block)
390+
end
391+
392+
# Returns this class exclusive options array set, looking up in the ancestors chain.
393+
#
394+
# ==== Returns
395+
# Array[Array[Thor::Option.name]]
396+
#
397+
def class_exclusive_option_names
398+
@class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, [])
399+
end
400+
401+
# Returns this class at least one of required options array set, looking up in the ancestors chain.
402+
#
403+
# ==== Returns
404+
# Array[Array[Thor::Option.name]]
405+
#
406+
def class_at_least_one_option_names
407+
@class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, [])
408+
end
409+
316410
# Removes a previous defined argument. If :undefine is given, undefine
317411
# accessors as well.
318412
#
@@ -693,6 +787,34 @@ def initialize_added #:nodoc:
693787
def dispatch(command, given_args, given_opts, config) #:nodoc:
694788
raise NotImplementedError
695789
end
790+
791+
# Register a relation of options for target(method_option/class_option)
792+
# by args and block.
793+
def register_options_relation_for( target, relation, *args, &block) # :nodoc:
794+
opt = args.pop if args.last.is_a? Hash
795+
opt ||= {}
796+
names = args.map{ |arg| arg.to_s }
797+
names += built_option_names(target, opt, &block) if block_given?
798+
command_scope_member(relation, opt) << names
799+
end
800+
801+
# Get target(method_options or class_options) options
802+
# of before and after by block evaluation.
803+
def built_option_names(target, opt = {}, &block) # :nodoc:
804+
before = command_scope_member(target, opt).map{ |k,v| v.name }
805+
instance_eval(&block)
806+
after = command_scope_member(target, opt).map{ |k,v| v.name }
807+
after - before
808+
end
809+
810+
# Get command scope member by name.
811+
def command_scope_member(name, options = {}) # :nodoc:
812+
if options[:for]
813+
find_and_refresh_command(options[:for]).send(name)
814+
else
815+
send(name)
816+
end
817+
end
696818
end
697819
end
698820
end

lib/thor/command.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
class Thor
2-
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
2+
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :options_relation, :ancestor_name)
33
FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
44

5-
def initialize(name, description, long_description, usage, options = nil)
6-
super(name.to_s, description, long_description, usage, options || {})
5+
def initialize(name, description, long_description, usage, options = nil, options_relation = nil)
6+
super(name.to_s, description, long_description, usage, options || {}, options_relation || {})
77
end
88

99
def initialize_copy(other) #:nodoc:
1010
super(other)
1111
self.options = other.options.dup if other.options
12+
self.options_relation = other.options_relation.dup if other.options_relation
1213
end
1314

1415
def hidden?
@@ -62,6 +63,14 @@ def formatted_usage(klass, namespace = true, subcommand = false)
6263
end.join("\n")
6364
end
6465

66+
def method_exclusive_option_names #:nodoc:
67+
self.options_relation[:exclusive_option_names] || []
68+
end
69+
70+
def method_at_least_one_option_names #:nodoc:
71+
self.options_relation[:at_least_one_option_names] || []
72+
end
73+
6574
protected
6675

6776
# Add usage with required arguments

lib/thor/error.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,10 @@ class RequiredArgumentMissingError < InvocationError
108108

109109
class MalformattedArgumentError < InvocationError
110110
end
111+
112+
class ExclusiveArgumentError < InvocationError
113+
end
114+
115+
class AtLeastOneRequiredArgumentError < InvocationError
116+
end
111117
end

0 commit comments

Comments
 (0)