thor 0.9.2 → 0.9.5

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.
@@ -0,0 +1,35 @@
1
+ == TODO
2
+
3
+ * Change Thor.start to parse ARGV in a single pass
4
+ * Improve spec coverage for Thor::Runner
5
+ * Improve help output to list shorthand switches, too
6
+ * Investigate and fix deep namespacing ("foo:bar:baz") issues
7
+
8
+ == 0.9.5, released 2008-08-27
9
+
10
+ * Improve Windows compatibility
11
+ * Update (incorrect) README and task.thor sample file
12
+ * Options hash is now frozen (once returned)
13
+ * Allow magic predicates on options object. For instance: `options.force?`
14
+ * Add support for :numeric type
15
+ * BACKWARDS INCOMPATIBLE: Refactor Thor::Options. You cannot access shorthand forms in options hash anymore (for instance, options[:f])
16
+ * Allow specifying optional args with default values: method_options(:user => "mislav")
17
+ * Don't write options for nil or false values. This allows, for example, turning color off when running specs.
18
+ * Exit with the status of the spec command to help CI stuff out some.
19
+
20
+ == 0.9.4, released 2008-08-13
21
+
22
+ * Try to add Windows compatibility.
23
+ * BACKWARDS INCOMPATIBLE: options hash is now accessed as a property in your class and is not passed as last argument anymore
24
+ * Allow options at the beginning of the argument list as well as the end.
25
+ * Make options available with symbol keys in addition to string keys.
26
+ * Allow true to be passed to Thor#method_options to denote a boolean option.
27
+ * If loading a thor file fails, don't give up, just print a warning and keep going.
28
+ * Make sure that we re-raise errors if they happened further down the pipe than we care about.
29
+ * Only delete the old file on updating when the installation of the new one is a success
30
+ * Make it Ruby 1.8.5 compatible.
31
+ * Don't raise an error if a boolean switch is defined multiple times.
32
+ * Thor::Options now doesn't parse through things that look like options but aren't.
33
+ * Add URI detection to install task, and make sure we don't append ".thor" to URIs
34
+ * Add rake2thor to the gem binfiles.
35
+ * Make sure local Thorfiles override system-wide ones.
@@ -4,58 +4,73 @@ thor
4
4
  Map options to a class. Simply create a class with the appropriate annotations, and have options automatically map
5
5
  to functions and parameters.
6
6
 
7
- Examples:
7
+ Example:
8
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]
9
+ class MyApp < Thor # [1]
10
+ map "-L" => :list # [2]
11
+
12
+ desc "install APP_NAME", "install one of the available apps" # [3]
13
+ method_options :force => :boolean, :alias => :optional # [4]
14
+ def install(name)
15
+ user_alias = options[:alias]
16
+ if options.force?
19
17
  # do something
20
18
  end
19
+ # ... other code ...
21
20
  end
22
21
 
23
22
  desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
24
23
  def list(search = "")
25
24
  # list everything
26
25
  end
27
-
28
26
  end
29
27
 
30
- MyApp.start
31
-
32
- Hermes automatically maps commands as follows:
28
+ Thor automatically maps commands as such:
33
29
 
34
- app install name --force
30
+ app install myname --force
35
31
 
36
32
  That gets converted to:
37
33
 
38
- MyApp.new.install("name", :force => true)
39
-
40
- [1] Use `extend Hermes` to turn a class into an option mapper
34
+ MyApp.new.install("myname")
35
+ # with {'force' => true} as options hash
41
36
 
42
- [2] Map additional non-valid identifiers to specific methods. In this case,
37
+ 1. Inherit from Thor to turn a class into an option mapper
38
+ 2. Map additional non-valid identifiers to specific methods. In this case,
43
39
  convert -L to :list
44
-
45
- [3] Describe the method immediately below. The first parameter is the usage information,
40
+ 3. Describe the method immediately below. The first parameter is the usage information,
46
41
  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.
42
+ 4. Provide any additional options. These will be marshaled from `--` and `-` params.
43
+ In this case, a `--force` and a `-f` option is added.
50
44
 
51
45
  Types for `method_options`
52
46
  --------------------------
53
47
 
54
48
  <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>
