botanicus-thor 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +52 -0
- data/LICENSE +20 -0
- data/README.markdown +76 -0
- data/Rakefile +6 -0
- data/bin/rake2thor +88 -0
- data/bin/thor +7 -0
- data/lib/thor/error.rb +5 -0
- data/lib/thor/options.rb +269 -0
- data/lib/thor/runner.rb +344 -0
- data/lib/thor/task.rb +85 -0
- data/lib/thor/task_hash.rb +23 -0
- data/lib/thor/tasks/package.rb +20 -0
- data/lib/thor/tasks.rb +88 -0
- data/lib/thor/util.rb +77 -0
- data/lib/thor.rb +177 -0
- metadata +72 -0
data/lib/thor/runner.rb
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "thor/util"
|
5
|
+
require "open-uri"
|
6
|
+
require "fileutils"
|
7
|
+
require "yaml"
|
8
|
+
require "digest/md5"
|
9
|
+
require "readline"
|
10
|
+
require "pathname"
|
11
|
+
|
12
|
+
class Thor::Runner < Thor
|
13
|
+
def self.globs_for(path)
|
14
|
+
["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
|
15
|
+
end
|
16
|
+
|
17
|
+
map "-T" => :list, "-i" => :install, "-u" => :update
|
18
|
+
|
19
|
+
desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
|
20
|
+
method_options :as => :optional, :relative => :boolean
|
21
|
+
def install(name)
|
22
|
+
initialize_thorfiles
|
23
|
+
|
24
|
+
# If a directory name is provided as the argument, look for a 'main.thor'
|
25
|
+
# task in said directory.
|
26
|
+
begin
|
27
|
+
if File.directory?(File.expand_path(name))
|
28
|
+
base, package = File.join(name, "main.thor"), :directory
|
29
|
+
contents = open(base).read
|
30
|
+
else
|
31
|
+
base, package = name, :file
|
32
|
+
contents = open(name).read
|
33
|
+
end
|
34
|
+
rescue OpenURI::HTTPError
|
35
|
+
raise Error, "Error opening URI `#{name}'"
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
raise Error, "Error opening file `#{name}'"
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "Your Thorfile contains: "
|
41
|
+
puts contents
|
42
|
+
print "Do you wish to continue [y/N]? "
|
43
|
+
response = Readline.readline
|
44
|
+
|
45
|
+
return false unless response =~ /^\s*y/i
|
46
|
+
|
47
|
+
as = options["as"] || begin
|
48
|
+
first_line = contents.split("\n")[0]
|
49
|
+
(match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
unless as
|
53
|
+
print "Please specify a name for #{name} in the system repository [#{name}]: "
|
54
|
+
as = Readline.readline
|
55
|
+
as = name if as.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
FileUtils.mkdir_p(thor_root)
|
59
|
+
FileUtils.touch(File.join(thor_root, "thor.yml"))
|
60
|
+
|
61
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
62
|
+
FileUtils.touch(yaml_file)
|
63
|
+
|
64
|
+
thor_yaml[as] = {
|
65
|
+
:filename => Digest::MD5.hexdigest(name + as),
|
66
|
+
:location => (options[:relative] || File.exists?(name)) ? name : File.expand_path(name),
|
67
|
+
:constants => Thor::Util.constants_in_contents(contents, base)
|
68
|
+
}
|
69
|
+
|
70
|
+
save_yaml(thor_yaml)
|
71
|
+
|
72
|
+
puts "Storing thor file in your system repository"
|
73
|
+
|
74
|
+
destination = File.join(thor_root, thor_yaml[as][:filename])
|
75
|
+
|
76
|
+
if package == :file
|
77
|
+
File.open(destination, "w") { |f| f.puts contents }
|
78
|
+
else
|
79
|
+
FileUtils.cp_r(name, destination)
|
80
|
+
end
|
81
|
+
|
82
|
+
thor_yaml[as][:filename] # Indicate sucess
|
83
|
+
end
|
84
|
+
|
85
|
+
desc "uninstall NAME", "uninstall a named Thor module"
|
86
|
+
def uninstall(name)
|
87
|
+
raise Error, "Can't find module `#{name}'" unless thor_yaml[name]
|
88
|
+
|
89
|
+
puts "Uninstalling #{name}."
|
90
|
+
|
91
|
+
FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}"))
|
92
|
+
|
93
|
+
thor_yaml.delete(name)
|
94
|
+
save_yaml(thor_yaml)
|
95
|
+
|
96
|
+
puts "Done."
|
97
|
+
end
|
98
|
+
|
99
|
+
desc "update NAME", "update a Thor file from its original location"
|
100
|
+
def update(name)
|
101
|
+
raise Error, "Can't find module `#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
|
102
|
+
|
103
|
+
puts "Updating `#{name}' from #{thor_yaml[name][:location]}"
|
104
|
+
old_filename = thor_yaml[name][:filename]
|
105
|
+
self.options = self.options.merge("as" => name)
|
106
|
+
filename = install(thor_yaml[name][:location])
|
107
|
+
unless filename == old_filename
|
108
|
+
File.delete(File.join(thor_root, old_filename))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
|
113
|
+
method_options :internal => :boolean
|
114
|
+
def installed
|
115
|
+
thor_root_glob.each do |f|
|
116
|
+
next if f =~ /thor\.yml$/
|
117
|
+
load_thorfile(f) unless Thor.subclass_files.keys.include?(File.expand_path(f))
|
118
|
+
end
|
119
|
+
|
120
|
+
klasses = Thor.subclasses
|
121
|
+
klasses -= [Thor, Thor::Runner] unless options["internal"]
|
122
|
+
display_klasses(true, klasses)
|
123
|
+
end
|
124
|
+
|
125
|
+
desc "list [SEARCH]",
|
126
|
+
"list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
|
127
|
+
method_options :substring => :boolean,
|
128
|
+
:group => :optional,
|
129
|
+
:all => :boolean
|
130
|
+
def list(search="")
|
131
|
+
initialize_thorfiles
|
132
|
+
search = ".*#{search}" if options["substring"]
|
133
|
+
search = /^#{search}.*/i
|
134
|
+
group = options[:group] || "standard"
|
135
|
+
|
136
|
+
classes = Thor.subclasses.select do |k|
|
137
|
+
(options[:all] || k.group_name == group) &&
|
138
|
+
Thor::Util.constant_to_thor_path(k.name) =~ search
|
139
|
+
end
|
140
|
+
display_klasses(false, classes)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Override Thor#help so we can give info about not-yet-loaded tasks
|
144
|
+
def help(task = nil)
|
145
|
+
initialize_thorfiles(task) if task && task.include?(?:)
|
146
|
+
super
|
147
|
+
end
|
148
|
+
|
149
|
+
def method_missing(meth, *args)
|
150
|
+
meth = meth.to_s
|
151
|
+
super(meth.to_sym, *args) unless meth.include?(?:)
|
152
|
+
|
153
|
+
initialize_thorfiles(meth)
|
154
|
+
task = Thor[meth]
|
155
|
+
task.parse(task.klass.new, ARGV[1..-1])
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.thor_root
|
159
|
+
return File.join(ENV["HOME"], '.thor') if ENV["HOME"]
|
160
|
+
|
161
|
+
if ENV["HOMEDRIVE"] && ENV["HOMEPATH"] then
|
162
|
+
return File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"], '.thor')
|
163
|
+
end
|
164
|
+
|
165
|
+
return File.join(ENV["APPDATA"], '.thor') if ENV["APPDATA"]
|
166
|
+
|
167
|
+
begin
|
168
|
+
File.expand_path("~")
|
169
|
+
rescue
|
170
|
+
if File::ALT_SEPARATOR then
|
171
|
+
"C:/"
|
172
|
+
else
|
173
|
+
"/"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.thor_root_glob
|
179
|
+
# On Windows thor_root will be something like this:
|
180
|
+
#
|
181
|
+
# C:\Documents and Settings\james\.thor
|
182
|
+
#
|
183
|
+
# If we don't #gsub the \ character, Dir.glob will fail.
|
184
|
+
files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
|
185
|
+
files.map! do |file|
|
186
|
+
File.directory?(file) ? File.join(file, "main.thor") : file
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def thor_root
|
193
|
+
self.class.thor_root
|
194
|
+
end
|
195
|
+
|
196
|
+
def thor_root_glob
|
197
|
+
self.class.thor_root_glob
|
198
|
+
end
|
199
|
+
|
200
|
+
def thor_yaml
|
201
|
+
@y ||= begin
|
202
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
203
|
+
yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
|
204
|
+
yaml || {}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def save_yaml(yaml)
|
209
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
210
|
+
File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml }
|
211
|
+
end
|
212
|
+
|
213
|
+
def display_klasses(with_modules = false, klasses = Thor.subclasses)
|
214
|
+
klasses -= [Thor, Thor::Runner] unless with_modules
|
215
|
+
raise Error, "No Thor tasks available" if klasses.empty?
|
216
|
+
|
217
|
+
if with_modules && !thor_yaml.empty?
|
218
|
+
max_name = thor_yaml.max { |(xk, xv), (yk, yv)| xk.to_s.size <=> yk.to_s.size }.first.size
|
219
|
+
modules_label = "Modules"
|
220
|
+
namespaces_label = "Namespaces"
|
221
|
+
column_width = [max_name + 4, modules_label.size + 1].max
|
222
|
+
|
223
|
+
print "%-#{column_width}s" % modules_label
|
224
|
+
puts namespaces_label
|
225
|
+
print "%-#{column_width}s" % ("-" * modules_label.size)
|
226
|
+
puts "-" * namespaces_label.size
|
227
|
+
|
228
|
+
thor_yaml.each do |name, info|
|
229
|
+
print "%-#{column_width}s" % name
|
230
|
+
puts info[:constants].map { |c| Thor::Util.constant_to_thor_path(c) }.join(", ")
|
231
|
+
end
|
232
|
+
|
233
|
+
puts
|
234
|
+
end
|
235
|
+
|
236
|
+
# Calculate the largest base class name
|
237
|
+
max_base = klasses.max do |x,y|
|
238
|
+
Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
|
239
|
+
end.name.size
|
240
|
+
|
241
|
+
# Calculate the size of the largest option description
|
242
|
+
max_left_item = klasses.max do |x,y|
|
243
|
+
(x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
|
244
|
+
end
|
245
|
+
|
246
|
+
max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
|
247
|
+
|
248
|
+
unless klasses.empty?
|
249
|
+
puts # add some spacing
|
250
|
+
klasses.each { |k| display_tasks(k, max_base, max_left); }
|
251
|
+
else
|
252
|
+
puts "\033[1;34mNo Thor tasks available\033[0m"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def display_tasks(klass, max_base, max_left)
|
257
|
+
if klass.tasks.values.length > 1
|
258
|
+
|
259
|
+
base = Thor::Util.constant_to_thor_path(klass.name)
|
260
|
+
|
261
|
+
if base.empty?
|
262
|
+
base = 'default'
|
263
|
+
puts "\033[1;35m#{base}\033[0m"
|
264
|
+
else
|
265
|
+
puts "\033[1;34m#{base}\033[0m"
|
266
|
+
end
|
267
|
+
|
268
|
+
puts "-" * base.length
|
269
|
+
|
270
|
+
klass.tasks.each true do |name, task|
|
271
|
+
format_string = "%-#{max_left + max_base + 5}s"
|
272
|
+
print format_string % task.formatted_usage(true)
|
273
|
+
puts task.description
|
274
|
+
end
|
275
|
+
|
276
|
+
unless klass.opts.empty?
|
277
|
+
puts "\nglobal options: #{Options.new(klass.opts)}"
|
278
|
+
end
|
279
|
+
|
280
|
+
puts # add some spacing
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def initialize_thorfiles(relevant_to = nil)
|
285
|
+
thorfiles(relevant_to).each do |f|
|
286
|
+
load_thorfile(f) unless Thor.subclass_files.keys.include?(File.expand_path(f))
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def load_thorfile(path)
|
291
|
+
txt = File.read(path)
|
292
|
+
begin
|
293
|
+
Thor::Tasks.class_eval(txt, path)
|
294
|
+
rescue Object => e
|
295
|
+
$stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Finds Thorfiles by traversing from your current directory down to the root
|
300
|
+
# directory of your system. If at any time we find a Thor file, we stop.
|
301
|
+
#
|
302
|
+
# ==== Example
|
303
|
+
# If we start at /Users/wycats/dev/thor ...
|
304
|
+
#
|
305
|
+
# 1. /Users/wycats/dev/thor
|
306
|
+
# 2. /Users/wycats/dev
|
307
|
+
# 3. /Users/wycats <-- we find a Thorfile here, so we stop
|
308
|
+
#
|
309
|
+
# Suppose we start at c:\Documents and Settings\james\dev\thor ...
|
310
|
+
#
|
311
|
+
# 1. c:\Documents and Settings\james\dev\thor
|
312
|
+
# 2. c:\Documents and Settings\james\dev
|
313
|
+
# 3. c:\Documents and Settings\james
|
314
|
+
# 4. c:\Documents and Settings
|
315
|
+
# 5. c:\ <-- no Thorfiles found!
|
316
|
+
def thorfiles(relevant_to=nil)
|
317
|
+
thorfiles = []
|
318
|
+
|
319
|
+
# This may seem a little odd at first. Suppose you're working on a Rails
|
320
|
+
# project and you traverse into the "app" directory. Because of the below
|
321
|
+
# you can execute "thor -T" and see any tasks you might have in the root
|
322
|
+
# directory of your Rails project.
|
323
|
+
Pathname.pwd.ascend do |path|
|
324
|
+
thorfiles = Thor::Runner.globs_for(path).map { |g| Dir[g] }.flatten
|
325
|
+
break unless thorfiles.empty?
|
326
|
+
end
|
327
|
+
|
328
|
+
# We want to load system-wide Thorfiles first so the local Thorfiles will
|
329
|
+
# override them.
|
330
|
+
files = (relevant_to ? thorfiles_relevant_to(relevant_to) : thor_root_glob)
|
331
|
+
files += thorfiles - ["#{thor_root}/thor.yml"]
|
332
|
+
|
333
|
+
files.map! do |file|
|
334
|
+
File.directory?(file) ? File.join(file, "main.thor") : file
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def thorfiles_relevant_to(meth)
|
339
|
+
klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
|
340
|
+
thor_yaml.select do |k, v|
|
341
|
+
v[:constants] && v[:constants].include?(klass_str)
|
342
|
+
end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
|
343
|
+
end
|
344
|
+
end
|
data/lib/thor/task.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'thor/error'
|
4
|
+
require 'thor/util'
|
5
|
+
|
6
|
+
class Thor
|
7
|
+
class Task < Struct.new(:meth, :description, :usage, :opts, :klass)
|
8
|
+
|
9
|
+
def self.dynamic(meth, klass)
|
10
|
+
new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
# keep the original opts - we need them later on
|
15
|
+
@options = args[3] || {}
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(obj, args)
|
20
|
+
list, hash = parse_args(args)
|
21
|
+
obj.options = hash
|
22
|
+
run(obj, *list)
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(obj, *params)
|
26
|
+
raise NoMethodError, "the `#{meth}' task of #{obj.class} is private" if
|
27
|
+
(obj.private_methods + obj.protected_methods).include?(meth)
|
28
|
+
|
29
|
+
obj.invoke(meth, *params)
|
30
|
+
rescue ArgumentError => e
|
31
|
+
|
32
|
+
# backtrace sans anything in this file
|
33
|
+
backtrace = e.backtrace.reject {|frame| frame =~ /^#{Regexp.escape(__FILE__)}/}
|
34
|
+
# also nix anything in thor.rb
|
35
|
+
backtrace = backtrace.reject { |frame| frame =~ /\/thor.rb/ }
|
36
|
+
|
37
|
+
# and sans anything that got us here
|
38
|
+
backtrace -= caller
|
39
|
+
raise e unless backtrace.empty?
|
40
|
+
|
41
|
+
# okay, they really did call it wrong
|
42
|
+
raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
|
43
|
+
rescue NoMethodError => e
|
44
|
+
begin
|
45
|
+
raise e unless e.message =~ /^undefined method `#{meth}' for #{Regexp.escape(obj.inspect)}$/
|
46
|
+
rescue
|
47
|
+
raise e
|
48
|
+
end
|
49
|
+
raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
|
50
|
+
end
|
51
|
+
|
52
|
+
def namespace(remove_default = true)
|
53
|
+
Thor::Util.constant_to_thor_path(klass, remove_default)
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_klass(klass)
|
57
|
+
new = self.dup
|
58
|
+
new.klass = klass
|
59
|
+
new
|
60
|
+
end
|
61
|
+
|
62
|
+
def opts
|
63
|
+
return super unless super.kind_of? Hash
|
64
|
+
@_opts ||= Options.new(super)
|
65
|
+
end
|
66
|
+
|
67
|
+
def full_opts
|
68
|
+
@_full_opts ||= Options.new((klass.opts || {}).merge(@options))
|
69
|
+
end
|
70
|
+
|
71
|
+
def formatted_usage(namespace = false)
|
72
|
+
(namespace ? self.namespace + ':' : '') + usage +
|
73
|
+
(opts ? " " + opts.formatted_usage : "")
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def parse_args(args)
|
79
|
+
return [[], {}] if args.nil?
|
80
|
+
hash = full_opts.parse(args)
|
81
|
+
list = full_opts.non_opts
|
82
|
+
[list, hash]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'thor/task'
|
4
|
+
|
5
|
+
class Thor::TaskHash < Hash
|
6
|
+
def initialize(klass)
|
7
|
+
super()
|
8
|
+
@klass = klass
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(local = false, &block)
|
12
|
+
super() { |k, t| yield k, t.with_klass(@klass) }
|
13
|
+
@klass.superclass.tasks.each { |k, t| yield k, t.with_klass(@klass) } unless local || @klass == Thor
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](name)
|
17
|
+
if task = super(name) || (@klass == Thor && @klass.superclass.tasks[name])
|
18
|
+
return task.with_klass(@klass)
|
19
|
+
end
|
20
|
+
|
21
|
+
Thor::Task.dynamic(name, @klass)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "thor/task"
|
4
|
+
|
5
|
+
class Thor::PackageTask < Thor::Task
|
6
|
+
attr_accessor :spec
|
7
|
+
attr_accessor :opts
|
8
|
+
|
9
|
+
def initialize(gemspec, opts = {})
|
10
|
+
super(:package, "build a gem package")
|
11
|
+
@spec = gemspec
|
12
|
+
@opts = {:dir => File.join(Dir.pwd, "pkg")}.merge(opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
FileUtils.mkdir_p(@opts[:dir])
|
17
|
+
Gem::Builder.new(spec).build
|
18
|
+
FileUtils.mv(spec.file_name, File.join(@opts[:dir], spec.file_name))
|
19
|
+
end
|
20
|
+
end
|
data/lib/thor/tasks.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
class Thor
|
7
|
+
def self.package_task(spec)
|
8
|
+
desc "package", "package up the gem"
|
9
|
+
define_method :package do
|
10
|
+
FileUtils.mkdir_p(File.join(Dir.pwd, "pkg"))
|
11
|
+
Gem::Builder.new(spec).build
|
12
|
+
FileUtils.mv(spec.file_name, File.join(Dir.pwd, "pkg", spec.file_name))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.install_task(spec)
|
17
|
+
package_task spec
|
18
|
+
|
19
|
+
null, sudo, gem = RUBY_PLATFORM =~ /mswin|mingw/ ? ['NUL', '', 'gem.bat'] :
|
20
|
+
['/dev/null', 'sudo', 'gem']
|
21
|
+
|
22
|
+
desc "install", "install the gem"
|
23
|
+
define_method :install do
|
24
|
+
old_stderr, $stderr = $stderr.dup, File.open(null, "w")
|
25
|
+
package
|
26
|
+
$stderr = old_stderr
|
27
|
+
system %{#{sudo} #{Gem.ruby} -S #{gem} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.spec_task(file_list, opts = {})
|
32
|
+
name = opts.delete(:name) || "spec"
|
33
|
+
rcov_dir = opts.delete(:rcov_dir) || "coverage"
|
34
|
+
file_list = file_list.map {|f| %["#{f}"]}.join(" ")
|
35
|
+
verbose = opts.delete(:verbose)
|
36
|
+
opts = {:format => "specdoc", :color => true}.merge(opts)
|
37
|
+
|
38
|
+
rcov_opts = convert_task_options(opts.delete(:rcov) || {})
|
39
|
+
rcov = !rcov_opts.empty?
|
40
|
+
options = convert_task_options(opts)
|
41
|
+
|
42
|
+
if rcov
|
43
|
+
FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
|
44
|
+
end
|
45
|
+
|
46
|
+
desc(name, "spec task")
|
47
|
+
define_method(name) do
|
48
|
+
require 'rbconfig'
|
49
|
+
cmd = RbConfig::CONFIG['ruby_install_name'] << " "
|
50
|
+
if rcov
|
51
|
+
cmd << "-S #{where('rcov')} -o #{rcov_dir} #{rcov_opts} "
|
52
|
+
end
|
53
|
+
cmd << where('spec')
|
54
|
+
cmd << " -- " if rcov
|
55
|
+
cmd << " "
|
56
|
+
cmd << file_list
|
57
|
+
cmd << " "
|
58
|
+
cmd << options
|
59
|
+
puts cmd if verbose
|
60
|
+
system(cmd)
|
61
|
+
exit($?.exitstatus)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def self.convert_task_options(opts)
|
67
|
+
opts.map do |key, value|
|
68
|
+
case value
|
69
|
+
when true
|
70
|
+
"--#{key}"
|
71
|
+
when Array
|
72
|
+
value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
|
73
|
+
when nil, false
|
74
|
+
""
|
75
|
+
else
|
76
|
+
"--#{key} #{value.inspect}"
|
77
|
+
end
|
78
|
+
end.join(" ")
|
79
|
+
end
|
80
|
+
|
81
|
+
def where(file)
|
82
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
83
|
+
file_with_path = File.join(path, file)
|
84
|
+
next unless File.exist?(file_with_path) && File.executable?(file_with_path)
|
85
|
+
return File.expand_path(file_with_path)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/thor/util.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'thor/error'
|
4
|
+
|
5
|
+
module ObjectSpace
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# @return <Array[Class]> All the classes in the object space.
|
10
|
+
def classes
|
11
|
+
klasses = []
|
12
|
+
ObjectSpace.each_object(Class) {|o| klasses << o}
|
13
|
+
klasses
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class Thor
|
20
|
+
module Tasks; end
|
21
|
+
|
22
|
+
module Util
|
23
|
+
|
24
|
+
def self.full_const_get(obj, name)
|
25
|
+
list = name.split("::")
|
26
|
+
list.shift if list.first.empty?
|
27
|
+
list.each do |x|
|
28
|
+
# This is required because const_get tries to look for constants in the
|
29
|
+
# ancestor chain, but we only want constants that are HERE
|
30
|
+
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
|
31
|
+
end
|
32
|
+
obj
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.constant_to_thor_path(str, remove_default = true)
|
36
|
+
str = str.to_s.gsub(/^Thor::Tasks::/, "")
|
37
|
+
str = snake_case(str).squeeze(":")
|
38
|
+
str.gsub!(/^default/, '') if remove_default
|
39
|
+
str
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.constant_from_thor_path(str)
|
43
|
+
make_constant(to_constant(str))
|
44
|
+
rescue NameError => e
|
45
|
+
raise e unless e.message =~ /^uninitialized constant (.*)$/
|
46
|
+
raise Error, "There was no available namespace `#{str}'."
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.to_constant(str)
|
50
|
+
str = 'default' if str.empty?
|
51
|
+
str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.constants_in_contents(str, file = __FILE__)
|
55
|
+
klasses = ObjectSpace.classes.dup
|
56
|
+
Module.new.class_eval(str, file)
|
57
|
+
klasses = ObjectSpace.classes - klasses
|
58
|
+
klasses = klasses.select {|k| k < Thor }
|
59
|
+
klasses.map! {|k| k.to_s.gsub(/#<Module:\w+>::/, '')}
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.make_constant(str, base = [Thor::Tasks, Object])
|
63
|
+
which = base.find do |obj|
|
64
|
+
full_const_get(obj, str) rescue nil
|
65
|
+
end
|
66
|
+
return full_const_get(which, str) if which
|
67
|
+
raise NameError, "uninitialized constant #{str}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.snake_case(str)
|
71
|
+
return str.downcase if str =~ /^[A-Z_]+$/
|
72
|
+
str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
|
73
|
+
return $+.downcase
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|