[rubygems/rubygems] Update vendored thor to v1.3.0

See https://github.com/rails/thor/releases/tag/v1.3.0

https://github.com/rubygems/rubygems/commit/3c7165474b
This commit is contained in:
Samuel Giddins 2023-10-19 09:22:34 -07:00 committed by git
parent 3e65115cef
commit af222d4db2
30 changed files with 822 additions and 358 deletions

View File

@ -147,7 +147,7 @@ module Bundler
spaces ? text.gsub(/#{spaces}/, "") : text
end
def word_wrap(text, line_width = @shell.terminal_width)
def word_wrap(text, line_width = Thor::Terminal.terminal_width)
strip_leading_spaces(text).split("\n").collect do |line|
line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
end * "\n"

View File

@ -65,8 +65,15 @@ class Bundler::Thor
# 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]
@ -74,6 +81,7 @@ class Bundler::Thor
command.long_description = long_description if long_description
else
@long_desc = long_description
@long_desc_wrap = options[:wrap] != false
end
end
@ -133,7 +141,7 @@ class Bundler::Thor
# # magic
# end
#
# method_option :foo => :bar, :for => :previous_command
# method_option :foo, :for => :previous_command
#
# def next_command
# # magic
@ -153,6 +161,9 @@ class Bundler::Thor
# :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
@ -163,6 +174,81 @@ class Bundler::Thor
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
@ -178,9 +264,16 @@ class Bundler::Thor
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)
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
@ -197,7 +290,7 @@ class Bundler::Thor
Bundler::Thor::Util.thor_classes_in(self).each do |klass|
list += klass.printable_commands(false)
end
list.sort! { |a, b| a[0] <=> b[0] }
sort_commands!(list)
if defined?(@package_name) && @package_name
shell.say "#{@package_name} commands:"
@ -205,9 +298,11 @@ class Bundler::Thor
shell.say "Commands:"
end
shell.print_table(list, :indent => 2, :truncate => true)
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.
@ -238,7 +333,7 @@ class Bundler::Thor
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 = [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
@ -346,6 +441,24 @@ class Bundler::Thor
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
@ -355,6 +468,28 @@ class Bundler::Thor
@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)
@ -415,12 +550,16 @@ class Bundler::Thor
@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
commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
@usage, @desc, @long_desc, @method_options, @hide = nil
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
@ -495,6 +634,14 @@ class Bundler::Thor
"
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

View File

@ -46,17 +46,17 @@ class Bundler::Thor
# Add runtime options that help actions execution.
#
def add_runtime_options!
class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime,
:desc => "Overwrite files that already exist"
class_option :force, type: :boolean, aliases: "-f", group: :runtime,
desc: "Overwrite files that already exist"
class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime,
:desc => "Run but do not make any changes"
class_option :pretend, type: :boolean, aliases: "-p", group: :runtime,
desc: "Run but do not make any changes"
class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime,
:desc => "Suppress status output"
class_option :quiet, type: :boolean, aliases: "-q", group: :runtime,
desc: "Suppress status output"
class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime,
:desc => "Skip files that already exist"
class_option :skip, type: :boolean, aliases: "-s", group: :runtime,
desc: "Skip files that already exist"
end
end
@ -113,9 +113,9 @@ class Bundler::Thor
#
def relative_to_original_destination_root(path, remove_dot = true)
root = @destination_stack[0]
if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ''].include?(path[root.size..root.size])
if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ""].include?(path[root.size..root.size])
path = path.dup
path[0...root.size] = '.'
path[0...root.size] = "."
remove_dot ? (path[2..-1] || "") : path
else
path
@ -223,8 +223,7 @@ class Bundler::Thor
contents = if is_uri
require "open-uri"
# for ruby 2.1-2.4
URI.send(:open, path, "Accept" => "application/x-thor-template", &:read)
URI.open(path, "Accept" => "application/x-thor-template", &:read)
else
File.open(path, &:read)
end
@ -285,7 +284,7 @@ class Bundler::Thor
#
def run_ruby_script(command, config = {})
return unless behavior == :invoke
run command, config.merge(:with => Bundler::Thor::Util.ruby_command)
run command, config.merge(with: Bundler::Thor::Util.ruby_command)
end
# Run a thor command. A hash of options can be given and it's converted to
@ -316,7 +315,7 @@ class Bundler::Thor
args.push Bundler::Thor::Options.to_switches(config)
command = args.join(" ").strip
run command, :with => :thor, :verbose => verbose, :pretend => pretend, :capture => capture
run command, with: :thor, verbose: verbose, pretend: pretend, capture: capture
end
protected
@ -324,7 +323,7 @@ class Bundler::Thor
# Allow current root to be shared between invocations.
#
def _shared_configuration #:nodoc:
super.merge!(:destination_root => destination_root)
super.merge!(destination_root: destination_root)
end
def _cleanup_options_and_set(options, key) #:nodoc:

View File

@ -43,7 +43,8 @@ class Bundler::Thor
# Boolean:: true if it is identical, false otherwise.
#
def identical?
exists? && File.binread(destination) == render
# binread uses ASCII-8BIT, so to avoid false negatives, the string must use the same
exists? && File.binread(destination) == String.new(render).force_encoding("ASCII-8BIT")
end
# Holds the content to be added to the file.

