323 lines
9.6 KiB
Ruby
323 lines
9.6 KiB
Ruby
|
require 'thor'
|
||
|
require 'thor/group'
|
||
|
require 'thor/core_ext/io_binary_read'
|
||
|
|
||
|
require 'fileutils'
|
||
|
require 'open-uri'
|
||
|
require 'yaml'
|
||
|
require 'digest/md5'
|
||
|
require 'pathname'
|
||
|
|
||
|
class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
|
||
|
map '-T' => :list, '-i' => :install, '-u' => :update, '-v' => :version
|
||
|
|
||
|
# Override Thor#help so it can give information about any class and any method.
|
||
|
#
|
||
|
def help(meth = nil)
|
||
|
if meth && !self.respond_to?(meth)
|
||
|
initialize_thorfiles(meth)
|
||
|
klass, command = 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)
|
||
|
else
|
||
|
super
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# If a command is not found on Thor::Runner, method missing is invoked and
|
||
|
# Thor::Runner is then responsible for finding the command in all classes.
|
||
|
#
|
||
|
def method_missing(meth, *args)
|
||
|
meth = meth.to_s
|
||
|
initialize_thorfiles(meth)
|
||
|
klass, command = 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)
|
||
|
end
|
||
|
|
||
|
desc 'install NAME', 'Install an optionally named Thor file into your system commands'
|
||
|
method_options :as => :string, :relative => :boolean, :force => :boolean
|
||
|
def install(name) # rubocop:disable MethodLength
|
||
|
initialize_thorfiles
|
||
|
|
||
|
# If a directory name is provided as the argument, look for a 'main.thor'
|
||
|
# command in said directory.
|
||
|
begin
|
||
|
if File.directory?(File.expand_path(name))
|
||
|
base, package = File.join(name, 'main.thor'), :directory
|
||
|
contents = open(base) { |input| input.read }
|
||
|
else
|
||
|
base, package = name, :file
|
||
|
contents = open(name) { |input| input.read }
|
||
|
end
|
||
|
rescue OpenURI::HTTPError
|
||
|
raise Error, "Error opening URI '#{name}'"
|
||
|
rescue Errno::ENOENT
|
||
|
fail Error, "Error opening file '#{name}'"
|
||
|
end
|
||
|
|
||
|
say 'Your Thorfile contains:'
|
||
|
say contents
|
||
|
|
||
|
unless options['force']
|
||
|
return false if no?('Do you wish to continue [y/N]?')
|
||
|
end
|
||
|
|
||
|
as = options['as'] || begin
|
||
|
first_line = contents.split("\n")[0]
|
||
|
(match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
|
||
|
end
|
||
|
|
||
|
unless as
|
||
|
basename = File.basename(name)
|
||
|
as = ask("Please specify a name for #{name} in the system repository [#{basename}]:")
|
||
|
as = basename if as.empty?
|
||
|
end
|
||
|
|
||
|
location = if options[:relative] || name =~ %r{^https?://}
|
||
|
name
|
||
|
else
|
||
|
File.expand_path(name)
|
||
|
end
|
||
|
|
||
|
thor_yaml[as] = {
|
||
|
:filename => Digest::MD5.hexdigest(name + as),
|
||
|
:location => location,
|
||
|
:namespaces => Thor::Util.namespaces_in_content(contents, base)
|
||
|
}
|
||
|
|
||
|
save_yaml(thor_yaml)
|
||
|
say 'Storing thor file in your system repository'
|
||
|
destination = File.join(thor_root, thor_yaml[as][:filename])
|
||
|
|
||
|
if package == :file
|
||
|
File.open(destination, 'w') { |f| f.puts contents }
|
||
|
else
|
||
|
FileUtils.cp_r(name, destination)
|
||
|
end
|
||
|
|
||
|
thor_yaml[as][:filename] # Indicate success
|
||
|
end
|
||
|
|
||
|
desc 'version', 'Show Thor version'
|
||
|
def version
|
||
|
require 'thor/version'
|
||
|
say "Thor #{Thor::VERSION}"
|
||
|
end
|
||
|
|
||
|
desc 'uninstall NAME', 'Uninstall a named Thor module'
|
||
|
def uninstall(name)
|
||
|
fail Error, "Can't find module '#{name}'" unless thor_yaml[name]
|
||
|
say "Uninstalling #{name}."
|
||
|
FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}"))
|
||
|
|
||
|
thor_yaml.delete(name)
|
||
|
save_yaml(thor_yaml)
|
||
|
|
||
|
puts 'Done.'
|
||
|
end
|
||
|
|
||
|
desc 'update NAME', 'Update a Thor file from its original location'
|
||
|
def update(name)
|
||
|
fail Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
|
||
|
|
||
|
say "Updating '#{name}' from #{thor_yaml[name][:location]}"
|
||
|
|
||
|
old_filename = thor_yaml[name][:filename]
|
||
|
self.options = options.merge('as' => name)
|
||
|
|
||
|
if File.directory? File.expand_path(name)
|
||
|
FileUtils.rm_rf(File.join(thor_root, old_filename))
|
||
|
|
||
|
thor_yaml.delete(old_filename)
|
||
|
save_yaml(thor_yaml)
|
||
|
|
||
|
filename = install(name)
|
||
|
else
|
||
|
filename = install(thor_yaml[name][:location])
|
||
|
end
|
||
|
|
||
|
unless filename == old_filename
|
||
|
File.delete(File.join(thor_root, old_filename))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
desc 'installed', 'List the installed Thor modules and commands'
|
||
|
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
|
||
|
def list(search = '')
|
||
|
initialize_thorfiles
|
||
|
|
||
|
search = ".*#{search}" if options['substring']
|
||
|
search = /^#{search}.*/i
|
||
|
group = options[:group] || 'standard'
|
||
|
|
||
|
klasses = Thor::Base.subclasses.select do |k|
|
||
|
(options[:all] || k.group == group) && k.namespace =~ search
|
||
|
end
|
||
|
|
||
|
display_klasses(false, false, klasses)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def self.banner(command, all = false, subcommand = false)
|
||
|
'thor ' + command.formatted_usage(self, all, subcommand)
|
||
|
end
|
||
|
|
||
|
def thor_root
|
||
|
Thor::Util.thor_root
|
||
|
end
|
||
|
|
||
|
def thor_yaml
|
||
|
@thor_yaml ||= begin
|
||
|
yaml_file = File.join(thor_root, 'thor.yml')
|
||
|
yaml = YAML.load_file(yaml_file) if File.exist?(yaml_file)
|
||
|
yaml || {}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Save the yaml file. If none exists in thor root, creates one.
|
||
|
#
|
||
|
def save_yaml(yaml)
|
||
|
yaml_file = File.join(thor_root, 'thor.yml')
|
||
|
|
||
|
unless File.exist?(yaml_file)
|
||
|
FileUtils.mkdir_p(thor_root)
|
||
|
yaml_file = File.join(thor_root, 'thor.yml')
|
||
|
FileUtils.touch(yaml_file)
|
||
|
end
|
||
|
|
||
|
File.open(yaml_file, 'w') { |f| f.puts yaml.to_yaml }
|
||
|
end
|
||
|
|
||
|
def self.exit_on_failure?
|
||
|
true
|
||
|
end
|
||
|
|
||
|
# Load the Thorfiles. If relevant_to is supplied, looks for specific files
|
||
|
# in the thor_root instead of loading them all.
|
||
|
#
|
||
|
# By default, it also traverses the current path until find Thor files, as
|
||
|
# described in thorfiles. This look up can be skipped by supplying
|
||
|
# skip_lookup true.
|
||
|
#
|
||
|
def initialize_thorfiles(relevant_to = nil, skip_lookup = false)
|
||
|
thorfiles(relevant_to, skip_lookup).each do |f|
|
||
|
Thor::Util.load_thorfile(f, nil, options[:debug]) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Finds Thorfiles by traversing from your current directory down to the root
|
||
|
# directory of your system. If at any time we find a Thor file, we stop.
|
||
|
#
|
||
|
# We also ensure that system-wide Thorfiles are loaded first, so local
|
||
|
# Thorfiles can override them.
|
||
|
#
|
||
|
# ==== Example
|
||
|
#
|
||
|
# If we start at /Users/wycats/dev/thor ...
|
||
|
#
|
||
|
# 1. /Users/wycats/dev/thor
|
||
|
# 2. /Users/wycats/dev
|
||
|
# 3. /Users/wycats <-- we find a Thorfile here, so we stop
|
||
|
#
|
||
|
# Suppose we start at c:\Documents and Settings\james\dev\thor ...
|
||
|
#
|
||
|
# 1. c:\Documents and Settings\james\dev\thor
|
||
|
# 2. c:\Documents and Settings\james\dev
|
||
|
# 3. c:\Documents and Settings\james
|
||
|
# 4. c:\Documents and Settings
|
||
|
# 5. c:\ <-- no Thorfiles found!
|
||
|
#
|
||
|
def thorfiles(relevant_to = nil, skip_lookup = false)
|
||
|
thorfiles = []
|
||
|
|
||
|
unless skip_lookup
|
||
|
Pathname.pwd.ascend do |path|
|
||
|
thorfiles = Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten
|
||
|
break unless thorfiles.empty?
|
||
|
end
|
||
|
end
|
||
|
|
||
|
files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Thor::Util.thor_root_glob)
|
||
|
files += thorfiles
|
||
|
files -= ["#{thor_root}/thor.yml"]
|
||
|
|
||
|
files.map! do |file|
|
||
|
File.directory?(file) ? File.join(file, 'main.thor') : file
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Load Thorfiles relevant to the given method. If you provide "foo:bar" it
|
||
|
# will load all thor files in the thor.yaml that has "foo" e "foo:bar"
|
||
|
# namespaces registered.
|
||
|
#
|
||
|
def thorfiles_relevant_to(meth)
|
||
|
lookup = [meth, meth.split(':')[0...-1].join(':')]
|
||
|
|
||
|
files = thor_yaml.select do |k, v|
|
||
|
v[:namespaces] && !(v[:namespaces] & lookup).empty?
|
||
|
end
|
||
|
|
||
|
files.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
|
||
|
end
|
||
|
|
||
|
# Display information about the given klasses. If with_module is given,
|
||
|
# it shows a table with information extracted from the yaml file.
|
||
|
#
|
||
|
def display_klasses(with_modules = false, show_internal = false, klasses = Thor::Base.subclasses)
|
||
|
klasses -= [Thor, Thor::Runner, Thor::Group] unless show_internal
|
||
|
|
||
|
fail Error, 'No Thor commands available' if klasses.empty?
|
||
|
show_modules if with_modules && !thor_yaml.empty?
|
||
|
|
||
|
list = Hash.new { |h, k| h[k] = [] }
|
||
|
groups = klasses.select { |k| k.ancestors.include?(Thor::Group) }
|
||
|
|
||
|
# Get classes which inherit from Thor
|
||
|
(klasses - groups).each { |k| list[k.namespace.split(':').first] += k.printable_commands(false) }
|
||
|
|
||
|
# Get classes which inherit from Thor::Base
|
||
|
groups.map! { |k| k.printable_commands(false).first }
|
||
|
list['root'] = groups
|
||
|
|
||
|
# Order namespaces with default coming first
|
||
|
list = list.sort { |a, b| a[0].sub(/^default/, '') <=> b[0].sub(/^default/, '') }
|
||
|
list.each { |n, commands| display_commands(n, commands) unless commands.empty? }
|
||
|
end
|
||
|
|
||
|
def display_commands(namespace, list) #:nodoc:
|
||
|
list.sort! { |a, b| a[0] <=> b[0] }
|
||
|
|
||
|
say shell.set_color(namespace, :blue, true)
|
||
|
say '-' * namespace.size
|
||
|
|
||
|
print_table(list, :truncate => true)
|
||
|
say
|
||
|
end
|
||
|
alias_method :display_tasks, :display_commands
|
||
|
|
||
|
def show_modules #:nodoc:
|
||
|
info = []
|
||
|
labels = %w[Modules Namespaces]
|
||
|
|
||
|
info << labels
|
||
|
info << ['-' * labels[0].size, '-' * labels[1].size]
|
||
|
|
||
|
thor_yaml.each do |name, hash|
|
||
|
info << [name, hash[:namespaces].join(', ')]
|
||
|
end
|
||
|
|
||
|
print_table info
|
||
|
say ''
|
||
|
end
|
||
|
end
|