49
+ <dt><code>:boolean</code></dt>
50
+ <dd>true if the option is passed</dd>
51
+ <dt><code>true</code></dt>
52
+ <dd>same as <code>:boolean</code></dd>
53
+ <dt><code>:required</code></dt>
54
+ <dd>the value for this option MUST be provided</dd>
55
+ <dt><code>:optional</code></dt>
56
+ <dd>the value for this option MAY be provided</dd>
57
+ <dt><code>:numeric</code></dt>
58
+ <dd>the value MAY be provided, but MUST be in numeric form</dd>
59
+ <dt>a String or Numeric</dt>
60
+ <dd>same as <code>:optional</code>, but fall back to the given object as default value</dd>
61
+ </dl>
62
+
63
+ In case of unsatisfied requirements, `Thor::Options::Error` is raised.
64
+
65
+ Examples of option parsing:
66
+
67
+ # let's say this is how we defined options for a method:
68
+ method_options(:force => :boolean, :retries => :numeric)
69
+
70
+ # here is how the following command-line invocations would be parsed:
71
+
72
+ command -f --retries 5 # => {'force' => true, 'retries' => 5}
73
+ command --force -r=5 # => {'force' => true, 'retries' => 5}
74
+ command -fr 5 # => {'force' => true, 'retries' => 5}
75
+ command --retries=5 # => {'retries' => 5}
76
+ command -r5 # => {'retries' => 5}
data/Rakefile CHANGED
@@ -1,41 +1,6 @@
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
1
  task :default => :install
2
+
38
3
  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
4
+ task :install do
5
+ sh %{ruby "#{File.dirname(__FILE__)}/bin/thor" :install}
6
+ end
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ruby2ruby'
5
+ require 'rake'
6
+
7
+ input = ARGV[0] || 'Rakefile'
8
+ output = ARGV[1] || 'Thorfile'
9
+
10
+ $requires = []
11
+
12
+ module Kernel
13
+ def require_with_record(file)
14
+ $requires << file if caller[1] =~ /rake2thor:/
15
+ require_without_record file
16
+ end
17
+ alias_method :require_without_record, :require
18
+ alias_method :require, :require_with_record
19
+ end
20
+
21
+ load input
22
+
23
+ @private_methods = []
24
+
25
+ def file_task_name(name)
26
+ "compile_" + name.gsub('/', '_slash_').gsub('.', '_dot_').gsub(/\W/, '_')
27
+ end
28
+
29
+ def method_for_task(task)
30
+ file_task = task.is_a?(Rake::FileTask)
31
+ comment = task.instance_variable_get('@comment')
32
+ prereqs = task.instance_variable_get('@prerequisites').select(&Rake::Task.method(:task_defined?))
33
+ actions = task.instance_variable_get('@actions')
34
+ name = task.name.gsub(/^([^:]+:)+/, '')
35
+ name = file_task_name(name) if file_task
36
+ meth = ''
37
+
38
+ meth << "desc #{name.inspect}, #{comment.inspect}\n" if comment
39
+ meth << "def #{name}\n"
40
+
41
+ meth << prereqs.map do |pre|
42
+ pre = pre.to_s
43
+ pre = file_task_name(pre) if Rake::Task[pre].is_a?(Rake::FileTask)
44
+ ' ' + pre
45
+ end.join("\n")
46
+
47
+ meth << "\n\n" unless prereqs.empty? || actions.empty?
48
+
49
+ meth << actions.map do |act|
50
+ act = act.to_ruby
51
+ unless act.gsub!(/^proc \{ \|(\w+)\|\n/,
52
+ " \\1 = Struct.new(:name).new(#{name.inspect}) # A crude mock Rake::Task object\n")
53
+ act.gsub!(/^proc \{\n/, '')
54
+ end
55
+ act.gsub(/\n\}$/, '')
56
+ end.join("\n")
57
+
58
+ meth << "\nend"
59
+
60
+ if file_task
61
+ @private_methods << meth
62
+ return
63
+ end
64
+
65
+ meth
66
+ end
67
+
68
+ body = Rake::Task.tasks.map(&method(:method_for_task)).compact.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n")
69
+
70
+ unless @private_methods.empty?
71
+ body << "\n\n private\n\n"
72
+ body << @private_methods.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n")
73
+ end
74
+
75
+ requires = $requires.map { |r| "require #{r.inspect}" }.join("\n")
76
+
77
+ File.open(output, 'w') { |f| f.write(<<END.lstrip) }
78
+ #{requires}
79
+
80
+ class Default < Thor
81
+ #{body}
82
+ end
83
+ END
data/bin/thor CHANGED
@@ -1,327 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # -*- mode: ruby -*-
2
3
 
3
- require "thor"
4
- require "open-uri"
5
- require "fileutils"
6
- require "yaml"
7
- require "digest/md5"
8
- require "readline"
4
+ require File.dirname(__FILE__) + "/../lib/thor"
5
+ require 'thor/runner'
9
6
 
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
7
+ Thor::Runner.start