View File

@ -58,7 +58,7 @@ class Bundler::Thor
def initialize(base, source, destination = nil, config = {}, &block)
@source = File.expand_path(Dir[Util.escape_globs(base.find_in_source_paths(source.to_s))].first)
@block = block
super(base, destination, {:recursive => true}.merge(config))
super(base, destination, {recursive: true}.merge(config))
end
def invoke!

View File

@ -33,7 +33,7 @@ class Bundler::Thor
#
def initialize(base, destination, config = {})
@base = base
@config = {:verbose => true}.merge(config)
@config = {verbose: true}.merge(config)
self.destination = destination
end

View File

@ -66,12 +66,15 @@ class Bundler::Thor
# ==== Parameters
# source<String>:: the address of the given content.
# destination<String>:: the relative path to the destination root.
# config<Hash>:: give :verbose => false to not log the status.
# config<Hash>:: give :verbose => false to not log the status, and
# :http_headers => <Hash> to add headers to an http request.
#
# ==== Examples
#
# get "http://gist.github.com/103208", "doc/README"
#
# get "http://gist.github.com/103208", "doc/README", :http_headers => {"Content-Type" => "application/json"}
#
# get "http://gist.github.com/103208" do |content|
# content.split("\n").first
# end
@ -82,7 +85,7 @@ class Bundler::Thor
render = if source =~ %r{^https?\://}
require "open-uri"
URI.send(:open, source) { |input| input.binmode.read }
URI.send(:open, source, config.fetch(:http_headers, {})) { |input| input.binmode.read }
else
source = File.expand_path(find_in_source_paths(source.to_s))
File.open(source) { |input| input.binmode.read }
@ -120,12 +123,7 @@ class Bundler::Thor
context = config.delete(:context) || instance_eval("binding")
create_file destination, nil, config do
match = ERB.version.match(/(\d+\.\d+\.\d+)/)
capturable_erb = if match && match[1] >= "2.2.0" # Ruby 2.6+
CapturableERB.new(::File.binread(source), :trim_mode => "-", :eoutvar => "@output_buffer")
else
CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer")
end
capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer")
content = capturable_erb.tap do |erb|
erb.filename = source
end.result(context)

View File

@ -21,7 +21,7 @@ class Bundler::Thor
# gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
# end
#
WARNINGS = { unchanged_no_flag: 'File unchanged! Either the supplied flag value not found or the content has already been inserted!' }
WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}
def insert_into_file(destination, *args, &block)
data = block_given? ? block : args.shift
@ -37,7 +37,7 @@ class Bundler::Thor
attr_reader :replacement, :flag, :behavior
def initialize(base, destination, data, config)
super(base, destination, {:verbose => true}.merge(config))
super(base, destination, {verbose: true}.merge(config))
@behavior, @flag = if @config.key?(:after)
[:after, @config.delete(:after)]
@ -59,6 +59,8 @@ class Bundler::Thor
if exists?
if replace!(/#{flag}/, content, config[:force])
say_status(:invoke)
elsif replacement_present?
say_status(:unchanged, color: :blue)
else
say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
end
@ -96,6 +98,8 @@ class Bundler::Thor
end
elsif warning
warning
elsif behavior == :unchanged
:unchanged
else
:subtract
end
@ -103,11 +107,18 @@ class Bundler::Thor
super(status, (color || config[:verbose]))
end
def content
@content ||= File.read(destination)
end
def replacement_present?
content.include?(replacement)
end
# Adds the content to the file.
#
def replace!(regexp, string, force)
content = File.read(destination)
if force || !content.include?(replacement)
if force || !replacement_present?
success = content.gsub!(regexp, string)
File.open(destination, "wb") { |file| file.write(content) } unless pretend?

View File

@ -24,9 +24,9 @@ class Bundler::Thor
class << self
def deprecation_warning(message) #:nodoc:
unless ENV['THOR_SILENCE_DEPRECATION']
unless ENV["THOR_SILENCE_DEPRECATION"]
warn "Deprecation warning: #{message}\n" +
'You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.'
"You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION."
end
end
end
@ -60,6 +60,7 @@ class Bundler::Thor
command_options = config.delete(:command_options) # hook for start
parse_options = parse_options.merge(command_options) if command_options
if local_options.is_a?(Array)
array_options = local_options
hash_options = {}
@ -73,9 +74,24 @@ class Bundler::Thor
# Let Bundler::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]
disable_required_check = self.class.disable_required_check? config[:current_command]
opts = Bundler::Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check)
current_command = config[:current_command]
stop_on_unknown = self.class.stop_on_unknown_option? current_command
# Give a relation of options.
# After parsing, Bundler::Thor::Options check whether right relations are kept
relations = if current_command.nil?
{exclusive_option_names: [], at_least_one_option_names: []}
else
current_command.options_relation
end
self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n }
self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n }
disable_required_check = self.class.disable_required_check? current_command
opts = Bundler::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]
@ -310,9 +326,92 @@ class Bundler::Thor
# :hide:: -- If you want to hide this option from the help.
#
def class_option(name, options = {})
unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
end
build_option(name, options, class_options)
end
# Adds and declares option group for exclusive options in the
# block and arguments. You can declare options as the outside of the block.
#
# ==== Parameters
# Array[Bundler::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 declares 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
# Returns this class exclusive options array set, looking up in the ancestors chain.
#
# ==== Returns
# Array[Array[Bundler::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.
#
# ==== Returns
# Array[Array[Bundler::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
# Removes a previous defined argument. If :undefine is given, undefine
# accessors as well.
#
@ -565,12 +664,12 @@ class Bundler::Thor
item.push(option.description ? "# #{option.description}" : "")
list << item
list << ["", "# Default: #{option.default}"] if option.show_default?
list << ["", "# Possible values: #{option.enum.join(', ')}"] if option.enum
list << ["", "# Default: #{option.print_default}"] if option.show_default?
list << ["", "# Possible values: #{option.enum_to_s}"] if option.enum
end
shell.say(group_name ? "#{group_name} options:" : "Options:")
shell.print_table(list, :indent => 2)
shell.print_table(list, indent: 2)
shell.say ""
end
@ -587,7 +686,7 @@ class Bundler::Thor
# options<Hash>:: Described in both class_option and method_option.
# scope<Hash>:: Options hash that is being built up
def build_option(name, options, scope) #:nodoc:
scope[name] = Bundler::Thor::Option.new(name, {:check_default_type => check_default_type}.merge!(options))
scope[name] = Bundler::Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options))
end
# Receives a hash of options, parse them and add to the scope. This is a
@ -693,6 +792,34 @@ class Bundler::Thor
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) # :nodoc:
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) # :nodoc:
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

