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 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.
@@ -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>
@@ -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
@@ -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