thor 0.9.2
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/LICENSE +20 -0
- data/README.markdown +61 -0
- data/Rakefile +41 -0
- data/bin/thor +327 -0
- data/lib/getopt.rb +238 -0
- data/lib/thor.rb +131 -0
- data/lib/thor/tasks.rb +76 -0
- data/lib/vendor/ruby2ruby.rb +1090 -0
- data/lib/vendor/sexp.rb +278 -0
- data/lib/vendor/sexp_processor.rb +336 -0
- data/lib/vendor/unified_ruby.rb +196 -0
- metadata +66 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Yehuda Katz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
thor
|
2
|
+
====
|
3
|
+
|
4
|
+
Map options to a class. Simply create a class with the appropriate annotations, and have options automatically map
|
5
|
+
to functions and parameters.
|
6
|
+
|
7
|
+
Examples:
|
8
|
+
|
9
|
+
class MyApp
|
10
|
+
extend Hermes # [1]
|
11
|
+
|
12
|
+
map "-L" => :list # [2]
|
13
|
+
|
14
|
+
desc "install APP_NAME", "install one of the available apps" # [3]
|
15
|
+
method_options :force => :boolean # [4]
|
16
|
+
def install(name, opts)
|
17
|
+
... code ...
|
18
|
+
if opts[:force]
|
19
|
+
# do something
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
|
24
|
+
def list(search = "")
|
25
|
+
# list everything
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
MyApp.start
|
31
|
+
|
32
|
+
Hermes automatically maps commands as follows:
|
33
|
+
|
34
|
+
app install name --force
|
35
|
+
|
36
|
+
That gets converted to:
|
37
|
+
|
38
|
+
MyApp.new.install("name", :force => true)
|
39
|
+
|
40
|
+
[1] Use `extend Hermes` to turn a class into an option mapper
|
41
|
+
|
42
|
+
[2] Map additional non-valid identifiers to specific methods. In this case,
|
43
|
+
convert -L to :list
|
44
|
+
|
45
|
+
[3] Describe the method immediately below. The first parameter is the usage information,
|
46
|
+
and the second parameter is the description.
|
47
|
+
|
48
|
+
[4] Provide any additional options. These will be marshaled from -- and - params.
|
49
|
+
In this case, a --force and a -f option is added.
|
50
|
+
|
51
|
+
Types for `method_options`
|
52
|
+
--------------------------
|
53
|
+
|
54
|
+
<dl>
|
55
|
+
<dt>:boolean</dt>
|
56
|
+
<dd>true if the option is passed</dd>
|
57
|
+
<dt>:required</dt>
|
58
|
+
<dd>A key/value option that MUST be provided</dd>
|
59
|
+
<dt>:optional</dt>
|
60
|
+
<dd>A key/value option that MAY be provided</dd>
|
61
|
+
</dl>
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
GEM = "thor"
|
8
|
+
GEM_VERSION = "0.9.2"
|
9
|
+
AUTHOR = "Yehuda Katz"
|
10
|
+
EMAIL = "wycats@gmail.com"
|
11
|
+
HOMEPAGE = "http://yehudakatz.com"
|
12
|
+
SUMMARY = "A gem that maps options to a class"
|
13
|
+
|
14
|
+
spec = Gem::Specification.new do |s|
|
15
|
+
s.name = GEM
|
16
|
+
s.version = GEM_VERSION
|
17
|
+
s.platform = Gem::Platform::RUBY
|
18
|
+
s.has_rdoc = true
|
19
|
+
s.extra_rdoc_files = ["README.markdown", "LICENSE"]
|
20
|
+
s.summary = SUMMARY
|
21
|
+
s.description = s.summary
|
22
|
+
s.author = AUTHOR
|
23
|
+
s.email = EMAIL
|
24
|
+
s.homepage = HOMEPAGE
|
25
|
+
|
26
|
+
s.require_path = 'lib'
|
27
|
+
s.autorequire = GEM
|
28
|
+
s.bindir = "bin"
|
29
|
+
s.executables = %w( thor )
|
30
|
+
s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{bin,lib,specs}/**/*")
|
31
|
+
end
|
32
|
+
|
33
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
34
|
+
pkg.gem_spec = spec
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :install
|
38
|
+
desc "install the gem locally"
|
39
|
+
task :install => [:package] do
|
40
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION} --no-rdoc --no-ri --no-update-sources}
|
41
|
+
end
|
data/bin/thor
ADDED
@@ -0,0 +1,327 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "open-uri"
|
5
|
+
require "fileutils"
|
6
|
+
require "yaml"
|
7
|
+
require "digest/md5"
|
8
|
+
require "readline"
|
9
|
+
|
10
|
+
module ObjectSpace
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
# ==== Returns
|
15
|
+
# Array[Class]:: All the classes in the object space.
|
16
|
+
def classes
|
17
|
+
klasses = []
|
18
|
+
ObjectSpace.each_object(Class) {|o| klasses << o}
|
19
|
+
klasses
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class Thor::Util
|
26
|
+
|
27
|
+
# @public
|
28
|
+
def self.constant_to_thor_path(str)
|
29
|
+
snake_case(str).squeeze(":")
|
30
|
+
end
|
31
|
+
|
32
|
+
# @public
|
33
|
+
def self.constant_from_thor_path(str)
|
34
|
+
make_constant(to_constant(str))
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.to_constant(str)
|
38
|
+
str.gsub(/:(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.constants_in_contents(str)
|
42
|
+
klasses = self.constants.dup
|
43
|
+
eval(str)
|
44
|
+
ret = self.constants - klasses
|
45
|
+
ret.each {|k| self.send(:remove_const, k)}
|
46
|
+
ret
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
# @private
|
51
|
+
def self.make_constant(str)
|
52
|
+
list = str.split("::")
|
53
|
+
obj = Object
|
54
|
+
list.each {|x| obj = obj.const_get(x) }
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
|
58
|
+
# @private
|
59
|
+
def self.snake_case(str)
|
60
|
+
return str.downcase if str =~ /^[A-Z]+$/
|
61
|
+
str.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
|
62
|
+
return $+.downcase
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
class Thor::Runner < Thor
|
68
|
+
|
69
|
+
def self.globs_for(path)
|
70
|
+
["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize_thorfiles(include_system = true)
|
74
|
+
thorfiles(include_system).each {|f| load f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
|
75
|
+
end
|
76
|
+
|
77
|
+
map "-T" => :list, "-i" => :install, "-u" => :update
|
78
|
+
|
79
|
+
desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
|
80
|
+
method_options :as => :optional
|
81
|
+
def install(name, opts)
|
82
|
+
initialize_thorfiles
|
83
|
+
begin
|
84
|
+
contents = open(name).read
|
85
|
+
rescue OpenURI::HTTPError
|
86
|
+
puts "The URI you provided: `#{name}' was invalid"
|
87
|
+
return
|
88
|
+
rescue Errno::ENOENT
|
89
|
+
puts "`#{name}' is not a valid file"
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
puts "Your Thorfile contains: "
|
94
|
+
puts contents
|
95
|
+
print "Do you wish to continue [y/N]? "
|
96
|
+
response = Readline.readline
|
97
|
+
|
98
|
+
return unless response =~ /^\s*y/i
|
99
|
+
|
100
|
+
constants = Thor::Util.constants_in_contents(contents)
|
101
|
+
|
102
|
+
name = name =~ /\.thor$/ ? name : "#{name}.thor"
|
103
|
+
|
104
|
+
as = opts["as"] || begin
|
105
|
+
first_line = contents.split("\n")[0]
|
106
|
+
(match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
|
107
|
+
end
|
108
|
+
|
109
|
+
if !as
|
110
|
+
print "Please specify a name for #{name} in the system repository [#{name}]: "
|
111
|
+
as = Readline.readline
|
112
|
+
as = name if as.empty?
|
113
|
+
end
|
114
|
+
|
115
|
+
FileUtils.mkdir_p thor_root
|
116
|
+
|
117
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
118
|
+
FileUtils.touch(yaml_file)
|
119
|
+
yaml = thor_yaml
|
120
|
+
|
121
|
+
yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => name, :constants => constants}
|
122
|
+
|
123
|
+
save_yaml(yaml)
|
124
|
+
|
125
|
+
puts "Storing thor file in your system repository"
|
126
|
+
|
127
|
+
File.open(File.join(thor_root, yaml[as][:filename] + ".thor"), "w") do |file|
|
128
|
+
file.puts contents
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
desc "uninstall NAME", "uninstall a named Thor module"
|
133
|
+
def uninstall(name)
|
134
|
+
yaml = thor_yaml
|
135
|
+
unless yaml[name]
|
136
|
+
puts "There was no module by that name installed"
|
137
|
+
return
|
138
|
+
end
|
139
|
+
|
140
|
+
puts "Uninstalling #{name}."
|
141
|
+
|
142
|
+
file = File.join(thor_root, "#{yaml[name][:filename]}.thor")
|
143
|
+
File.delete(file)
|
144
|
+
yaml.delete(name)
|
145
|
+
save_yaml(yaml)
|
146
|
+
|
147
|
+
puts "Done."
|
148
|
+
end
|
149
|
+
|
150
|
+
desc "update NAME", "update a Thor file from its original location"
|
151
|
+
def update(name)
|
152
|
+
yaml = thor_yaml
|
153
|
+
if !yaml[name] || !yaml[name][:location]
|
154
|
+
puts "`#{name}' was not found in the system repository"
|
155
|
+
else
|
156
|
+
puts "Updating `#{name}' from #{yaml[name][:location]}"
|
157
|
+
install(yaml[name][:location], "as" => name)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def installed
|
162
|
+
Dir["#{ENV["HOME"]}/.thor/**/*.thor"].each do |f|
|
163
|
+
load f unless Thor.subclass_files.keys.include?(File.expand_path(f))
|
164
|
+
end
|
165
|
+
display_klasses(true)
|
166
|
+
end
|
167
|
+
|
168
|
+
desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
|
169
|
+
method_options :substring => :boolean
|
170
|
+
def list(search = "", options = {})
|
171
|
+
initialize_thorfiles
|
172
|
+
search = ".*#{search}" if options["substring"]
|
173
|
+
search = /^#{search}.*/i
|
174
|
+
|
175
|
+
display_klasses(false, Thor.subclasses.select {|k|
|
176
|
+
Thor::Util.constant_to_thor_path(k.name) =~ search})
|
177
|
+
end
|
178
|
+
|
179
|
+
def method_missing(meth, *args)
|
180
|
+
initialize_thorfiles(false)
|
181
|
+
meth = meth.to_s
|
182
|
+
unless meth =~ /:/
|
183
|
+
puts "Thor tasks must contain a :"
|
184
|
+
return
|
185
|
+
end
|
186
|
+
|
187
|
+
thor_klass = meth.split(":")[0...-1].join(":")
|
188
|
+
to_call = meth.split(":").last
|
189
|
+
|
190
|
+
yaml = thor_yaml
|
191
|
+
|
192
|
+
klass_str = Thor::Util.to_constant(thor_klass)
|
193
|
+
files = yaml.inject([]) { |a,(k,v)| a << v[:filename] if v[:constants] && v[:constants].include?(klass_str); a }
|
194
|
+
|
195
|
+
unless files.empty?
|
196
|
+
files.each do |f|
|
197
|
+
load File.join(thor_root, "#{f}.thor")
|
198
|
+
end
|
199
|
+
klass = Thor::Util.constant_from_thor_path(thor_klass)
|
200
|
+
else
|
201
|
+
begin
|
202
|
+
klass = Thor::Util.constant_from_thor_path(thor_klass)
|
203
|
+
rescue
|
204
|
+
puts "There was no available namespace `#{thor_klass}'."
|
205
|
+
return
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
unless klass.ancestors.include?(Thor)
|
210
|
+
puts "`#{thor_klass}' is not a Thor module"
|
211
|
+
return
|
212
|
+
end
|
213
|
+
|
214
|
+
ARGV.replace [to_call, *(args + ARGV)].compact
|
215
|
+
begin
|
216
|
+
klass.start
|
217
|
+
rescue ArgumentError
|
218
|
+
puts "You need to call #{to_call} as `#{klass.usage_for_method(to_call)}'"
|
219
|
+
rescue NoMethodError
|
220
|
+
puts "`#{to_call}' is not available in #{thor_klass}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
def thor_root
|
226
|
+
File.join(ENV["HOME"], ".thor")
|
227
|
+
end
|
228
|
+
|
229
|
+
def thor_yaml
|
230
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
231
|
+
yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
|
232
|
+
yaml || {}
|
233
|
+
end
|
234
|
+
|
235
|
+
def save_yaml(yaml)
|
236
|
+
yaml_file = File.join(thor_root, "thor.yml")
|
237
|
+
File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
|
238
|
+
end
|
239
|
+
|
240
|
+
def display_klasses(with_modules = false, klasses = Thor.subclasses)
|
241
|
+
klasses = klasses - [Thor::Runner]
|
242
|
+
|
243
|
+
if klasses.empty?
|
244
|
+
puts "No thorfiles available"
|
245
|
+
return
|
246
|
+
end
|
247
|
+
|
248
|
+
if with_modules
|
249
|
+
yaml = thor_yaml
|
250
|
+
max_name = yaml.max {|(xk,xv),(yk,yv)| xk.size <=> yk.size }.first.size
|
251
|
+
|
252
|
+
print "%-#{max_name + 4}s" % "Name"
|
253
|
+
puts "Modules"
|
254
|
+
print "%-#{max_name + 4}s" % "----"
|
255
|
+
puts "-------"
|
256
|
+
|
257
|
+
yaml.each do |name, info|
|
258
|
+
print "%-#{max_name + 4}s" % name
|
259
|
+
puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
|
260
|
+
end
|
261
|
+
|
262
|
+
puts
|
263
|
+
end
|
264
|
+
|
265
|
+
puts "Tasks"
|
266
|
+
puts "-----"
|
267
|
+
|
268
|
+
# Calculate the largest base class name
|
269
|
+
max_base = klasses.max do |x,y|
|
270
|
+
Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
|
271
|
+
end.name.size
|
272
|
+
|
273
|
+
# Calculate the size of the largest option description
|
274
|
+
max_left_item = klasses.max do |x,y|
|
275
|
+
(x.help_list && x.help_list.max.usage + x.help_list.max.opt).to_i <=>
|
276
|
+
(y.help_list && y.help_list.max.usage + y.help_list.max.opt).to_i
|
277
|
+
end
|
278
|
+
|
279
|
+
max_left = max_left_item.help_list.max.usage + max_left_item.help_list.max.opt
|
280
|
+
|
281
|
+
klasses.map {|k| k.help_list}.compact.each do |item|
|
282
|
+
display_tasks(item, max_base, max_left)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def display_tasks(item, max_base, max_left)
|
287
|
+
base = Thor::Util.constant_to_thor_path(item.klass.name)
|
288
|
+
item.usages.each do |name, usage|
|
289
|
+
format_string = "%-#{max_left + max_base + 5}s"
|
290
|
+
print format_string %
|
291
|
+
"#{base}:#{item.usages.assoc(name).last} #{display_opts(item.opts.assoc(name) && item.opts.assoc(name).last)}"
|
292
|
+
puts item.descriptions.assoc(name).last
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def display_opts(opts)
|
297
|
+
return "" unless opts
|
298
|
+
opts.map do |opt, val|
|
299
|
+
if val == true || val == "BOOLEAN"
|
300
|
+
"[#{opt}]"
|
301
|
+
elsif val == "REQUIRED"
|
302
|
+
opt + "=" + opt.gsub(/\-/, "").upcase
|
303
|
+
elsif val == "OPTIONAL"
|
304
|
+
"[" + opt + "=" + opt.gsub(/\-/, "").upcase + "]"
|
305
|
+
end
|
306
|
+
end.join(" ")
|
307
|
+
end
|
308
|
+
|
309
|
+
def thorfiles(include_system = true)
|
310
|
+
path = Dir.pwd
|
311
|
+
system_thorfiles = Dir["#{ENV["HOME"]}/.thor/**/*.thor"]
|
312
|
+
thorfiles = []
|
313
|
+
|
314
|
+
# Look for Thorfile or *.thor in the current directory or a parent directory, until the root
|
315
|
+
while thorfiles.empty?
|
316
|
+
thorfiles = Dir[*Thor::Runner.globs_for(path)]
|
317
|
+
path = File.dirname(path)
|
318
|
+
break if path == "/"
|
319
|
+
end
|
320
|
+
thorfiles + (include_system ? system_thorfiles : [])
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
unless defined?(Spec)
|
326
|
+
Thor::Runner.start
|
327
|
+
end
|