#!/usr/bin/env ruby require "thor" require "open-uri" require "fileutils" require "yaml" require "digest/md5" require "readline" module ObjectSpace class << self # ==== Returns # Array[Class]:: All the classes in the object space. def classes klasses = [] ObjectSpace.each_object(Class) {|o| klasses << o} klasses end end end class Thor::Util # @public def self.constant_to_thor_path(str) snake_case(str).squeeze(":") end # @public def self.constant_from_thor_path(str) make_constant(to_constant(str)) end def self.to_constant(str) str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } end def self.constants_in_contents(str) klasses = self.constants.dup eval(str) ret = self.constants - klasses ret.each {|k| self.send(:remove_const, k)} ret end private # @private def self.make_constant(str) list = str.split("::") obj = Object list.each {|x| obj = obj.const_get(x) } obj end # @private def self.snake_case(str) return str.downcase if str =~ /^[A-Z]+$/ str.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/ return $+.downcase end end class Thor::Runner < Thor def self.globs_for(path) ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] end def initialize_thorfiles(include_system = true) thorfiles(include_system).each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))} end map "-T" => :list, "-i" => :install, "-u" => :update desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates" method_options :as => :optional def install(name, opts) initialize_thorfiles begin contents = open(name).read rescue OpenURI::HTTPError puts "The URI you provided: `#{name}' was invalid" return rescue Errno::ENOENT puts "`#{name}' is not a valid file" return end puts "Your Thorfile contains: " puts contents print "Do you wish to continue [y/N]? " response = Readline.readline return unless response =~ /^\s*y/i constants = Thor::Util.constants_in_contents(contents) name = name =~ /\.thor$/ ? name : "#{name}.thor" as = opts["as"] || begin first_line = contents.split("\n")[0] (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil end if !as print "Please specify a name for #{name} in the system repository [#{name}]: " as = Readline.readline as = name if as.empty? end FileUtils.mkdir_p thor_root yaml_file = File.join(thor_root, "thor.yml") FileUtils.touch(yaml_file) yaml = thor_yaml yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => name, :constants => constants} save_yaml(yaml) puts "Storing thor file in your system repository" File.open(File.join(thor_root, yaml[as][:filename] + ".thor"), "w") do |file| file.puts contents end end desc "uninstall NAME", "uninstall a named Thor module" def uninstall(name) yaml = thor_yaml unless yaml[name] puts "There was no module by that name installed" return end puts "Uninstalling #{name}." file = File.join(thor_root, "#{yaml[name][:filename]}.thor") File.delete(file) yaml.delete(name) save_yaml(yaml) puts "Done." end desc "update NAME", "update a Thor file from its original location" def update(name) yaml = thor_yaml if !yaml[name] || !yaml[name][:location] puts "`#{name}' was not found in the system repository" else puts "Updating `#{name}' from #{yaml[name][:location]}" install(yaml[name][:location], "as" => name) end end def installed Dir["#{ENV["HOME"]}/.thor/**/*.thor"].each do |f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f)) end display_klasses(true) end desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)" method_options :substring => :boolean def list(search = "", options = {}) initialize_thorfiles search = ".*#{search}" if options["substring"] search = /^#{search}.*/i display_klasses(false, Thor.subclasses.select {|k| Thor::Util.constant_to_thor_path(k.name) =~ search}) end def method_missing(meth, *args) initialize_thorfiles(false) meth = meth.to_s unless meth =~ /:/ puts "Thor tasks must contain a :" return end thor_klass = meth.split(":")[0...-1].join(":") to_call = meth.split(":").last yaml = thor_yaml klass_str = Thor::Util.to_constant(thor_klass) files = yaml.inject([]) { |a,(k,v)| a << v[:filename] if v[:constants] && v[:constants].include?(klass_str); a } unless files.empty? files.each do |f| load File.join(thor_root, "#{f}.thor") end klass = Thor::Util.constant_from_thor_path(thor_klass) else begin klass = Thor::Util.constant_from_thor_path(thor_klass) rescue puts "There was no available namespace `#{thor_klass}'." return end end unless klass.ancestors.include?(Thor) puts "`#{thor_klass}' is not a Thor module" return end ARGV.replace [to_call, *(args + ARGV)].compact begin klass.start rescue ArgumentError puts "You need to call #{to_call} as `#{klass.usage_for_method(to_call)}'" rescue NoMethodError puts "`#{to_call}' is not available in #{thor_klass}" end end private def thor_root File.join(ENV["HOME"], ".thor") end def thor_yaml yaml_file = File.join(thor_root, "thor.yml") yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file) yaml || {} end def save_yaml(yaml) yaml_file = File.join(thor_root, "thor.yml") File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml } end def display_klasses(with_modules = false, klasses = Thor.subclasses) klasses = klasses - [Thor::Runner] if klasses.empty? puts "No thorfiles available" return end if with_modules yaml = thor_yaml max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size print "%-#{max_name + 4}s" % "Name" puts "Modules" print "%-#{max_name + 4}s" % "----" puts "-------" yaml.each do |name, info| print "%-#{max_name + 4}s" % name puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ") end puts end puts "Tasks" puts "-----" # Calculate the largest base class name max_base = klasses.max do |x,y| Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size end.name.size # Calculate the size of the largest option description max_left_item = klasses.max do |x,y| (x.help_list && x.help_list.max.usage + x.help_list.max.opt).to_i <=> (y.help_list && y.help_list.max.usage + y.help_list.max.opt).to_i end max_left = max_left_item.help_list.max.usage + max_left_item.help_list.max.opt klasses.map {|k| k.help_list}.compact.each do |item| display_tasks(item, max_base, max_left) end end def display_tasks(item, max_base, max_left) base = Thor::Util.constant_to_thor_path(item.klass.name) item.usages.each do |name, usage| format_string = "%-#{max_left + max_base + 5}s" print format_string % "#{base}:#{item.usages.assoc(name).last} #{display_opts(item.opts.assoc(name) && item.opts.assoc(name).last)}" puts item.descriptions.assoc(name).last end end def display_opts(opts) return "" unless opts opts.map do |opt, val| if val == true || val == "BOOLEAN" "[#{opt}]" elsif val == "REQUIRED" opt + "=" + opt.gsub(/\-/, "").upcase elsif val == "OPTIONAL" "[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]" end end.join(" ") end def thorfiles(include_system = true) path = Dir.pwd system_thorfiles = Dir["#{ENV["HOME"]}/.thor/**/*.thor"] thorfiles = [] # Look for Thorfile or *.thor in the current directory or a parent directory, until the root while thorfiles.empty? thorfiles = Dir[*Thor::Runner.globs_for(path)] path = File.dirname(path) break if path == "/" end thorfiles + (include_system ? system_thorfiles : []) end end unless defined?(Spec) Thor::Runner.start end