Skip to content

Commit

Permalink
Add a new feature excusive options and at least one option
Browse files Browse the repository at this point in the history
  • Loading branch information
nstoym authored and rafaelfranca committed May 11, 2023
1 parent f3d5d19 commit d107719
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 11 deletions.
121 changes: 120 additions & 1 deletion lib/thor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,97 @@ def method_option(name, options = {})
end
alias_method :option, :method_option

# Returns this class exclusive options array set.
#
# ==== Rturns
# Array[Array[Thor::Option.name]]
#
def method_exclusive_option_names
@method_exclusive_option_names ||=[]
end

# Adds and declareds option group for exclusive options in the
# block and arguments. You can declare options as the outside of the block.
# If :for is given as option,
# it allows you to change the options from a prvious defined command.
#
# ==== Parameters
# Array[Thor::Option.name]
# options<Hash>:: :for is applied for previous defined command.
#
# ==== Examples
#
# exclusive do
# option :one
# option :two
# end
#
# Or
#
# option :one
# option :two
# exclusive :one, :two
#
# If you give "--one" and "--two" at the same time.
# ExclusiveArgumentsError will be raised.
#
def method_exclusive(*args, &block)
register_options_relation_for(:method_options,
:method_exclusive_option_names, *args, &block)
end
alias_method :exclusive, :method_exclusive

# Returns this class at least one of required options array set.
#
# ==== Rturns
# Array[Array[Thor::Option.name]]
#
def method_at_least_one_option_names
@method_at_least_one_option_names ||=[]
end

# Adds and declareds option group for required at least one of options in the
# block of arguments. You can declare options as the outside of the block.
# If :for is given as option,
# it allows you to change the options from a prvious defined command.
#
# ==== Parameters
# Array[Thor::Option.name]
# options<Hash>:: :for is applied for previous defined command.
#
# ==== Examples
#
# at_least_one do
# option :one
# option :two
# end
#
# Or
#
# option :one
# option :two
# at_least_one :one, :two
#
# If you do not give "--one" and "--two".
# AtLeastOneRequiredArgumentError will be raised.
#
# You can use at_least_one and exclusive at the same time.
#
# exclusive do
# at_least_one do
# option :one
# option :two
# end
# end
#
# Then it is required either only one of "--one" or "--two".
#
def method_at_least_one(*args, &block)
register_options_relation_for(:method_options,
:method_at_least_one_option_names, *args, &block)
end
alias_method :at_least_one, :method_at_least_one

# Prints help information for the given command.
#
# ==== Parameters
Expand All @@ -178,6 +269,9 @@ def command_help(shell, command_name)
shell.say " #{banner(command).split("\n").join("\n ")}"
shell.say
class_options_help(shell, nil => command.options.values)
print_exclusive_options(shell, command)
print_at_least_one_required_options(shell, command)

if command.long_description
shell.say "Description:"
shell.print_wrapped(command.long_description, :indent => 2)
Expand Down Expand Up @@ -208,6 +302,8 @@ def help(shell, subcommand = false)
shell.print_table(list, :indent => 2, :truncate => true)
shell.say
class_options_help(shell)
print_exclusive_options(shell)
print_at_least_one_required_options(shell)
end

# Returns commands ready to be printed.
Expand All @@ -222,6 +318,26 @@ def printable_commands(all = true, subcommand = false)
end
alias_method :printable_tasks, :printable_commands

def print_exclusive_options(shell, command = nil)
opts = []
opts = command.method_exclusive_option_names unless command.nil?
opts += class_exclusive_option_names
unless opts.empty?
shell.say "Exclusive Options:"
shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, :indent => 2 )
shell.say
end
end
def print_at_least_one_required_options(shell, command = nil)
opts = []
opts = command.method_at_least_one_option_names unless command.nil?
opts += class_at_least_one_option_names
unless opts.empty?
shell.say "Required At Least One:"
shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, :indent => 2 )
shell.say
end
end
def subcommands
@subcommands ||= from_superclass(:subcommands, [])
end
Expand Down Expand Up @@ -419,8 +535,11 @@ def create_command(meth) #:nodoc:

if @usage && @desc
base_class = @hide ? Thor::HiddenCommand : Thor::Command
commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
relations = {:exclusive_option_names => method_exclusive_option_names,
:at_least_one_option_names => method_at_least_one_option_names}
commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options, relations)
@usage, @desc, @long_desc, @method_options, @hide = nil
@method_exclusive_option_names, @method_at_least_one_option_names = nil
true
elsif all_commands[meth] || meth == "method_missing"
true
Expand Down
123 changes: 121 additions & 2 deletions lib/thor/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,21 @@ def initialize(args = [], local_options = {}, config = {})
# 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.
stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command]
current_command = config[:current_command]
stop_on_unknown = self.class.stop_on_unknown_option? current_command

# Give a relation of options.
# After parsing, Thor::Options check whether right relations are kept
relations = {:exclusive_option_names => [], :at_least_one_option_names => []}
unless current_command.nil?
relations = current_command.options_relation
end
self.class.class_exclusive_option_names.map{ |n| relations[:exclusive_option_names] << n}
disable_required_check = self.class.disable_required_check? config[:current_command]
opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check)
self.class.class_at_least_one_option_names.map{ |n| relations[:at_least_one_option_names] << n}

opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)

self.options = opts.parse(array_options)
self.options = config[:class_options].merge(options) if config[:class_options]

Expand Down Expand Up @@ -278,6 +290,24 @@ def arguments
@arguments ||= from_superclass(:arguments, [])
end

# Returns this class exclusive options array set, looking up in the ancestors chain.
#
# ==== Rturns
# Array[Array[Thor::Option.name]]
#
def class_exclusive_option_names
@class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, [])
end

# Returns this class at least one of required options array set, looking up in the ancestors chain.
#
# ==== Rturns
# Array[Array[Thor::Option.name]]
#
def class_at_least_one_option_names
@class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, [])
end

# Adds a bunch of options to the set of class options.
#
# class_options :foo => false, :bar => :required, :baz => :string
Expand Down Expand Up @@ -313,6 +343,67 @@ def class_option(name, options = {})
build_option(name, options, class_options)
end

# Adds and declareds option group for exclusive options in the
# block and arguments. You can declare options as the outside of the block.
#
# ==== Parameters
# Array[Thor::Option.name]
#
# ==== Examples
#
# class_exclusive do
# class_option :one
# class_option :two
# end
#
# Or
#
# class_option :one
# class_option :two
# class_exclusive :one, :two
#
# If you give "--one" and "--two" at the same time.
# ExclusiveArgumentsError will be raised.
#
def class_exclusive(*args, &block)
register_options_relation_for(:class_options,
:class_exclusive_option_names, *args, &block)
end

# Adds and declareds option group for required at least one of options in the
# block and arguments. You can declare options as the outside of the block.
#
# ==== Examples
#
# class_at_least_one do
# class_option :one
# class_option :two
# end
#
# Or
#
# class_option :one
# class_option :two
# class_at_least_one :one, :two
#
# If you do not give "--one" and "--two".
# AtLeastOneRequiredArgumentError will be raised.
# You can use class_at_least_one and class_exclusive at the same time.
#
# class_exclusive do
# class_at_least_one do
# class_option :one
# class_option :two
# end
# end
#
# Then it is required either only one of "--one" or "--two".
#
def class_at_least_one(*args, &block)
register_options_relation_for(:class_options,
:class_at_least_one_option_names, *args, &block)
end

# Removes a previous defined argument. If :undefine is given, undefine
# accessors as well.
#
Expand Down Expand Up @@ -693,6 +784,34 @@ def initialize_added #:nodoc:
def dispatch(command, given_args, given_opts, config) #:nodoc:
raise NotImplementedError
end

# Register a relation of options for target(method_option/class_option)
# by args and block.
def register_options_relation_for( target, relation, *args, &block)
opt = args.pop if args.last.is_a? Hash
opt ||= {}
names = args.map{ |arg| arg.to_s }
names += built_option_names(target, opt, &block) if block_given?
command_scope_member(relation, opt) << names
end

# Get target(method_options or class_options) options
# of before and after by block evaluation.
def built_option_names(target, opt = {}, &block)
before = command_scope_member(target, opt).map{ |k,v| v.name }
instance_eval(&block)
after = command_scope_member(target, opt).map{ |k,v| v.name }
after - before
end

# Get command scope member by name.
def command_scope_member( name, options = {} ) #:nodoc:
if options[:for]
find_and_refresh_command(options[:for]).send(name)
else
send( name )
end
end
end
end
end
16 changes: 12 additions & 4 deletions lib/thor/command.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
class Thor
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :options_relation, :ancestor_name)
FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/

def initialize(name, description, long_description, usage, options = nil)
super(name.to_s, description, long_description, usage, options || {})
def initialize(name, description, long_description, usage, options = nil, options_relation = nil)
super(name.to_s, description, long_description, usage, options || {}, options_relation || {})
end

def initialize_copy(other) #:nodoc:
super(other)
self.options = other.options.dup if other.options
self.options_relation = other.options_relation.dup if other.options_relation
end

def hidden?
Expand Down Expand Up @@ -62,6 +63,14 @@ def formatted_usage(klass, namespace = true, subcommand = false)
end.join("\n")
end

def method_exclusive_option_names #:nodoc:
self.options_relation[:exclusive_option_names] || []
end

def method_at_least_one_option_names #:nodoc:
self.options_relation[:at_least_one_option_names] || []
end

protected

# Add usage with required arguments
Expand All @@ -82,7 +91,6 @@ def not_debugging?(instance)
def required_options
@required_options ||= options.map { |_, o| o.usage if o.required? }.compact.sort.join(" ")
end

# Given a target, checks if this class name is a public method.
def public_method?(instance) #:nodoc:
!(instance.public_methods & [name.to_s, name.to_sym]).empty?
Expand Down
6 changes: 6 additions & 0 deletions lib/thor/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,10 @@ class RequiredArgumentMissingError < InvocationError

class MalformattedArgumentError < InvocationError
end

class ExclusiveArgumentError < InvocationError
end

class AtLeastOneRequiredArgumentError < InvocationError
end
end
Loading

0 comments on commit d107719

Please sign in to comment.