View File

@ -1,14 +1,15 @@
class Bundler::Thor
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
class Command < Struct.new(:name, :description, :long_description, :wrap_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, wrap_long_description, usage, options = nil, options_relation = nil)
super(name.to_s, description, long_description, wrap_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?
@ -62,6 +63,14 @@ class Bundler::Thor
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
@ -127,7 +136,7 @@ class Bundler::Thor
# A dynamic command that handles method missing scenarios.
class DynamicCommand < Command
def initialize(name, options = nil)
super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options)
super(name.to_s, "A dynamically-generated command", name.to_s, nil, name.to_s, options)
end
def run(instance, args = [])

View File

@ -38,6 +38,10 @@ class Bundler::Thor
super(convert_key(key), *args)
end
def slice(*keys)
super(*keys.map{ |key| convert_key(key) })
end
def key?(key)
super(convert_key(key))
end

View File

@ -1,26 +1,15 @@
class Bundler::Thor
Correctable = if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable) # rubocop:disable Naming/ConstantName
# In order to support versions of Ruby that don't have keyword
# arguments, we need our own spell checker class that doesn't take key
# words. Even though this code wouldn't be hit because of the check
# above, it's still necessary because the interpreter would otherwise be
# unable to parse the file.
class NoKwargSpellChecker < DidYouMean::SpellChecker # :nodoc:
def initialize(dictionary)
@dictionary = dictionary
end
end
Module.new do
def to_s
super + DidYouMean.formatter.message_for(corrections)
end
Module.new do
def to_s
super + DidYouMean.formatter.message_for(corrections)
end
def corrections
@corrections ||= self.class.const_get(:SpellChecker).new(self).corrections
end
end
end
def corrections
@corrections ||= self.class.const_get(:SpellChecker).new(self).corrections
end
end
end
# Bundler::Thor::Error is raised when it's caused by wrong usage of thor classes. Those
# errors have their backtrace suppressed and are nicely shown to the user.
@ -45,7 +34,7 @@ class Bundler::Thor
end
def spell_checker
NoKwargSpellChecker.new(error.all_commands)
DidYouMean::SpellChecker.new(dictionary: error.all_commands)
end
end
@ -87,7 +76,7 @@ class Bundler::Thor
end
def spell_checker
@spell_checker ||= NoKwargSpellChecker.new(error.switches)
@spell_checker ||= DidYouMean::SpellChecker.new(dictionary: error.switches)
end
end
@ -108,4 +97,10 @@ class Bundler::Thor
class MalformattedArgumentError < InvocationError
end
class ExclusiveArgumentError < InvocationError
end
class AtLeastOneRequiredArgumentError < InvocationError
end
end

View File

@ -143,7 +143,7 @@ class Bundler::Thor
# Configuration values that are shared between invocations.
def _shared_configuration #:nodoc:
{:invocations => @_invocations}
{invocations: @_invocations}
end
# This method simply retrieves the class and command to be invoked.

View File

@ -13,10 +13,10 @@ class Bundler::Thor
end
def entered?
@depth > 0
@depth.positive?
end
private
private
def push
@depth += 1

View File

@ -24,6 +24,17 @@ class Bundler::Thor
validate! # Trigger specific validations
end
def print_default
if @type == :array and @default.is_a?(Array)
@default.map { |x|
p = x.gsub('"','\\"')
"\"#{p}\""
}.join(" ")
else
@default
end
end
def usage
required? ? banner : "[#{banner}]"
end
@ -41,11 +52,19 @@ class Bundler::Thor
end
end
def enum_to_s
if enum.respond_to? :join
enum.join(", ")
else
"#{enum.first}..#{enum.last}"
end
end
protected
def validate!
raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil?
raise ArgumentError, "An argument cannot have an enum other than an array." if @enum && !@enum.is_a?(Array)
raise ArgumentError, "An argument cannot have an enum other than an enumerable." if @enum && !@enum.is_a?(Enumerable)
end
def valid_type?(type)

