thor 0.9.2 → 0.9.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +35 -0
- data/README.markdown +47 -32
- data/Rakefile +4 -39
- data/bin/rake2thor +83 -0
- data/bin/thor +4 -324
- data/lib/thor.rb +104 -97
- data/lib/thor/error.rb +3 -0
- data/lib/thor/options.rb +238 -0
- data/lib/thor/ordered_hash.rb +64 -0
- data/lib/thor/runner.rb +260 -0
- data/lib/thor/task.rb +68 -0
- data/lib/thor/task_hash.rb +22 -0
- data/lib/thor/tasks.rb +39 -38
- data/lib/thor/tasks/package.rb +18 -0
- data/lib/thor/util.rb +43 -0
- metadata +17 -10
- data/lib/getopt.rb +0 -238
- data/lib/vendor/ruby2ruby.rb +0 -1090
- data/lib/vendor/sexp.rb +0 -278
- data/lib/vendor/sexp_processor.rb +0 -336
- data/lib/vendor/unified_ruby.rb +0 -196
@@ -0,0 +1,64 @@
|
|
1
|
+
class Thor
|
2
|
+
# This class is based on the Ruby 1.9 ordered hashes.
|
3
|
+
# It keeps the semantics and most of the efficiency of normal hashes
|
4
|
+
# while also keeping track of the order in which elements were set.
|
5
|
+
class OrderedHash
|
6
|
+
Node = Struct.new(:key, :value, :next, :prev)
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@hash = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_copy(other)
|
14
|
+
@hash = other.instance_variable_get('@hash').clone
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
@hash[key] && @hash[key].value
|
19
|
+
end
|
20
|
+
|
21
|
+
def []=(key, value)
|
22
|
+
node = Node.new(key, value)
|
23
|
+
|
24
|
+
if old = @hash[key]
|
25
|
+
if old.prev
|
26
|
+
old.prev.next = old.next
|
27
|
+
else # old is @first and @last
|
28
|
+
@first = @last = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if @first.nil?
|
33
|
+
@first = @last = node
|
34
|
+
else
|
35
|
+
node.prev = @last
|
36
|
+
@last.next = node
|
37
|
+
@last = node
|
38
|
+
end
|
39
|
+
|
40
|
+
@hash[key] = node
|
41
|
+
value
|
42
|
+
end
|
43
|
+
|
44
|
+
def each
|
45
|
+
return unless @first
|
46
|
+
yield [@first.key, @first.value]
|
47
|
+
node = @first
|
48
|
+
yield [node.key, node.value] while node = node.next
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def values
|
53
|
+
self.map { |k, v| v }
|
54
|
+
end
|
55
|
+
|
56
|
+
def +(other)
|
57
|
+
new = clone
|
58
|
+
other.each do |key, value|
|
59
|
+
new[key] = value unless self[key]
|
60
|
+
end
|
61
|
+
new
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/thor/runner.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require "thor/util"
|
3
|
+
require "open-uri"
|
4
|
+
require "fileutils"
|
5
|
+
require "yaml"
|
6
|
+
require "digest/md5"
|
7
|
+
require "readline"
|
8
|
+
|
9
|
+
class Thor::Runner < Thor
|
10
|
+
|
11
|
+
def self.globs_for(path)
|
12
|
+
["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
|
13
|
+
end
|
14
|
+
|
15
|
+
map "-T" => :list, "-i" => :install, "-u" => :update
|
16
|
+
|
17
|
+
desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
|
18
|
+
method_options :as => :optional, :relative => :boolean
|
19
|
+
def install(name)
|
20
|
+
initialize_thorfiles
|
21
|
+
begin
|
22
|
+
contents = open(name).read
|
23
|
+
rescue OpenURI::HTTPError
|
24
|
+
raise Error, "Error opening URI `#{name}'"
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
raise Error, "Error opening file `#{name}'"
|
27
|
+
end
|
28
|
+
|
29
|
+
is_uri = File.exist?(name) ? false : true
|
30
|
+
|
31
|
+
puts "Your Thorfile contains: "
|
32
|
+
puts contents
|
33
|
+
print "Do you wish to continue [y/N]? "
|
34
|
+
response = Readline.readline
|
35
|
+
|
36
|
+
return false unless response =~ /^\s*y/i
|
37
|
+
|
38
|
+
constants = Thor::Util.constants_in_contents(contents)
|
39
|
+
|
40
|
+
# name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
|
41
|
+
|
42
|
+
as = options["as"] || begin
|
43
|
+
first_line = contents.split("\n")[0]
|
44
|
+
(match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
|
45
|
+
end
|
46
|
+
|
47
|
+
if !as
|
48
|
+
print "Please specify a name for #{name} in the system repository [#{name}]: "
|
49
|
+
as = Readline.readline
|
50
|
+
as = name if as.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
FileUtils.mkdir_p thor_root
|
54
|
+
|
55
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
56
|
+
FileUtils.touch(yaml_file)
|
57
|
+
yaml = thor_yaml
|
58
|
+
|
59
|
+
location = (options[:relative] || is_uri) ? name : File.expand_path(name)
|
60
|
+
yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => location, :constants => constants}
|
61
|
+
|
62
|
+
save_yaml(yaml)
|
63
|
+
|
64
|
+
puts "Storing thor file in your system repository"
|
65
|
+
|
66
|
+
File.open(File.join(thor_root, yaml[as][:filename]), "w") do |file|
|
67
|
+
file.puts contents
|
68
|
+
end
|
69
|
+
|
70
|
+
yaml[as][:filename] # Indicate sucess
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "uninstall NAME", "uninstall a named Thor module"
|
74
|
+
def uninstall(name)
|
75
|
+
yaml = thor_yaml
|
76
|
+
raise Error, "Can't find module `#{name}'" unless yaml[name]
|
77
|
+
|
78
|
+
puts "Uninstalling #{name}."
|
79
|
+
|
80
|
+
file = File.join(thor_root, "#{yaml[name][:filename]}")
|
81
|
+
File.delete(file)
|
82
|
+
yaml.delete(name)
|
83
|
+
save_yaml(yaml)
|
84
|
+
|
85
|
+
puts "Done."
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "update NAME", "update a Thor file from its original location"
|
89
|
+
def update(name)
|
90
|
+
yaml = thor_yaml
|
91
|
+
raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
|
92
|
+
|
93
|
+
puts "Updating `#{name}' from #{yaml[name][:location]}"
|
94
|
+
old_filename = yaml[name][:filename]
|
95
|
+
options["as"] = name
|
96
|
+
filename = install(yaml[name][:location])
|
97
|
+
unless filename == old_filename
|
98
|
+
File.delete(File.join(thor_root, old_filename))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
|
103
|
+
method_options :internal => :boolean
|
104
|
+
def installed
|
105
|
+
thor_root_glob.each do |f|
|
106
|
+
next if f =~ /thor\.yml$/
|
107
|
+
load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))
|
108
|
+
end
|
109
|
+
|
110
|
+
klasses = Thor.subclasses
|
111
|
+
klasses -= [Thor, Thor::Runner] unless options['internal']
|
112
|
+
display_klasses(true, klasses)
|
113
|
+
end
|
114
|
+
|
115
|
+
desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
|
116
|
+
method_options :substring => :boolean
|
117
|
+
def list(search = "")
|
118
|
+
initialize_thorfiles
|
119
|
+
search = ".*#{search}" if options["substring"]
|
120
|
+
search = /^#{search}.*/i
|
121
|
+
|
122
|
+
display_klasses(false, Thor.subclasses.select {|k|
|
123
|
+
Thor::Util.constant_to_thor_path(k.name) =~ search})
|
124
|
+
end
|
125
|
+
|
126
|
+
# Override Thor#help so we can give info about not-yet-loaded tasks
|
127
|
+
def help(task = nil)
|
128
|
+
initialize_thorfiles(task) if task && task.include?(?:)
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
def method_missing(meth, *args)
|
133
|
+
meth = meth.to_s
|
134
|
+
super(meth.to_sym, *args) unless meth.include? ?:
|
135
|
+
|
136
|
+
initialize_thorfiles(meth)
|
137
|
+
task = Thor[meth]
|
138
|
+
task.parse task.klass.new, ARGV[1..-1]
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.thor_root
|
142
|
+
File.join(ENV["HOME"] || ENV["APPDATA"], ".thor")
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.thor_root_glob
|
146
|
+
# On Windows thor_root will be something like this:
|
147
|
+
#
|
148
|
+
# C:\Documents and Settings\james\.thor
|
149
|
+
#
|
150
|
+
# If we don't #gsub the \ character, Dir.glob will fail.
|
151
|
+
Dir["#{thor_root.gsub(/\\/, '/')}/**/*"]
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
def thor_root
|
156
|
+
self.class.thor_root
|
157
|
+
end
|
158
|
+
|
159
|
+
def thor_root_glob
|
160
|
+
self.class.thor_root_glob
|
161
|
+
end
|
162
|
+
|
163
|
+
def thor_yaml
|
164
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
165
|
+
yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
|
166
|
+
yaml || {}
|
167
|
+
end
|
168
|
+
|
169
|
+
def save_yaml(yaml)
|
170
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
171
|
+
File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
|
172
|
+
end
|
173
|
+
|
174
|
+
def display_klasses(with_modules = false, klasses = Thor.subclasses)
|
175
|
+
klasses -= [Thor, Thor::Runner] unless with_modules
|
176
|
+
raise Error, "No Thor tasks available" if klasses.empty?
|
177
|
+
|
178
|
+
if with_modules && !(yaml = thor_yaml).empty?
|
179
|
+
max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
|
180
|
+
modules_label = "Modules"
|
181
|
+
namespaces_label = "Namespaces"
|
182
|
+
column_width = [max_name + 4, modules_label.size + 1].max
|
183
|
+
|
184
|
+
print "%-#{column_width}s" % modules_label
|
185
|
+
puts namespaces_label
|
186
|
+
print "%-#{column_width}s" % ("-" * modules_label.size)
|
187
|
+
puts "-" * namespaces_label.size
|
188
|
+
|
189
|
+
yaml.each do |name, info|
|
190
|
+
print "%-#{column_width}s" % name
|
191
|
+
puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
|
192
|
+
end
|
193
|
+
|
194
|
+
puts
|
195
|
+
end
|
196
|
+
|
197
|
+
puts "Tasks"
|
198
|
+
puts "-----"
|
199
|
+
|
200
|
+
# Calculate the largest base class name
|
201
|
+
max_base = klasses.max do |x,y|
|
202
|
+
Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
|
203
|
+
end.name.size
|
204
|
+
|
205
|
+
# Calculate the size of the largest option description
|
206
|
+
max_left_item = klasses.max do |x,y|
|
207
|
+
(x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
|
208
|
+
end
|
209
|
+
|
210
|
+
max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
|
211
|
+
|
212
|
+
klasses.each {|k| display_tasks(k, max_base, max_left)}
|
213
|
+
end
|
214
|
+
|
215
|
+
def display_tasks(klass, max_base, max_left)
|
216
|
+
base = Thor::Util.constant_to_thor_path(klass.name)
|
217
|
+
klass.tasks.each true do |name, task|
|
218
|
+
format_string = "%-#{max_left + max_base + 5}s"
|
219
|
+
print format_string % task.formatted_usage(true)
|
220
|
+
puts task.description
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def initialize_thorfiles(relevant_to = nil)
|
225
|
+
thorfiles(relevant_to).each {|f| load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
|
226
|
+
end
|
227
|
+
|
228
|
+
def load_thorfile(path)
|
229
|
+
begin
|
230
|
+
load path
|
231
|
+
rescue Object => e
|
232
|
+
$stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def thorfiles(relevant_to = nil)
|
237
|
+
path = Dir.pwd
|
238
|
+
thorfiles = []
|
239
|
+
|
240
|
+
# Look for Thorfile or *.thor in the current directory or a parent directory, until the root
|
241
|
+
while thorfiles.empty?
|
242
|
+
thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
|
243
|
+
path = File.dirname(path)
|
244
|
+
break if path == "/"
|
245
|
+
end
|
246
|
+
|
247
|
+
# We want to load system-wide Thorfiles first
|
248
|
+
# so the local Thorfiles will override them.
|
249
|
+
(relevant_to ? thorfiles_relevant_to(relevant_to) :
|
250
|
+
thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
|
251
|
+
end
|
252
|
+
|
253
|
+
def thorfiles_relevant_to(meth)
|
254
|
+
klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
|
255
|
+
thor_yaml.select do |k, v|
|
256
|
+
v[:constants] && v[:constants].include?(klass_str)
|
257
|
+
end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
data/lib/thor/task.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'thor/error'
|
2
|
+
require 'thor/util'
|
3
|
+
|
4
|
+
class Thor
|
5
|
+
class Task < Struct.new(:meth, :description, :usage, :opts, :klass)
|
6
|
+
def self.dynamic(meth, klass)
|
7
|
+
new(meth, "A dynamically-generated task", meth.to_s, nil, klass)
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse(obj, args)
|
11
|
+
list, hash = parse_args(args)
|
12
|
+
obj.options = hash
|
13
|
+
run(obj, *list)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(obj, *params)
|
17
|
+
raise NoMethodError, "the `#{meth}' task of #{obj.class} is private" if
|
18
|
+
(obj.private_methods + obj.protected_methods).include?(meth)
|
19
|
+
|
20
|
+
obj.send(meth, *params)
|
21
|
+
rescue ArgumentError => e
|
22
|
+
# backtrace sans anything in this file
|
23
|
+
backtrace = e.backtrace.reject {|frame| frame =~ /^#{Regexp.escape(__FILE__)}/}
|
24
|
+
# and sans anything that got us here
|
25
|
+
backtrace -= caller
|
26
|
+
raise e unless backtrace.empty?
|
27
|
+
|
28
|
+
# okay, they really did call it wrong
|
29
|
+
raise Error, "`#{meth}' was called incorrectly. Call as `#{formatted_usage}'"
|
30
|
+
rescue NoMethodError => e
|
31
|
+
begin
|
32
|
+
raise e unless e.message =~ /^undefined method `#{meth}' for #{Regexp.escape(obj.inspect)}$/
|
33
|
+
rescue
|
34
|
+
raise e
|
35
|
+
end
|
36
|
+
raise Error, "The #{namespace false} namespace doesn't have a `#{meth}' task"
|
37
|
+
end
|
38
|
+
|
39
|
+
def namespace(remove_default = true)
|
40
|
+
Thor::Util.constant_to_thor_path(klass, remove_default)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_klass(klass)
|
44
|
+
new = self.dup
|
45
|
+
new.klass = klass
|
46
|
+
new
|
47
|
+
end
|
48
|
+
|
49
|
+
def opts
|
50
|
+
return super unless super.kind_of? Hash
|
51
|
+
self.opts = Options.new(super)
|
52
|
+
end
|
53
|
+
|
54
|
+
def formatted_usage(namespace = false)
|
55
|
+
(namespace ? self.namespace + ':' : '') + usage +
|
56
|
+
(opts ? " " + opts.formatted_usage : "")
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def parse_args(args)
|
62
|
+
return [args, {}] unless opts
|
63
|
+
hash = opts.parse(args)
|
64
|
+
list = opts.non_opts
|
65
|
+
[list, hash]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'thor/ordered_hash'
|
2
|
+
require 'thor/task'
|
3
|
+
|
4
|
+
class Thor::TaskHash < Thor::OrderedHash
|
5
|
+
def initialize(klass)
|
6
|
+
super()
|
7
|
+
@klass = klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def each(local = false, &block)
|
11
|
+
super() { |k, t| yield k, t.with_klass(@klass) }
|
12
|
+
@klass.superclass.tasks.each { |k, t| yield k, t.with_klass(@klass) } unless local || @klass == Thor
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](name)
|
16
|
+
if task = super(name) || (@klass == Thor && @klass.superclass.tasks[name])
|
17
|
+
return task.with_klass(@klass)
|
18
|
+
end
|
19
|
+
|
20
|
+
Thor::Task.dynamic(name, @klass)
|
21
|
+
end
|
22
|
+
end
|
data/lib/thor/tasks.rb
CHANGED
@@ -2,29 +2,28 @@ require "thor"
|
|
2
2
|
require "fileutils"
|
3
3
|
|
4
4
|
class Thor
|
5
|
-
def self.package_task
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
13
|
-
RUBY
|
5
|
+
def self.package_task(spec)
|
6
|
+
desc "package", "package up the gem"
|
7
|
+
define_method :package do
|
8
|
+
FileUtils.mkdir_p(File.join(Dir.pwd, "pkg"))
|
9
|
+
Gem::Builder.new(spec).build
|
10
|
+
FileUtils.mv(spec.file_name, File.join(Dir.pwd, "pkg", spec.file_name))
|
11
|
+
end
|
14
12
|
end
|
15
13
|
|
16
|
-
def self.install_task
|
17
|
-
package_task
|
14
|
+
def self.install_task(spec)
|
15
|
+
package_task spec
|
16
|
+
|
17
|
+
null, sudo, gem = RUBY_PLATFORM =~ /w(in)?32$/ ? ['NUL', '', 'gem.bat'] :
|
18
|
+
['/dev/null', 'sudo', 'gem']
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
RUBY
|
20
|
+
desc "install", "install the gem"
|
21
|
+
define_method :install do
|
22
|
+
old_stderr, $stderr = $stderr.dup, File.open(null, "w")
|
23
|
+
package
|
24
|
+
$stderr = old_stderr
|
25
|
+
system %{#{sudo} #{gem} install pkg/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
|
26
|
+
end
|
28
27
|
end
|
29
28
|
|
30
29
|
def self.spec_task(file_list, opts = {})
|
@@ -42,32 +41,34 @@ class Thor
|
|
42
41
|
FileUtils.rm_rf(File.join(Dir.pwd, rcov_dir))
|
43
42
|
end
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
cmd << "-S rcov -o #{rcov_dir} #{rcov_opts.inspect[1...-1]} "
|
51
|
-
end
|
52
|
-
cmd << `which spec`.chomp
|
53
|
-
cmd << " -- " if #{rcov.inspect}
|
54
|
-
cmd << " "
|
55
|
-
cmd << #{file_list.inspect}
|
56
|
-
cmd << " "
|
57
|
-
cmd << #{options.inspect}
|
58
|
-
puts cmd if #{verbose.inspect}
|
59
|
-
system(cmd)
|
44
|
+
desc(name, "spec task")
|
45
|
+
define_method(name) do
|
46
|
+
cmd = "ruby "
|
47
|
+
if rcov
|
48
|
+
cmd << "-S rcov -o #{rcov_dir} #{rcov_opts} "
|
60
49
|
end
|
61
|
-
|
50
|
+
cmd << `which spec`.chomp
|
51
|
+
cmd << " -- " if rcov
|
52
|
+
cmd << " "
|
53
|
+
cmd << file_list
|
54
|
+
cmd << " "
|
55
|
+
cmd << options
|
56
|
+
puts cmd if verbose
|
57
|
+
system(cmd)
|
58
|
+
exit($?.exitstatus)
|
59
|
+
end
|
62
60
|
end
|
63
61
|
|
64
62
|
private
|
65
63
|
def self.convert_task_options(opts)
|
66
64
|
opts.map do |key, value|
|
67
|
-
|
65
|
+
case value
|
66
|
+
when true
|
68
67
|
"--#{key}"
|
69
|
-
|
68
|
+
when Array
|
70
69
|
value.map {|v| "--#{key} #{v.inspect}"}.join(" ")
|
70
|
+
when nil, false
|
71
|
+
""
|
71
72
|
else
|
72
73
|
"--#{key} #{value.inspect}"
|
73
74
|
end
|