require_relative "thor/base"
class Bundler::Thor
$thor_runner ||= false
class << self
# Allows for custom "Command" package naming.
#
# === Parameters
# name<String>
# options<Hash>
#
def package_name(name, _ = {})
@package_name = name.nil? || name == "" ? nil : name
end
# Sets the default command when thor is executed without an explicit command to be called.
#
# ==== Parameters
# meth<Symbol>:: name of the default command
#
def default_command(meth = nil)
if meth
@default_command = meth == :none ? "help" : meth.to_s
else
@default_command ||= from_superclass(:default_command, "help")
end
end
alias_method :default_task, :default_command
# Registers another Bundler::Thor subclass as a command.
#
# ==== Parameters
# klass<Class>:: Bundler::Thor subclass to register
# command<String>:: Subcommand name to use
# usage<String>:: Short usage for the subcommand
# description<String>:: Description for the subcommand
def register(klass, subcommand_name, usage, description, options = {})
if klass <= Bundler::Thor::Group
desc usage, description, options
define_method(subcommand_name) { |*args| invoke(klass, args) }
else
desc usage, description, options
subcommand subcommand_name, klass
end
end
# Defines the usage and the description of the next command.
#
# ==== Parameters
# usage<String>
# description<String>
# options<String>
#
def desc(usage, description, options = {})
if options[:for]
command = find_and_refresh_command(options[:for])
command.usage = usage if usage
command.description = description if description
else
@usage = usage
@desc = description
@hide = options[:hide] || false
end
end
# Defines the long description of the next command.
#
# Long description is by default indented, line-wrapped and repeated whitespace merged.
# In order to print long description verbatim, with indentation and spacing exactly
# as found in the code, use the +wrap+ option
#
# long_desc 'your very long description', wrap: false
#
# ==== Parameters
# long description<String>
# options<Hash>
#
def long_desc(long_description, options = {})
if options[:for]
command = find_and_refresh_command(options[:for])
command.long_description = long_description if long_description
else
@long_desc = long_description
@long_desc_wrap = options[:wrap] != false
end
end
# Maps an input to a command. If you define:
#
# map "-T" => "list"
#
# Running:
#
# thor -T
#
# Will invoke the list command.
#
# ==== Parameters
# Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given command.
#
def map(mappings = nil, **kw)
@map ||= from_superclass(:map, {})
if mappings && !kw.empty?
mappings = kw.merge!(mappings)
else
mappings ||= kw
end
if mappings
mappings.each do |key, value|
if key.respond_to?(:each)
key.each { |subkey| @map[subkey] = value }
else
@map[key] = value
end
end
end
@map
end
# Declares the options for the next command to be declared.
#
# ==== Parameters
# Hash[Symbol => Object]:: The hash key is the name of the option and the value
# is the type of the option. Can be :string, :array, :hash, :boolean, :numeric
# or :required (string). If you give a value, the type of the value is used.
#
def method_options(options = nil)
@method_options ||= {}
build_options(options, @method_options) if options
@method_options
end
alias_method :options, :method_options
# Adds an option to the set of method options. If :for is given as option,
# it allows you to change the options from a previous defined command.
#
# def previous_command
# # magic
# end
#
# method_option :foo, :for => :previous_command
#
# def next_command
# # magic
# end
#
# ==== Parameters
# name<Symbol>:: The name of the argument.
# options<Hash>:: Described below.
#
# ==== Options
# :desc - Description for the argument.
# :required - If the argument is required or not.
# :default - Default value for this argument. It cannot be required and have default values.
# :aliases - Aliases for this option.
# :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
# :banner - String to show on usage notes.
# :hide - If you want to hide this option from the help.
#
def method_option(name, options = {})
unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
end
scope = if options[:for]
find_and_refresh_command(options[:for]).options
else
method_options
end
build_option(name, options, scope)
end
alias_method :option, :method_option
# Adds and declares 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 previous defined command.
#
# ==== Parameters
# Array[Bundler::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
# Adds and declares 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 previous defined command.
#
# ==== Parameters
# Array[Bundler::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
# shell<Bundler::Thor::Shell>
# command_name<String>
#
def command_help(shell, command_name)
meth = normalize_command_name(command_name)
command = all_commands[meth]
handle_no_command_error(meth) unless command
shell.say "Usage:"
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:"
if command.wrap_long_description
shell.print_wrapped(command.long_description, indent: 2)
else
shell.say command.long_description
end
else
shell.say command.description
end
end
alias_method :task_help, :command_help
# Prints help information for this class.
#
# ==== Parameters
# shell<Bundler::Thor::Shell>
#
def help(shell, subcommand = false)
list = printable_commands(true, subcommand)
Bundler::Thor::Util.thor_classes_in(self).each do |klass|
list += klass.printable_commands(false)
end
sort_commands!(list)
if defined?(@package_name) && @package_name
shell.say "#{@package_name} commands:"
else
shell.say "Commands:"
end
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.
def printable_commands(all = true, subcommand = false)
(all ? all_commands : commands).map do |_, command|
next if command.hidden?
item = []
item << banner(command, false, subcommand)
item << (command.description ? "# #{command.description.gsub(/\s+/m, ' ')}" : "")
item
end.compact
end
alias_method :printable_tasks, :printable_commands
def subcommands
@subcommands ||= from_superclass(:subcommands, [])
end
alias_method :subtasks, :subcommands
def subcommand_classes
@subcommand_classes ||= {}
end
def subcommand(subcommand, subcommand_class)
subcommands << subcommand.to_s
subcommand_class.subcommand_help subcommand
subcommand_classes[subcommand.to_s] = subcommand_class
define_method(subcommand) do |*args|
args, opts = Bundler::Thor::Arguments.split(args)
invoke_args = [args, opts, {invoked_via_subcommand: true, class_options: options}]
invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h")
invoke subcommand_class, *invoke_args
end
subcommand_class.commands.each do |_meth, command|
command.ancestor_name = subcommand
end
end
alias_method :subtask, :subcommand
# Extend check unknown options to accept a hash of conditions.
#
# === Parameters
# options<Hash>: A hash containing :only and/or :except keys
def check_unknown_options!(options = {})
@check_unknown_options ||= {}
options.each do |key, value|
if value
@check_unknown_options[key] = Array(value)
else
@check_unknown_options.delete(key)
end
end
@check_unknown_options
end
# Overwrite check_unknown_options? to take subcommands and options into account.
def check_unknown_options?(config) #:nodoc:
options = check_unknown_options
return false unless options
command = config[:current_command]
return true unless command
name = command.name
if subcommands.include?(name)
false
elsif options[:except]
!options[:except].include?(name.to_sym)
elsif options[:only]
options[:only].include?(name.to_sym)
else
true
end
end
# Stop parsing of options as soon as an unknown option or a regular
# argument is encountered. All remaining arguments are passed to the command.
# This is useful if you have a command that can receive arbitrary additional
# options, and where those additional options should not be handled by
# Bundler::Thor.
#
# ==== Example
#
# To better understand how this is useful, let's consider a command that calls
# an external command. A user may want to pass arbitrary options and
# arguments to that command. The command itself also accepts some options,
# which should be handled by Bundler::Thor.
#
# class_option "verbose", :type => :boolean
# stop_on_unknown_option! :exec
# check_unknown_options! :except => :exec
#
# desc "exec", "Run a shell command"
# def exec(*args)
# puts "diagnostic output" if options[:verbose]
# Kernel.exec(*args)
# end
#
# Here +exec+ can be called with +--verbose+ to get diagnostic output,
# e.g.:
#
# $ thor exec --verbose echo foo
# diagnostic output
# foo
#
# But if +--verbose+ is given after +echo+, it is passed to +echo+ instead:
#
# $ thor exec echo --verbose foo
# --verbose foo
#
# ==== Parameters
# Symbol ...:: A list of commands that should be affected.
def stop_on_unknown_option!(*command_names)
@stop_on_unknown_option = stop_on_unknown_option | command_names
end
def stop_on_unknown_option?(command) #:nodoc:
command && stop_on_unknown_option.include?(command.name.to_sym)
end
# Disable the check for required options for the given commands.
# This is useful if you have a command that does not need the required options
# to work, like help.
#
# ==== Parameters
# Symbol ...:: A list of commands that should be affected.
def disable_required_check!(*command_names)
@disable_required_check = disable_required_check | command_names
end
def disable_required_check?(command) #:nodoc:
command && disable_required_check.include?(command.name.to_sym)
end
protected
# Returns this class exclusive options array set.
#
# ==== Returns
# Array[Array[Bundler::Thor::Option.name]]
#
def method_exclusive_option_names #:nodoc:
@method_exclusive_option_names ||= []
end
# Returns this class at least one of required options array set.
#
# ==== Returns
# Array[Array[Bundler::Thor::Option.name]]
#
def method_at_least_one_option_names #:nodoc:
@method_at_least_one_option_names ||= []
end
def stop_on_unknown_option #:nodoc:
@stop_on_unknown_option ||= []
end
# help command has the required check disabled by default.
def disable_required_check #:nodoc:
@disable_required_check ||= [:help]
end
def print_exclusive_options(shell, command = nil) # :nodoc:
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) # :nodoc:
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
# The method responsible for dispatching given the args.
def dispatch(meth, given_args, given_opts, config) #:nodoc:
meth ||= retrieve_command_name(given_args)
command = all_commands[normalize_command_name(meth)]
if !command && config[:invoked_via_subcommand]
# We're a subcommand and our first argument didn't match any of our
# commands. So we put it back and call our default command.
given_args.unshift(meth)
command = all_commands[normalize_command_name(default_command)]
end
if command
args, opts = Bundler::Thor::Options.split(given_args)
if stop_on_unknown_option?(command) && !args.empty?
# given_args starts with a non-option, so we treat everything as
# ordinary arguments
args.concat opts
opts.clear
end
else
args = given_args
opts = nil
command = dynamic_command_class.new(meth)
end
opts = given_opts || opts || []
config[:current_command] = command
config[:command_options] = command.options
instance = new(args, opts, config)
yield instance if block_given?
args = instance.args
trailing = args[Range.new(arguments.size, -1)]
instance.invoke_command(command, trailing || [])
end
# The banner for this class. You can customize it if you are invoking the
# thor class by another ways which is not the Bundler::Thor::Runner. It receives
# the command that is going to be invoked and a boolean which indicates if
# the namespace should be displayed as arguments.
#
def banner(command, namespace = nil, subcommand = false)
command.formatted_usage(self, $thor_runner, subcommand).split("\n").map do |formatted_usage|
"#{basename} #{formatted_usage}"
end.join("\n")
end
def baseclass #:nodoc:
Bundler::Thor
end
def dynamic_command_class #:nodoc:
Bundler::Thor::DynamicCommand
end
def create_command(meth) #:nodoc:
@usage ||= nil
@desc ||= nil
@long_desc ||= nil
@long_desc_wrap ||= nil
@hide ||= nil
if @usage && @desc
base_class = @hide ? Bundler::Thor::HiddenCommand : Bundler::Thor::Command
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, @long_desc_wrap, @usage, method_options, relations)
@usage, @desc, @long_desc, @long_desc_wrap, @method_options, @hide = nil
@method_exclusive_option_names, @method_at_least_one_option_names = nil
true
elsif all_commands[meth] || meth == "method_missing"
true
else
puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \
"Call desc if you want this method to be available as command or declare it inside a " \
"no_commands{} block. Invoked from #{caller[1].inspect}."
false
end
end
alias_method :create_task, :create_command
def initialize_added #:nodoc:
class_options.merge!(method_options)
@method_options = nil
end
# Retrieve the command name from given args.
def retrieve_command_name(args) #:nodoc:
meth = args.first.to_s unless args.empty?
args.shift if meth && (map[meth] || meth !~ /^\-/)
end
alias_method :retrieve_task_name, :retrieve_command_name
# receives a (possibly nil) command name and returns a name that is in
# the commands hash. In addition to normalizing aliases, this logic
# will determine if a shortened command is an unambiguous substring of
# a command or alias.
#
# +normalize_command_name+ also converts names like +animal-prison+
# into +animal_prison+.
def normalize_command_name(meth) #:nodoc:
return default_command.to_s.tr("-", "_") unless meth
possibilities = find_command_possibilities(meth)
raise AmbiguousTaskError, "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]" if possibilities.size > 1
if possibilities.empty?
meth ||= default_command
elsif map[meth]
meth = map[meth]
else
meth = possibilities.first
end
meth.to_s.tr("-", "_") # treat foo-bar as foo_bar
end
alias_method :normalize_task_name, :normalize_command_name
# this is the logic that takes the command name passed in by the user
# and determines whether it is an unambiguous substrings of a command or
# alias name.
def find_command_possibilities(meth)
len = meth.to_s.length
possibilities = all_commands.merge(map).keys.select { |n| meth == n[0, len] }.sort
unique_possibilities = possibilities.map { |k| map[k] || k }.uniq
if possibilities.include?(meth)
[meth]
elsif unique_possibilities.size == 1
unique_possibilities
else
possibilities
end
end
alias_method :find_task_possibilities, :find_command_possibilities
def subcommand_help(cmd)
desc "help [COMMAND]", "Describe subcommands or one specific subcommand"
class_eval "
def help(command = nil, subcommand = true); super; end
"
end
alias_method :subtask_help, :subcommand_help
# Sort the commands, lexicographically by default.
#
# Can be overridden in the subclass to change the display order of the
# commands.
def sort_commands!(list)
list.sort! { |a, b| a[0] <=> b[0] }
end
end
include Bundler::Thor::Base
map HELP_MAPPINGS => :help
desc "help [COMMAND]", "Describe available commands or one specific command"
def help(command = nil, subcommand = false)
if command
if self.class.subcommands.include? command
self.class.subcommand_classes[command].help(shell, true)
else
self.class.command_help(shell, command)
end
else
self.class.help(shell, subcommand)
end
end
end