View File

@ -30,11 +30,7 @@ class Bundler::Thor
arguments.each do |argument|
if !argument.default.nil?
begin
@assigns[argument.human_name] = argument.default.dup
rescue TypeError # Compatibility shim for un-dup-able Fixnum in Ruby < 2.4
@assigns[argument.human_name] = argument.default
end
@assigns[argument.human_name] = argument.default.dup
elsif argument.required?
@non_assigned_required << argument
end
@ -121,8 +117,18 @@ class Bundler::Thor
#
def parse_array(name)
return shift if peek.is_a?(Array)
array = []
array << shift while current_is_value?
while current_is_value?
value = shift
if !value.empty?
validate_enum_value!(name, value, "Expected all values of '%s' to be one of %s; got %s")
end
array << value
end
array
end
@ -138,11 +144,9 @@ class Bundler::Thor
end
value = $&.index(".") ? shift.to_f : shift.to_i
if @switches.is_a?(Hash) && switch = @switches[name]
if switch.enum && !switch.enum.include?(value)
raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
end
end
validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
value
end
@ -156,15 +160,27 @@ class Bundler::Thor
nil
else
value = shift
if @switches.is_a?(Hash) && switch = @switches[name]
if switch.enum && !switch.enum.include?(value)
raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
end
end
validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s")
value
end
end
# Raises an error if the switch is an enum and the values aren't included on it.
#
def validate_enum_value!(name, value, message)
return unless @switches.is_a?(Hash)
switch = @switches[name]
return unless switch
if switch.enum && !switch.enum.include?(value)
raise MalformattedArgumentError, message % [name, switch.enum_to_s, value]
end
end
# Raises an error if @non_assigned_required array is not empty.
#
def check_requirement!

View File

@ -11,7 +11,7 @@ class Bundler::Thor
super
@lazy_default = options[:lazy_default]
@group = options[:group].to_s.capitalize if options[:group]
@aliases = Array(options[:aliases])
@aliases = normalize_aliases(options[:aliases])
@hide = options[:hide]
end
@ -69,7 +69,7 @@ class Bundler::Thor
value.class.name.downcase.to_sym
end
new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
new(name.to_s, required: required, type: type, default: default, aliases: aliases)
end
def switch_name
@ -90,7 +90,7 @@ class Bundler::Thor
sample = "[#{sample}]".dup unless required?
if boolean?
sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-")
sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.match(/\Ano[\-_]/)
end
aliases_for_usage.ljust(padding) + sample
@ -104,6 +104,15 @@ class Bundler::Thor
end
end
def show_default?
case default
when TrueClass, FalseClass
true
else
super
end
end
VALID_TYPES.each do |type|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{type}?
@ -142,8 +151,8 @@ class Bundler::Thor
raise ArgumentError, err
elsif @check_default_type == nil
Bundler::Thor.deprecation_warning "#{err}.\n" +
'This will be rejected in the future unless you explicitly pass the options `check_default_type: false`' +
' or call `allow_incompatible_default_type!` in your code'
"This will be rejected in the future unless you explicitly pass the options `check_default_type: false`" +
" or call `allow_incompatible_default_type!` in your code"
end
end
end
@ -159,5 +168,11 @@ class Bundler::Thor
def dasherize(str)
(str.length > 1 ? "--" : "-") + str.tr("_", "-")
end
private
def normalize_aliases(aliases)
Array(aliases).map { |short| short.to_s.sub(/^(?!\-)/, "-") }
end
end
end

View File

@ -29,8 +29,10 @@ class Bundler::Thor
#
# If +stop_on_unknown+ is true, #parse will stop as soon as it encounters
# an unknown option or a regular argument.
def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false)
def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false, relations = {})
@stop_on_unknown = stop_on_unknown
@exclusives = (relations[:exclusive_option_names] || []).select{|array| !array.empty?}
@at_least_ones = (relations[:at_least_one_option_names] || []).select{|array| !array.empty?}
@disable_required_check = disable_required_check
options = hash_options.values
super(options)
@ -50,8 +52,7 @@ class Bundler::Thor
options.each do |option|
@switches[option.switch_name] = option
option.aliases.each do |short|
name = short.to_s.sub(/^(?!\-)/, "-")
option.aliases.each do |name|
@shorts[name] ||= option.switch_name
end
end
@ -101,7 +102,7 @@ class Bundler::Thor
unshift($1.split("").map { |f| "-#{f}" })
next
when EQ_RE
unshift($2, :is_value => true)
unshift($2, is_value: true)
switch = $1
when SHORT_NUM
unshift($2)
@ -132,12 +133,38 @@ class Bundler::Thor
end
check_requirement! unless @disable_required_check
check_exclusive!
check_at_least_one!
assigns = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
assigns.freeze
assigns
end
def check_exclusive!
opts = @assigns.keys
# When option A and B are exclusive, if A and B are given at the same time,
# the diffrence of argument array size will decrease.
found = @exclusives.find{ |ex| (ex - opts).size < ex.size - 1 }
if found
names = names_to_switch_names(found & opts).map{|n| "'#{n}'"}
class_name = self.class.name.split("::").last.downcase
fail ExclusiveArgumentError, "Found exclusive #{class_name} #{names.join(", ")}"
end
end
def check_at_least_one!
opts = @assigns.keys
# When at least one is required of the options A and B,
# if the both options were not given, none? would be true.
found = @at_least_ones.find{ |one_reqs| one_reqs.none?{ |o| opts.include? o} }
if found
names = names_to_switch_names(found).map{|n| "'#{n}'"}
class_name = self.class.name.split("::").last.downcase
fail AtLeastOneRequiredArgumentError, "Not found at least one of required #{class_name} #{names.join(", ")}"
end
end
def check_unknown!
to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
@ -148,6 +175,17 @@ class Bundler::Thor
protected
# Option names changes to swith name or human name
def names_to_switch_names(names = [])
@switches.map do |_, o|
if names.include? o.name
o.respond_to?(:switch_name) ? o.switch_name : o.human_name
else
nil
end
end.compact
end
def assign_result!(option, result)
if option.repeatable && option.type == :hash
(@assigns[option.human_name] ||= {}).merge!(result)

