thor 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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