public
Fork of wycats/thor
Description: A scripting framework that replaces rake and sake
Homepage: http://www.yehudakatz.com
Clone URL: git://github.com/indirect/thor.git
First pass of Thor::Runner
wycats (author)
Wed May 07 19:28:50 -0700 2008
commit  b522f05ef166d142e80e30d264ae0dfba09ac4f8
tree    544c3e15034bccf357738955403bb4abed570e99
parent  82ff27a67d7fa1078c231bb52b71cbd9b3e0fb37
...
28
29
30
31
 
 
 
32
33
34
...
43
44
45
 
46
47
48
...
28
29
30
 
31
32
33
34
35
36
...
45
46
47
48
49
50
51
0
@@ -28,7 +28,9 @@ spec = Gem::Specification.new do |s|
0
   
0
   s.require_path = 'lib'
0
   s.autorequire = GEM
0
- s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,specs}/**/*")
0
+ s.bindir = "bin"
0
+ s.executables = %w( thor )
0
+ s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{bin,lib,specs}/**/*")
0
 end
0
 
0
 Rake::GemPackageTask.new(spec) do |pkg|
0
@@ -43,6 +45,7 @@ Spec::Rake::SpecTask.new do |t|
0
   t.spec_opts << "-fs --color"
0
 end
0
 
0
+task :specs => :spec
0
 
0
 desc "install the gem locally"
0
 task :install => [:package] do
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
0
@@ -0,0 +1,131 @@
0
+require "thor"
0
+
0
+class Thor::Util
0
+ # @public
0
+ def self.constant_to_thor_path(str)
0
+ snake_case(str).squeeze(":")
0
+ end
0
+
0
+ # @public
0
+ def self.constant_from_thor_path(str)
0
+ make_constant(to_constant(str))
0
+ end
0
+
0
+ private
0
+ # @private
0
+ def self.to_constant(str)
0
+ str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
0
+ end
0
+
0
+ # @private
0
+ def self.make_constant(str)
0
+ list = str.split("::")
0
+ obj = Object
0
+ list.each {|x| obj = obj.const_get(x) }
0
+ obj
0
+ end
0
+
0
+ # @private
0
+ def self.snake_case(str)
0
+ return str.downcase if str =~ /^[A-Z]+$/
0
+ str.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
0
+ return $+.downcase
0
+ end
0
+
0
+end
0
+
0
+class Thor::Runner < Thor
0
+
0
+ def self.globs_for(path)
0
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
0
+ end
0
+
0
+ map "-T" => :list
0
+
0
+ desc "list [SEARCH]", "list the available thor tasks"
0
+ method_options :force => :boolean
0
+ def list(search = "")
0
+ search = /.*#{search}.*/
0
+ thorfiles.each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
0
+
0
+ # Calculate the largest base class name
0
+ max_base = Thor.subclasses.max do |x,y|
0
+ Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
0
+ end.name.size
0
+
0
+ # Calculate the size of the largest option description
0
+ max_left_item = Thor.subclasses.max do |x,y|
0
+ (x.help_list && x.help_list.max.usage + x.help_list.max.opt).to_i <=>
0
+ (y.help_list && y.help_list.max.usage + y.help_list.max.opt).to_i
0
+ end
0
+
0
+ max_left = max_left_item.help_list.max.usage + max_left_item.help_list.max.opt
0
+
0
+ Thor.subclasses.map {|k| k.help_list}.compact.each do |item|
0
+ display_tasks(item, max_base, max_left)
0
+ end
0
+ end
0
+
0
+ def method_missing(meth, *args)
0
+ meth = meth.to_s
0
+ unless meth =~ /:/
0
+ puts "Thor tasks must contain a :"
0
+ return
0
+ end
0
+
0
+ klass = meth.split(":")[0...-1].join(":")
0
+ to_call = meth.split(":").last
0
+ begin
0
+ klass = Thor::Util.constant_from_thor_path(klass)
0
+ rescue NameError
0
+ puts "There was no available namespace `#{klass}'."
0
+ return
0
+ end
0
+
0
+ ARGV.replace [to_call, ARGV[1..-1]].compact
0
+ klass.start
0
+ end
0
+
0
+ private
0
+ def display_tasks(item, max_base, max_left)
0
+ base = Thor::Util.constant_to_thor_path(item.klass.name)
0
+ item.usages.each do |name, usage|
0
+ format_string = "%-#{max_left + max_base + 5}s"
0
+ print format_string %
0
+ "#{base}:#{item.usages.assoc(name).last} #{display_opts(item.opts.assoc(name) && item.opts.assoc(name).last)}"
0
+ puts item.descriptions.assoc(name).last
0
+ end
0
+ end
0
+
0
+ def display_opts(opts)
0
+ return "" unless opts
0
+ opts.map do |opt, val|
0
+ if val == true || val == "BOOLEAN"
0
+ "[#{opt}]"
0
+ elsif val == "REQUIRED"
0
+ opt + "=" + opt.gsub(/\-/, "").upcase
0
+ elsif val == "OPTIONAL"
0
+ "[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
0
+ end
0
+ end.join(" ")
0
+ end
0
+
0
+ def thorfiles
0
+ path = Dir.pwd
0
+ system_thorfiles = Dir["#{ENV["HOME"]}/.thor/**/*.thor"]
0
+ thorfiles = []
0
+
0
+ # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
0
+ while thorfiles.empty?
0
+ thorfiles = Dir[*Thor::Runner.globs_for(path)]
0
+ path = File.dirname(path)
0
+ break if path == "/"
0
+ end
0
+ thorfiles
0
+ end
0
+
0
+end
0
+
0
+unless defined?(Spec)
0
+ Thor::Runner.start
0
+end
0
\ No newline at end of file
...
1
2
3
 
 
 
 
 
 
 
 
 
 
 
 
 
