Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First pass of Thor::Runner

  • Loading branch information...
commit b522f05ef166d142e80e30d264ae0dfba09ac4f8 1 parent 82ff27a
@wycats wycats authored
View
7 .autotest
@@ -0,0 +1,7 @@
+Autotest.add_hook :initialize do |at|
+ at.clear_mappings
+ at.add_exception(/\.git/)
+ at.add_mapping(%r{^spec/.*_spec}) {|filename,_| at.files_matching %r{#{filename}}}
+ at.add_mapping(%r{^bin/thor}) {|_,_| at.files_matching %r{spec/thor_runner_spec}}
+ at.add_mapping(%r{}) {|_,_| at.files_matching %r{spec/.*_spec}}
+end
View
5 Rakefile
@@ -28,7 +28,9 @@ spec = Gem::Specification.new do |s|
s.require_path = 'lib'
s.autorequire = GEM
- s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,specs}/**/*")
+ s.bindir = "bin"
+ s.executables = %w( thor )
+ s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{bin,lib,specs}/**/*")
end
Rake::GemPackageTask.new(spec) do |pkg|
@@ -43,6 +45,7 @@ Spec::Rake::SpecTask.new do |t|
t.spec_opts << "-fs --color"
end
+task :specs => :spec
desc "install the gem locally"
task :install => [:package] do
View
131 bin/thor
@@ -0,0 +1,131 @@
+require "thor"
+
+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
+
+ private
+ # @private
+ def self.to_constant(str)
+ str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
+ end
+
+ # @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
+
+ map "-T" => :list
+
+ desc "list [SEARCH]", "list the available thor tasks"
+ method_options :force => :boolean
+ def list(search = "")
+ search = /.*#{search}.*/
+ thorfiles.each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
+
+ # Calculate the largest base class name
+ max_base = Thor.subclasses.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 = Thor.subclasses.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
+
+ Thor.subclasses.map {|k| k.help_list}.compact.each do |item|
+ display_tasks(item, max_base, max_left)
+ end
+ end
+
+ def method_missing(meth, *args)
+ meth = meth.to_s
+ unless meth =~ /:/
+ puts "Thor tasks must contain a :"
+ return
+ end
+
+ klass = meth.split(":")[0...-1].join(":")
+ to_call = meth.split(":").last
+ begin
+ klass = Thor::Util.constant_from_thor_path(klass)
+ rescue NameError
+ puts "There was no available namespace `#{klass}'."
+ return
+ end
+
+ ARGV.replace [to_call, ARGV[1..-1]].compact
+ klass.start
+ end
+
+ private
+ 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
+ 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
+ end
+
+end
+
+unless defined?(Spec)
+ Thor::Runner.start
+end
View
85 lib/thor.rb
@@ -1,6 +1,19 @@
require "#{File.dirname(__FILE__)}/getopt"
class Thor
+ def self.inherited(klass)
+ subclass_files[File.expand_path(caller[0].split(":")[0])] << klass
+ subclasses << klass
+ end
+
+ def self.subclass_files
+ @subclass_files ||= Hash.new {|h,k| h[k] = []}
+ end
+
+ def self.subclasses
+ @subclasses ||= []
+ end
+
def self.method_added(meth)
return if !public_instance_methods.include?(meth.to_s) || !@usage
@descriptions ||= []
@@ -26,6 +39,31 @@ def self.method_options(opts)
end
end
+ def self.help_list
+ return nil unless @usages
+ @help_list ||= begin
+ max_usage = @usages.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
+ max_opts = @opts.empty? ? 0 : format_opts(@opts.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last).size
+ max_desc = @descriptions.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
+ Struct.new(:klass, :usages, :opts, :descriptions, :max).new(
+ self, @usages, @opts, @descriptions, Struct.new(:usage, :opt, :desc).new(max_usage, max_opts, max_desc)
+ )
+ end
+ end
+
+ def self.format_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 self.start
meth = ARGV.shift
params = []
@@ -42,51 +80,22 @@ def self.start
params << options
end
new(meth, params).instance_variable_get("@results")
- end
-
- def thor_usages
- self.class.instance_variable_get("@usages")
- end
-
- def thor_descriptions
- self.class.instance_variable_get("@descriptions")
- end
-
- def thor_opts
- self.class.instance_variable_get("@opts")
end
-
def initialize(op, params)
- @results = send(op.to_sym, *params) if public_methods.include?(op)
+ @results = send(op.to_sym, *params) if public_methods.include?(op) || !methods.include?(op)
end
-
- private
- def format_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
-
- public
+
desc "help", "show this screen"
def help
+ list = self.class.help_list
puts "Options"
puts "-------"
- max_usage = thor_usages.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
- max_opts = thor_opts.empty? ? 0 : format_opts(thor_opts.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last).size
- max_desc = thor_descriptions.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
- thor_usages.each do |meth, usage|
- format = "%-" + (max_usage + max_opts + 4).to_s + "s"
- print format % (thor_usages.assoc(meth)[1] + (thor_opts.assoc(meth) ? " " + format_opts(thor_opts.assoc(meth)[1]) : ""))
- puts thor_descriptions.assoc(meth)[1]
+ list.usages.each do |meth, usage|
+ format = "%-" + (list.max.usage + list.max.opt + 4).to_s + "s"
+ print format % (list.usages.assoc(meth)[1] + (list.opts.assoc(meth) ? " " + self.class.format_opts(list.opts.assoc(meth)[1]) : ""))
+ puts list.descriptions.assoc(meth)[1]
end
- end
+ end
+
end
View
29 spec/spec_helper.rb
@@ -1,2 +1,31 @@
$TESTING=true
$:.push File.join(File.dirname(__FILE__), '..', 'lib')
+
+module Spec::Expectations::ObjectExpectations
+ alias_method :must, :should
+ alias_method :must_not, :should_not
+end
+
+class StdOutCapturer
+ attr_reader :output
+
+ def initialize
+ @output = ""
+ end
+
+ def self.call_func
+ begin
+ old_out = $stdout
+ output = new
+ $stdout = output
+ yield
+ ensure
+ $stdout = old_out
+ end
+ output.output
+ end
+
+ def write(s)
+ @output += s
+ end
+end
View
83 spec/thor_runner_spec.rb
@@ -0,0 +1,83 @@
+require File.dirname(__FILE__) + '/spec_helper'
+require "thor"
+
+load File.join("#{File.dirname(__FILE__)}", "..", "bin", "thor")
+
+class StdOutCapturer
+ attr_reader :output
+
+ def initialize
+ @output = ""
+ end
+
+ def self.call_func
+ begin
+ old_out = $stdout
+ output = new
+ $stdout = output
+ yield
+ ensure
+ $stdout = old_out
+ end
+ output.output
+ end
+
+ def write(s)
+ @output += s
+ end
+end
+
+module MyTasks
+ class ThorTask < Thor
+ desc "foo", "bar"
+ def foo
+ "foo"
+ end
+ end
+end
+
+class ThorTask2 < Thor
+end
+
+describe Thor::Util do
+ it "knows how to convert class names into thor names" do
+ Thor::Util.constant_to_thor_path("FooBar::BarBaz::BazBat").must == "foo_bar:bar_baz:baz_bat"
+ end
+
+ it "knows how to convert a thor name to a constant" do
+ Thor::Util.constant_from_thor_path("my_tasks:thor_task").must == MyTasks::ThorTask
+ end
+end
+
+describe Thor do
+ it "tracks its subclasses, grouped by the files they come from" do
+ Thor.subclass_files[File.expand_path(__FILE__)].must == [MyTasks::ThorTask, ThorTask2]
+ end
+
+ it "tracks its subclasses in an Array" do
+ Thor.subclasses.must include(MyTasks::ThorTask)
+ Thor.subclasses.must include(ThorTask2)
+ end
+end
+
+describe Thor::Runner do
+ it "can give a list of the available tasks" do
+ ARGV.replace ["list"]
+ StdOutCapturer.call_func { Thor::Runner.start }.must =~ /my_tasks:thor_task:foo +bar/
+ end
+
+ it "runs tasks from other Thor files" do
+ ARGV.replace ["my_tasks:thor_task:foo"]
+ Thor::Runner.start.should == "foo"
+ end
+
+ it "prints an error if a thor task is not namespaced" do
+ ARGV.replace ["hello"]
+ StdOutCapturer.call_func { Thor::Runner.start }.must =~ /Thor tasks must contain a :/
+ end
+
+ it "prints an error if the namespace could not be found" do
+ ARGV.replace ["hello:goodbye"]
+ StdOutCapturer.call_func { Thor::Runner.start }.must =~ /There was no available namespace `hello'/
+ end
+end
View
70 spec/thor_spec.rb
@@ -1,27 +1,6 @@
require File.dirname(__FILE__) + '/spec_helper'
require "thor"
-class StdOutCapturer
- attr_reader :output
-
- def initialize
- @output = ""
- end
-
- def self.call_func
- old_out = $stdout
- output = new
- $stdout = output
- yield
- $stdout = old_out
- output.output
- end
-
- def write(s)
- @output += s
- end
-end
-
class MyApp < Thor
map "-T" => :animal
@@ -53,76 +32,95 @@ def bar(baz, bat, opts)
def baz(bat, opts)
[bat, opts]
end
+
+ def method_missing(meth, *args)
+ [meth, args]
+ end
+
+ private
+ desc "what", "what"
+ def what
+ end
end
describe "thor" do
it "calls a no-param method when no params are passed" do
ARGV.replace ["zoo"]
- MyApp.start.should == true
+ MyApp.start.must == true
end
it "calls a single-param method when a single param is passed" do
ARGV.replace ["animal", "fish"]
- MyApp.start.should == ["fish"]
+ MyApp.start.must == ["fish"]
end
it "calls the alias of a method if one is provided via .map" do
ARGV.replace ["-T", "fish"]
- MyApp.start.should == ["fish"]
+ MyApp.start.must == ["fish"]
end
it "raises an error if a required param is not provided" do
ARGV.replace ["animal"]
- lambda { MyApp.start }.should raise_error(ArgumentError)
+ lambda { MyApp.start }.must raise_error(ArgumentError)
end
it "calls a method with an optional boolean param when the param is passed" do
ARGV.replace ["foo", "one", "--force"]
- MyApp.start.should == ["one", {"force" => true, "f" => true}]
+ MyApp.start.must == ["one", {"force" => true, "f" => true}]
end
it "calls a method with an optional boolean param when the param is not passed" do
ARGV.replace ["foo", "one"]
- MyApp.start.should == ["one", {}]
+ MyApp.start.must == ["one", {}]
end
it "calls a method with a required key/value param" do
ARGV.replace ["bar", "one", "two", "--option1", "hello"]
- MyApp.start.should == ["one", "two", {"option1" => "hello", "o" => "hello"}]
+ MyApp.start.must == ["one", "two", {"option1" => "hello", "o" => "hello"}]
end
it "errors out when a required key/value option is not passed" do
ARGV.replace ["bar", "one", "two"]
- lambda { MyApp.start }.should raise_error(Getopt::Long::Error)
+ lambda { MyApp.start }.must raise_error(Getopt::Long::Error)
end
it "calls a method with an optional key/value param" do
ARGV.replace ["baz", "one", "--option1", "hello"]
- MyApp.start.should == ["one", {"option1" => "hello", "o" => "hello"}]
+ MyApp.start.must == ["one", {"option1" => "hello", "o" => "hello"}]
end
it "calls a method with an empty Hash for options if an optional key/value param is not provided" do
ARGV.replace ["baz", "one"]
- MyApp.start.should == ["one", {}]
+ MyApp.start.must == ["one", {}]
+ end
+
+ it "calls method_missing if an unknown method is passed in" do
+ ARGV.replace ["unk", "hello"]
+ MyApp.start.must == [:unk, ["hello"]]
+ end
+
+ it "does not call a private method no matter what" do
+ ARGV.replace ["what"]
+ MyApp.start.must == nil
end
it "provides useful help info for a simple method" do
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /zoo +zoo around/
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /zoo +zoo around/
end
it "provides useful help info for a method with one param" do
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /animal TYPE +horse around/
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /animal TYPE +horse around/
end
it "provides useful help info for a method with boolean options" do
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /foo BAR \-\-force +do some fooing/
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /foo BAR \[\-\-force\] +do some fooing/
end
it "provides useful help info for a method with required options" do
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /bar BAZ BAT \-\-option1=OPTION1 +do some barring/
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /bar BAZ BAT \-\-option1=OPTION1 +do some barring/
end
it "provides useful help info for a method with optional options" do
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /baz BAT \[\-\-option1=OPTION1\] +do some bazzing/
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /baz BAT \[\-\-option1=OPTION1\] +do some bazzing/
end
end
View
0  task.thor
No changes.
Please sign in to comment.
Something went wrong with that request. Please try again.