View File

@ -23,7 +23,7 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
initialize_thorfiles(meth)
klass, command = Bundler::Thor::Util.find_class_and_command_by_namespace(meth)
self.class.handle_no_command_error(command, false) if klass.nil?
klass.start(["-h", command].compact, :shell => shell)
klass.start(["-h", command].compact, shell: shell)
else
super
end
@ -38,11 +38,11 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
klass, command = Bundler::Thor::Util.find_class_and_command_by_namespace(meth)
self.class.handle_no_command_error(command, false) if klass.nil?
args.unshift(command) if command
klass.start(args, :shell => shell)
klass.start(args, shell: shell)
end
desc "install NAME", "Install an optionally named Bundler::Thor file into your system commands"
method_options :as => :string, :relative => :boolean, :force => :boolean
method_options as: :string, relative: :boolean, force: :boolean
def install(name) # rubocop:disable Metrics/MethodLength
initialize_thorfiles
@ -53,7 +53,7 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
package = :file
require "open-uri"
begin
contents = URI.send(:open, name, &:read) # Using `send` for Ruby 2.4- support
contents = URI.open(name, &:read)
rescue OpenURI::HTTPError
raise Error, "Error opening URI '#{name}'"
end
@ -69,7 +69,7 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
base = name
package = :file
require "open-uri"
contents = URI.send(:open, name, &:read) # for ruby 2.1-2.4
contents = URI.open(name, &:read)
end
rescue Errno::ENOENT
raise Error, "Error opening file '#{name}'"
@ -101,9 +101,9 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
end
thor_yaml[as] = {
:filename => Digest::SHA256.hexdigest(name + as),
:location => location,
:namespaces => Bundler::Thor::Util.namespaces_in_content(contents, base)
filename: Digest::SHA256.hexdigest(name + as),
location: location,
namespaces: Bundler::Thor::Util.namespaces_in_content(contents, base)
}
save_yaml(thor_yaml)
@ -164,14 +164,14 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc:
end
desc "installed", "List the installed Bundler::Thor modules and commands"
method_options :internal => :boolean
method_options internal: :boolean
def installed
initialize_thorfiles(nil, true)
display_klasses(true, options["internal"])
end
desc "list [SEARCH]", "List the available thor commands (--substring means .*SEARCH)"
method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean
method_options substring: :boolean, group: :string, all: :boolean, debug: :boolean
def list(search = "")
initialize_thorfiles
@ -313,7 +313,7 @@ private
say shell.set_color(namespace, :blue, true)
say "-" * namespace.size
print_table(list, :truncate => true)
print_table(list, truncate: true)
say
end
alias_method :display_tasks, :display_commands

View File

@ -75,7 +75,7 @@ class Bundler::Thor
# Allow shell to be shared between invocations.
#
def _shared_configuration #:nodoc:
super.merge!(:shell => shell)
super.merge!(shell: shell)
end
end
end

View File

