thor 0.9.2 → 0.9.5

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