4
5
6
...
26
27
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
30
31
...
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
 
79
80
 
81
82
83
84
85
86
87
88
89
 
 
 
 
90
91
 
 
92
93
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
...
80
81
82
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
85
 
86
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
89
90
91
92
93
 
 
 
 
 
 
 
94
95
96
97
98
 
99
100
101
102
0
@@ -1,6 +1,19 @@
0
 require "#{File.dirname(__FILE__)}/getopt"
0
 
0
 class Thor
0
+ def self.inherited(klass)
0
+ subclass_files[File.expand_path(caller[0].split(":")[0])] << klass
0
+ subclasses << klass
0
+ end
0
+
0
+ def self.subclass_files
0
+ @subclass_files ||= Hash.new {|h,k| h[k] = []}
0
+ end
0
+
0
+ def self.subclasses
0
+ @subclasses ||= []
0
+ end
0
+
0
   def self.method_added(meth)
0
     return if !public_instance_methods.include?(meth.to_s) || !@usage
0
     @descriptions ||= []
0
@@ -26,6 +39,31 @@ class Thor
0
     end
0
   end
0
 
0
+ def self.help_list
0
+ return nil unless @usages
0
+ @help_list ||= begin
0
+ max_usage = @usages.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
0
+ max_opts = @opts.empty? ? 0 : format_opts(@opts.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last).size
0
+ max_desc = @descriptions.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
0
+ Struct.new(:klass, :usages, :opts, :descriptions, :max).new(
0
+ self, @usages, @opts, @descriptions, Struct.new(:usage, :opt, :desc).new(max_usage, max_opts, max_desc)
0
+ )
0
+ end
0
+ end
0
+
0
+ def self.format_opts(opts)
0
+ return "" unless opts
0
+ opts.map do |opt, val|
0
+ if val == true || val == "BOOLEAN"
0
+ "[#{opt}]"
0
+ elsif val == "REQUIRED"
0
+ opt + "=" + opt.gsub(/\-/, "").upcase
0
+ elsif val == "OPTIONAL"
0
+ "[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
0
+ end
0
+ end.join(" ")
0
+ end
0
+
0
   def self.start
0
     meth = ARGV.shift
0
     params = []
0
@@ -42,51 +80,22 @@ class Thor
0
       params << options
0
     end
0
     new(meth, params).instance_variable_get("@results")
0
- end
0
-
0
- def thor_usages
0
- self.class.instance_variable_get("@usages")
0
- end
0
-
0
- def thor_descriptions
0
- self.class.instance_variable_get("@descriptions")
0
- end
0
-
0
- def thor_opts
0
- self.class.instance_variable_get("@opts")
0
   end
0
-
0
 
0
   def initialize(op, params)
0
- @results = send(op.to_sym, *params) if public_methods.include?(op)
0
+ @results = send(op.to_sym, *params) if public_methods.include?(op) || !methods.include?(op)
0
   end