@ -1,8 +1,10 @@
require_relative "column_printer"
require_relative "table_printer"
require_relative "wrapped_printer"
class Bundler::Thor
module Shell
class Basic
DEFAULT_TERMINAL_WIDTH = 80
attr_accessor :base
attr_reader :padding
@ -145,14 +147,14 @@ class Bundler::Thor
# "yes".
#
def yes?(statement, color = nil)
!!(ask(statement, color, :add_to_history => false) =~ is?(:yes))
!!(ask(statement, color, add_to_history: false) =~ is?(:yes))
end
# Make a question the to user and returns true if the user replies "n" or
# "no".
#
def no?(statement, color = nil)
!!(ask(statement, color, :add_to_history => false) =~ is?(:no))
!!(ask(statement, color, add_to_history: false) =~ is?(:no))
end
# Prints values in columns
@ -161,16 +163,8 @@ class Bundler::Thor
# Array[String, String, ...]
#
def print_in_columns(array)
return if array.empty?
colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
array.each_with_index do |value, index|
# Don't output trailing spaces when printing the last column
if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
stdout.puts value
else
stdout.printf("%-#{colwidth}s", value)
end
end
printer = ColumnPrinter.new(stdout)
printer.print(array)
end
# Prints a table.
@ -181,58 +175,11 @@ class Bundler::Thor
# ==== Options
# indent<Integer>:: Indent the first column by indent value.
# colwidth<Integer>:: Force the first column to colwidth spaces wide.
# borders<Boolean>:: Adds ascii borders.
#
def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength
return if array.empty?
formats = []
indent = options[:indent].to_i
colwidth = options[:colwidth]
options[:truncate] = terminal_width if options[:truncate] == true
formats << "%-#{colwidth + 2}s".dup if colwidth
start = colwidth ? 1 : 0
colcount = array.max { |a, b| a.size <=> b.size }.size
maximas = []
start.upto(colcount - 1) do |index|
maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max
maximas << maxima
formats << if index == colcount - 1
# Don't output 2 trailing spaces when printing the last column
"%-s".dup
else
"%-#{maxima + 2}s".dup
end
end
formats[0] = formats[0].insert(0, " " * indent)
formats << "%s"
array.each do |row|
sentence = "".dup
row.each_with_index do |column, index|
maxima = maximas[index]
f = if column.is_a?(Numeric)
if index == row.size - 1
# Don't output 2 trailing spaces when printing the last column
"%#{maxima}s"
else
"%#{maxima}s "
end
else
formats[index]
end
sentence << f % column.to_s
end
sentence = truncate(sentence, options[:truncate]) if options[:truncate]
stdout.puts sentence
end
printer = TablePrinter.new(stdout, options)
printer.print(array)
end
# Prints a long string, word-wrapping the text to the current width of the
@ -245,33 +192,8 @@ class Bundler::Thor
# indent<Integer>:: Indent each line of the printed paragraph by indent value.
#
def print_wrapped(message, options = {})
indent = options[:indent] || 0
width = terminal_width - indent
paras = message.split("\n\n")
paras.map! do |unwrapped|
words = unwrapped.split(" ")
counter = words.first.length
words.inject do |memo, word|
word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n")
counter = 0 if word.include? "\n"
if (counter + word.length + 1) < width
memo = "#{memo} #{word}"
counter += (word.length + 1)
else
memo = "#{memo}\n#{word}"
counter = word.length
end
memo
end
end.compact!
paras.each do |para|
para.split("\n").each do |line|
stdout.puts line.insert(0, " " * indent)
end
stdout.puts unless para == paras.last
end
printer = WrappedPrinter.new(stdout, options)
printer.print(message)
end
# Deals with file collision and returns true if the file should be
@ -289,7 +211,7 @@ class Bundler::Thor
loop do
answer = ask(
%[Overwrite #{destination}? (enter "h" for help) #{options}],
:add_to_history => false
add_to_history: false
)
case answer
@ -316,24 +238,11 @@ class Bundler::Thor
say "Please specify merge tool to `THOR_MERGE` env."
else
say file_collision_help
say file_collision_help(block_given?)
end
end
end
# This code was copied from Rake, available under MIT-LICENSE
# Copyright (c) 2003, 2004 Jim Weirich
def terminal_width
result = if ENV["THOR_COLUMNS"]
ENV["THOR_COLUMNS"].to_i
else
unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH
end
result < 10 ? DEFAULT_TERMINAL_WIDTH : result
rescue
DEFAULT_TERMINAL_WIDTH
end
# Called if something goes wrong during the execution. This is used by Bundler::Thor
# internally and should not be used inside your scripts. If something went
# wrong, you can always raise an exception. If you raise a Bundler::Thor::Error, it
@ -384,16 +293,21 @@ class Bundler::Thor
end
end
def file_collision_help #:nodoc:
<<-HELP
def file_collision_help(block_given) #:nodoc:
help = <<-HELP
Y - yes, overwrite
n - no, do not overwrite
a - all, overwrite this and all others
q - quit, abort
d - diff, show the differences between the old and the new
h - help, show this help
m - merge, run merge tool
HELP
if block_given
help << <<-HELP
d - diff, show the differences between the old and the new
m - merge, run merge tool
HELP
end
help
end
def show_diff(destination, content) #:nodoc:
@ -411,46 +325,8 @@ class Bundler::Thor
mute? || (base && base.options[:quiet])
end
# Calculate the dynamic width of the terminal
def dynamic_width
@dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
end
def dynamic_width_stty
`stty size 2>/dev/null`.split[1].to_i
end
def dynamic_width_tput
`tput cols 2>/dev/null`.to_i
end
def unix?
RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i
end
def truncate(string, width)
as_unicode do
chars = string.chars.to_a
if chars.length <= width
chars.join
else
chars[0, width - 3].join + "..."
end
end
end
if "".respond_to?(:encode)
def as_unicode
yield
end
else
def as_unicode
old = $KCODE
$KCODE = "U"
yield
ensure
$KCODE = old
end
Terminal.unix?
end
def ask_simply(statement, color, options)

View File

@ -1,4 +1,5 @@
require_relative "basic"
require_relative "lcs_diff"
class Bundler::Thor
module Shell
@ -6,6 +7,8 @@ class Bundler::Thor
# Bundler::Thor::Shell::Basic to see all available methods.
#
class Color < Basic
include LCSDiff
# Embed in a String to clear all previous ANSI sequences.
CLEAR = "\e[0m"
# The start of an ANSI bold sequence.
@ -105,52 +108,7 @@ class Bundler::Thor
end
def are_colors_disabled?
!ENV['NO_COLOR'].nil? && !ENV['NO_COLOR'].empty?
end
# Overwrite show_diff to show diff with colors if Diff::LCS is
# available.
#
def show_diff(destination, content) #:nodoc:
if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil?
actual = File.binread(destination).to_s.split("\n")
content = content.to_s.split("\n")
Diff::LCS.sdiff(actual, content).each do |diff|
output_diff_line(diff)
end
else
super
end
end
def output_diff_line(diff) #:nodoc:
case diff.action
when "-"
say "- #{diff.old_element.chomp}", :red, true
when "+"
say "+ #{diff.new_element.chomp}", :green, true
when "!"
say "- #{diff.old_element.chomp}", :red, true
say "+ #{diff.new_element.chomp}", :green, true
else
say " #{diff.old_element.chomp}", nil, true
end
end
# Check if Diff::LCS is loaded. If it is, use it to create pretty output
# for diff.
#
def diff_lcs_loaded? #:nodoc:
return true if defined?(Diff::LCS)
return @diff_lcs_loaded unless @diff_lcs_loaded.nil?
@diff_lcs_loaded = begin
require "diff/lcs"
true
rescue LoadError
false
end
!ENV["NO_COLOR"].nil? && !ENV["NO_COLOR"].empty?
end
end
end

View File

@ -0,0 +1,29 @@
require_relative "terminal"
class Bundler::Thor
module Shell
class ColumnPrinter
attr_reader :stdout, :options
def initialize(stdout, options = {})
@stdout = stdout
@options = options
@indent = options[:indent].to_i
end
def print(array)
return if array.empty?
colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
array.each_with_index do |value, index|
# Don't output trailing spaces when printing the last column
if ((((index + 1) % (Terminal.terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
stdout.puts value
else
stdout.printf("%-#{colwidth}s", value)
end
end
end
end
end
end

View File

@ -1,4 +1,5 @@
require_relative "basic"
require_relative "lcs_diff"
class Bundler::Thor
module Shell
@ -6,6 +7,8 @@ class Bundler::Thor
# Bundler::Thor::Shell::Basic to see all available methods.
#
class HTML < Basic
include LCSDiff
# The start of an HTML bold sequence.
BOLD = "font-weight: bold"
@ -76,51 +79,6 @@ class Bundler::Thor
def can_display_colors?
true
end
# Overwrite show_diff to show diff with colors if Diff::LCS is
# available.
#
def show_diff(destination, content) #:nodoc:
if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil?
actual = File.binread(destination).to_s.split("\n")
content = content.to_s.split("\n")
Diff::LCS.sdiff(actual, content).each do |diff|
output_diff_line(diff)
end
else
super
end
end
def output_diff_line(diff) #:nodoc:
case diff.action
when "-"
say "- #{diff.old_element.chomp}", :red, true
when "+"
say "+ #{diff.new_element.chomp}", :green, true
when "!"
say "- #{diff.old_element.chomp}", :red, true
say "+ #{diff.new_element.chomp}", :green, true
else
say " #{diff.old_element.chomp}", nil, true
end
end
# Check if Diff::LCS is loaded. If it is, use it to create pretty output
# for diff.
#
def diff_lcs_loaded? #:nodoc:
return true if defined?(Diff::LCS)
return @diff_lcs_loaded unless @diff_lcs_loaded.nil?
@diff_lcs_loaded = begin
require "diff/lcs"
true
rescue LoadError
false
end
end
end
end
end

View File

@ -0,0 +1,49 @@
module LCSDiff
protected
# Overwrite show_diff to show diff with colors if Diff::LCS is
# available.
def show_diff(destination, content) #:nodoc:
if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil?
actual = File.binread(destination).to_s.split("\n")
content = content.to_s.split("\n")
Diff::LCS.sdiff(actual, content).each do |diff|
output_diff_line(diff)
end
else
super
end
end
private
def output_diff_line(diff) #:nodoc:
case diff.action
when "-"
say "- #{diff.old_element.chomp}", :red, true
when "+"
say "+ #{diff.new_element.chomp}", :green, true
when "!"
say "- #{diff.old_element.chomp}", :red, true
say "+ #{diff.new_element.chomp}", :green, true
else
say " #{diff.old_element.chomp}", nil, true
end
end
# Check if Diff::LCS is loaded. If it is, use it to create pretty output
# for diff.
def diff_lcs_loaded? #:nodoc:
return true if defined?(Diff::LCS)
return @diff_lcs_loaded unless @diff_lcs_loaded.nil?
@diff_lcs_loaded = begin
require "diff/lcs"
true
rescue LoadError
false
end
end
end

View File

@ -0,0 +1,134 @@
require_relative "column_printer"
require_relative "terminal"
class Bundler::Thor
module Shell
class TablePrinter < ColumnPrinter
BORDER_SEPARATOR = :separator
def initialize(stdout, options = {})
super
@formats = []
@maximas = []
@colwidth = options[:colwidth]
@truncate = options[:truncate] == true ? Terminal.terminal_width : options[:truncate]
@padding = 1
end
def print(array)
return if array.empty?
prepare(array)
print_border_separator if options[:borders]
array.each do |row|
if options[:borders] && row == BORDER_SEPARATOR
print_border_separator
next
end
sentence = "".dup
row.each_with_index do |column, index|
sentence << format_cell(column, row.size, index)
end
sentence = truncate(sentence)
sentence << "|" if options[:borders]
stdout.puts indentation + sentence
end
print_border_separator if options[:borders]
end
private
def prepare(array)
array = array.reject{|row| row == BORDER_SEPARATOR }
@formats << "%-#{@colwidth + 2}s".dup if @colwidth
start = @colwidth ? 1 : 0
colcount = array.max { |a, b| a.size <=> b.size }.size
start.upto(colcount - 1) do |index|
maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max
@maximas << maxima
@formats << if options[:borders]
"%-#{maxima}s".dup
elsif index == colcount - 1
# Don't output 2 trailing spaces when printing the last column
"%-s".dup
else
"%-#{maxima + 2}s".dup
end
end
@formats << "%s"
end
def format_cell(column, row_size, index)
maxima = @maximas[index]
f = if column.is_a?(Numeric)
if options[:borders]
# With borders we handle padding separately
"%#{maxima}s"
elsif index == row_size - 1
# Don't output 2 trailing spaces when printing the last column
"%#{maxima}s"
else
"%#{maxima}s "
end
else
@formats[index]
end
cell = "".dup
cell << "|" + " " * @padding if options[:borders]
cell << f % column.to_s
cell << " " * @padding if options[:borders]
cell
end
def print_border_separator
separator = @maximas.map do |maxima|
"+" + "-" * (maxima + 2 * @padding)
end
stdout.puts indentation + separator.join + "+"
end
def truncate(string)
return string unless @truncate
as_unicode do
chars = string.chars.to_a
if chars.length <= @truncate
chars.join
else
chars[0, @truncate - 3 - @indent].join + "..."
end
end
end
def indentation
" " * @indent
end
if "".respond_to?(:encode)
def as_unicode
yield
end
else
def as_unicode
old = $KCODE # rubocop:disable Style/GlobalVars
$KCODE = "U" # rubocop:disable Style/GlobalVars
yield
ensure
$KCODE = old # rubocop:disable Style/GlobalVars
end
end
end
end
end

View File

@ -0,0 +1,42 @@
class Bundler::Thor
module Shell
module Terminal
DEFAULT_TERMINAL_WIDTH = 80
class << self
# This code was copied from Rake, available under MIT-LICENSE
# Copyright (c) 2003, 2004 Jim Weirich
def terminal_width
result = if ENV["THOR_COLUMNS"]
ENV["THOR_COLUMNS"].to_i
else
unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH
end
result < 10 ? DEFAULT_TERMINAL_WIDTH : result
rescue
DEFAULT_TERMINAL_WIDTH
end
def unix?
RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i
end
private
# Calculate the dynamic width of the terminal
def dynamic_width
@dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
end
def dynamic_width_stty
`stty size 2>/dev/null`.split[1].to_i
end
def dynamic_width_tput
`tput cols 2>/dev/null`.to_i
end
end
end
end
end

View File

@ -0,0 +1,38 @@
require_relative "column_printer"
require_relative "terminal"
class Bundler::Thor
module Shell
class WrappedPrinter < ColumnPrinter
def print(message)
width = Terminal.terminal_width - @indent
paras = message.split("\n\n")
paras.map! do |unwrapped|
words = unwrapped.split(" ")
counter = words.first.length
words.inject do |memo, word|
word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n")
counter = 0 if word.include? "\n"
if (counter + word.length + 1) < width
memo = "#{memo} #{word}"
counter += (word.length + 1)
else
memo = "#{memo}\n#{word}"
counter = word.length
end
memo
end
end.compact!
paras.each do |para|
para.split("\n").each do |line|
stdout.puts line.insert(0, " " * @indent)
end
stdout.puts unless para == paras.last
end
end
end
end
end

View File

@ -130,9 +130,10 @@ class Bundler::Thor
#
def find_class_and_command_by_namespace(namespace, fallback = true)
if namespace.include?(":") # look for a namespaced command
pieces = namespace.split(":")
command = pieces.pop
klass = Bundler::Thor::Util.find_by_namespace(pieces.join(":"))
*pieces, command = namespace.split(":")
namespace = pieces.join(":")
namespace = "default" if namespace.empty?
klass = Bundler::Thor::Base.subclasses.detect { |thor| thor.namespace == namespace && thor.commands.keys.include?(command) }
end
unless klass # look for a Bundler::Thor::Group with the right name
klass = Bundler::Thor::Util.find_by_namespace(namespace)

View File

@ -1,3 +1,3 @@
class Bundler::Thor
VERSION = "1.2.2"
VERSION = "1.3.0"
end