require_relative "../thor"
require_relative "group"
require "yaml"
require "digest/sha2"
require "pathname"
class Thor::Runner < Thor #:nodoc:
map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version
def self.banner(command, all = false, subcommand = false)
"thor " + command.formatted_usage(self, all, subcommand)
end
def self.exit_on_failure?
true
end
# Override Thor#help so it can give information about any class and any method.
#
def help(meth = nil)
if meth && !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 Metrics/MethodLength
initialize_thorfiles
is_uri = name =~ %r{^https?\://}
if is_uri
base = name
package = :file
require "open-uri"
begin
contents = URI.open(name, &:read)
rescue OpenURI::HTTPError
raise Error, "Error opening URI '#{name}'"
end
else
# 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 = File.join(name, "main.thor")
package = :directory
contents = File.open(base, &:read)
else
base = name
package = :file
require "open-uri"
contents = URI.open(name, &:read)
end
rescue Errno::ENOENT
raise Error, "Error opening file '#{name}'"
end
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] || is_uri
name
else
File.expand_path(name)
end
thor_yaml[as] = {
filename: Digest::SHA256.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
require "fileutils"
FileUtils.cp_r(name, destination)
end
thor_yaml[as][:filename] # Indicate success
end
desc "version", "Show Thor version"
def version
require_relative "version"
say "Thor #{Thor::VERSION}"
end
desc "uninstall NAME", "Uninstall a named Thor module"
def uninstall(name)
raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
say "Uninstalling #{name}."
require "fileutils"
FileUtils.rm_rf(File.join(thor_root, (thor_yaml[name][:filename]).to_s))
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)
raise 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)
require "fileutils"
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
File.delete(File.join(thor_root, old_filename)) unless filename == old_filename
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 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)
require "fileutils"
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
# 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 |_, v|
v[:namespaces] && !(v[:namespaces] & lookup).empty?
end
files.map { |_, v| File.join(thor_root, (v[:filename]).to_s) }
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
raise 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