0
-
0
- private
0
- def format_opts(opts)
0
- return "" unless opts
0
- opts.map do |opt, val|
0
- if val == true || val == "BOOLEAN"
0
- opt
0
- elsif val == "REQUIRED"
0
- opt + "=" + opt.gsub(/\-/, "").upcase
0
- elsif val == "OPTIONAL"
0
- "[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
0
- end
0
- end.join(" ")
0
- end
0
-
0
- public
0
+
0
   desc "help", "show this screen"
0
   def help
0
+ list = self.class.help_list
0
     puts "Options"
0
     puts "-------"
0
- max_usage = thor_usages.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
0
- 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
0
- max_desc = thor_descriptions.max {|x,y| x.last.to_s.size <=> y.last.to_s.size}.last.size
0
- thor_usages.each do |meth, usage|
0
- format = "%-" + (max_usage + max_opts + 4).to_s + "s"
0
- print format % (thor_usages.assoc(meth)[1] + (thor_opts.assoc(meth) ? " " + format_opts(thor_opts.assoc(meth)[1]) : ""))
0
- puts thor_descriptions.assoc(meth)[1]
0
+ list.usages.each do |meth, usage|
0
+ format = "%-" + (list.max.usage + list.max.opt + 4).to_s + "s"
0
+ print format % (list.usages.assoc(meth)[1] + (list.opts.assoc(meth) ? " " + self.class.format_opts(list.opts.assoc(meth)[1]) : ""))
0
+ puts list.descriptions.assoc(meth)[1]
0
     end
0
- end
0
+ end
0
+
0
 end
0
\ No newline at end of file
...
1
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
0
@@ -1,2 +1,31 @@
0
 $TESTING=true
0
 $:.push File.join(File.dirname(__FILE__), '..', 'lib')
0
+
0
+module Spec::Expectations::ObjectExpectations
0
+ alias_method :must, :should
0
+ alias_method :must_not, :should_not
0
+end
0
+
0
+class StdOutCapturer
0
+ attr_reader :output
0
+
0
+ def initialize
0
+ @output = ""
0
+ end
0
+
0
+ def self.call_func
0
+ begin
0
+ old_out = $stdout
0
+ output = new
0
+ $stdout = output
0
+ yield
0
+ ensure
0
+ $stdout = old_out
0
+ end
0
+ output.output
0
+ end
0
+
0
+ def write(s)
0
+ @output += s
0
+ end
0
+end
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
53
54
55
 
 
 
 
 
 
 
 
 
56
57
58
59
60
61
 
62
63
64
65
66
 
67
68
69
70
71
 
72
73
74
75
76
 
77
78
79
80
81
 
82
83
84
85
86
 
87
88
89
90
91
 
92
93
94
95
96
 
97
98
99
100
101
 
102
103
104
105
106
 
 
 
 
 
 
 
 
 
 
 
107
108
109
110
 
111
112
113
114
 
115
116
117
118
 
119
120
121
122
 
123
124
125
126
 
127
128
129
...
1
2
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
5
6
...
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 
49
50
51
52
53
 
54
55
56
57
58
 
59
60
61
62
63
 
64
65
66
67
68
 
69
70
71
72
73
 
74
75
76
77
78
 
79
80
81
82
83
 
84
85
86
87
88
 
89
90
91
92
93
 
94
95
96
97
98
99
100
101
102
103
104
105
106
107
 
108
109
110
111
 
112
113
114
115
 
116
117
118
119
 
120
121
122
123
 
124
125
126
127
0
@@ -1,27 +1,6 @@
0
 require File.dirname(__FILE__) + '/spec_helper'
0
 require "thor"
0
 
0
-class StdOutCapturer
0
- attr_reader :output
0
-
0
- def initialize
0
- @output = ""
0
- end
0
-
0
- def self.call_func
0
- old_out = $stdout
0
- output = new
0
- $stdout = output
0
- yield
0
- $stdout = old_out
0
- output.output
0
- end
0
-
0
- def write(s)
0
- @output += s
0
- end
0
-end
0
-
0
 class MyApp < Thor
0
   
0
   map "-T" => :animal
0
@@ -53,76 +32,95 @@ class MyApp < Thor
0
   def baz(bat, opts)
0
     [bat, opts]
0
   end
0
+
0
+ def method_missing(meth, *args)
0
+ [meth, args]
0
+ end
0
+
0
+ private
0
+ desc "what", "what"
0
+ def what
0
+ end
0
 end
0
 
0
 describe "thor" do
0
   it "calls a no-param method when no params are passed" do
0
     ARGV.replace ["zoo"]
0
- MyApp.start.should == true
0
+ MyApp.start.must == true
0
   end
0
   
0
   it "calls a single-param method when a single param is passed" do
0
     ARGV.replace ["animal", "fish"]
0
- MyApp.start.should == ["fish"]
0
+ MyApp.start.must == ["fish"]
0
   end
0
   
0
   it "calls the alias of a method if one is provided via .map" do
0
     ARGV.replace ["-T", "fish"]
0
- MyApp.start.should == ["fish"]
0
+ MyApp.start.must == ["fish"]
0
   end
0
   
0
   it "raises an error if a required param is not provided" do
0
     ARGV.replace ["animal"]
0
- lambda { MyApp.start }.should raise_error(ArgumentError)
0
+ lambda { MyApp.start }.must raise_error(ArgumentError)
0
   end
0
   
0
   it "calls a method with an optional boolean param when the param is passed" do
0
     ARGV.replace ["foo", "one", "--force"]
0
- MyApp.start.should == ["one", {"force" => true, "f" => true}]
0
+ MyApp.start.must == ["one", {"force" => true, "f" => true}]
0
   end
0
   
0
   it "calls a method with an optional boolean param when the param is not passed" do
0
     ARGV.replace ["foo", "one"]
0
- MyApp.start.should == ["one", {}]
0
+ MyApp.start.must == ["one", {}]
0
   end
0
   
0
   it "calls a method with a required key/value param" do
0
     ARGV.replace ["bar", "one", "two", "--option1", "hello"]
0
- MyApp.start.should == ["one", "two", {"option1" => "hello", "o" => "hello"}]
0
+ MyApp.start.must == ["one", "two", {"option1" => "hello", "o" => "hello"}]
0
   end
0
   
0
   it "errors out when a required key/value option is not passed" do
0
     ARGV.replace ["bar", "one", "two"]
0
- lambda { MyApp.start }.should raise_error(Getopt::Long::Error)
0
+ lambda { MyApp.start }.must raise_error(Getopt::Long::Error)
0
   end
0
   
0
   it "calls a method with an optional key/value param" do
0
     ARGV.replace ["baz", "one", "--option1", "hello"]
0
- MyApp.start.should == ["one", {"option1" => "hello", "o" => "hello"}]
0
+ MyApp.start.must == ["one", {"option1" => "hello", "o" => "hello"}]
0
   end
0
   
0
   it "calls a method with an empty Hash for options if an optional key/value param is not provided" do
0
     ARGV.replace ["baz", "one"]
0
- MyApp.start.should == ["one", {}]
0
+ MyApp.start.must == ["one", {}]
0
+ end
0
+
0
+ it "calls method_missing if an unknown method is passed in" do
0
+ ARGV.replace ["unk", "hello"]
0
+ MyApp.start.must == [:unk, ["hello"]]
0
+ end
0
+
0
+ it "does not call a private method no matter what" do
0
+ ARGV.replace ["what"]
0
+ MyApp.start.must == nil
0
   end
0
   
0
   it "provides useful help info for a simple method" do
0
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /zoo +zoo around/
0
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /zoo +zoo around/
0
   end
0
   
0
   it "provides useful help info for a method with one param" do
0
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /animal TYPE +horse around/
0
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /animal TYPE +horse around/
0
   end
0
   
0
   it "provides useful help info for a method with boolean options" do
0
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /foo BAR \-\-force +do some fooing/
0
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /foo BAR \[\-\-force\] +do some fooing/
0
   end
0
   
0
   it "provides useful help info for a method with required options" do
0
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /bar BAZ BAT \-\-option1=OPTION1 +do some barring/
0
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /bar BAZ BAT \-\-option1=OPTION1 +do some barring/
0
   end
0
   
0
   it "provides useful help info for a method with optional options" do
0
- StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.should =~ /baz BAT \[\-\-option1=OPTION1\] +do some bazzing/
0
+ StdOutCapturer.call_func { ARGV.replace ["help"]; MyApp.start }.must =~ /baz BAT \[\-\-option1=OPTION1\] +do some bazzing/
0
   end
0
 end
0
\ No newline at end of file

Comments

    No one has commented yet.