thor 0.12.0 → 0.13.0

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.
Files changed (43) hide show
  1. data/CHANGELOG.rdoc +5 -5
  2. data/README.rdoc +65 -2
  3. data/Thorfile +15 -9
  4. data/bin/thor +1 -0
  5. data/lib/thor/actions/create_file.rb +2 -2
  6. data/lib/thor/actions/directory.rb +2 -4
  7. data/lib/thor/actions/file_manipulation.rb +10 -6
  8. data/lib/thor/actions/inject_into_file.rb +10 -7
  9. data/lib/thor/actions.rb +6 -5
  10. data/lib/thor/base.rb +45 -32
  11. data/lib/thor/core_ext/file_binary_read.rb +9 -0
  12. data/lib/thor/group.rb +46 -37
  13. data/lib/thor/runner.rb +49 -42
  14. data/lib/thor/shell/basic.rb +49 -29
  15. data/lib/thor/shell/color.rb +1 -1
  16. data/lib/thor/shell.rb +1 -1
  17. data/lib/thor/task.rb +27 -38
  18. data/lib/thor/util.rb +4 -22
  19. data/lib/thor/version.rb +1 -1
  20. data/lib/thor.rb +43 -45
  21. data/spec/actions/create_file_spec.rb +7 -7
  22. data/spec/actions/directory_spec.rb +5 -4
  23. data/spec/actions/file_manipulation_spec.rb +29 -16
  24. data/spec/actions/inject_into_file_spec.rb +29 -0
  25. data/spec/actions_spec.rb +14 -13
  26. data/spec/base_spec.rb +16 -1
  27. data/spec/fixtures/bundle/main.thor +1 -0
  28. data/spec/fixtures/doc/%file_name%.rb.tt +1 -0
  29. data/spec/fixtures/doc/README +3 -0
  30. data/spec/fixtures/group.thor +83 -0
  31. data/spec/fixtures/invoke.thor +112 -0
  32. data/spec/fixtures/script.thor +134 -0
  33. data/spec/fixtures/task.thor +10 -0
  34. data/spec/group_spec.rb +1 -7
  35. data/spec/runner_spec.rb +35 -39
  36. data/spec/shell/basic_spec.rb +56 -62
  37. data/spec/shell/color_spec.rb +6 -6
  38. data/spec/spec.opts +1 -0
  39. data/spec/spec_helper.rb +5 -4
  40. data/spec/task_spec.rb +14 -32
  41. data/spec/thor_spec.rb +21 -22
  42. data/spec/util_spec.rb +7 -31
  43. metadata +28 -19
data/CHANGELOG.rdoc CHANGED
@@ -1,9 +1,9 @@
1
- == TODO
2
-
3
- * Improve spec coverage for Thor::Runner
4
-
5
- == 0.12, released 2009-11-06
1
+ == 0.12, released 2010-01-02
6
2
 
3
+ * Methods generated by attr_* are automatically not marked as tasks
4
+ * inject_into_file does not add the same content twice, unless :force is set
5
+ * Removed rr in favor to rspec mock framework
6
+ * Improved output for thor -T
7
7
  * [#7] Do not force white color on status
8
8
  * [#8] Yield a block with the filename on directory
9
9
 
data/README.rdoc CHANGED
@@ -145,7 +145,7 @@ When invoking the task one:
145
145
 
146
146
  The output is "1 2 3", which means that the three task was invoked only once.
147
147
  You can even invoke tasks from another class, so be sure to check the
148
- documentation.
148
+ documentation[http://rdoc.info/rdoc/wycats/thor/blob/f939a3e8a854616784cac1dcff04ef4f3ee5f7ff/Thor.html].
149
149
 
150
150
  == Thor::Group
151
151
 
@@ -227,7 +227,70 @@ To use them, you just need to include Thor::Actions in your Thor classes:
227
227
 
228
228
  Some actions like copy file requires that a class method called source_root is
229
229
  defined in your class. This is the directory where your templates should be
230
- placed. Be sure to check the documentation.
230
+ placed. Be sure to check the documentation on actions[http://rdoc.info/rdoc/wycats/thor/blob/f939a3e8a854616784cac1dcff04ef4f3ee5f7ff/Thor/Actions.html].
231
+
232
+ == Generators
233
+
234
+ A great use for Thor is creating custom generators. Combining Thor::Group,
235
+ Thor::Actions and ERB templates makes this very easy. Here is an example:
236
+
237
+ class Newgem < Thor::Group
238
+ include Thor::Actions
239
+
240
+ # Define arguments and options
241
+ argument :name
242
+ class_option :test_framework, :default => :test_unit
243
+
244
+ def self.source_root
245
+ File.dirname(__FILE__)
246
+ end
247
+
248
+ def create_lib_file
249
+ template('templates/newgem.tt', "#{name}/lib/#{name}.rb")
250
+ end
251
+
252
+ def create_test_file
253
+ test = options[:test_framework] == "rspec" ? :spec : :test
254
+ create_file "#{name}/#{test}/#{name}_#{test}.rb"
255
+ end
256
+
257
+ def copy_licence
258
+ if yes?("Use MIT license?")
259
+ # Make a copy of the MITLICENSE file at the source root
260
+ copy_file "MITLICENSE", "#{name}/MITLICENSE"
261
+ else
262
+ say "Shame on you…", :red
263
+ end
264
+ end
265
+ end
266
+
267
+ Doing a <tt>thor -T</tt> will show how to run our generator. It should read:
268
+ <tt>thor newgem NAME</tt>. This shows that we have to supply a NAME
269
+ argument for our generator to run.
270
+
271
+ The <tt>create_lib_file</tt> uses an ERB template. This is what it looks like:
272
+
273
+ class <%= name.camelize %>
274
+ end
275
+
276
+ The arguments that you set in your generator will automatically be passed in
277
+ when <tt>template</tt> gets called. Be sure to read the documentation[http://rdoc.info/rdoc/wycats/thor/blob/f939a3e8a854616784cac1dcff04ef4f3ee5f7ff/Thor/Actions.html] for
278
+ more options.
279
+
280
+ Running the generator with <tt>thor newgem devise</tt> will
281
+ create two files: "devise/lib/devise.rb",
282
+ "devise/test/devise_test.rb". The user will then be prompt (with the
283
+ use of the method <tt>yes?</tt>) if he wants to copy the MITLICENSE. If you
284
+ want to change the test framework, you can add the option:
285
+ <tt>thor newgem devise --test-framework=rspec</tt>
286
+ This will generate: "devise/lib/devise.rb" and
287
+ "devise/spec/devise_spec.rb".
288
+
289
+ == Further Reading
290
+
291
+ Thor has many scripting possibilities beyond these examples. Be sure to read
292
+ through the documentation[http://rdoc.info/rdoc/wycats/thor/blob/f939a3e8a854616784cac1dcff04ef4f3ee5f7ff/Thor.html] and specs[http://github.com/wycats/thor/tree/master/spec/] to get a better understanding of all the
293
+ options Thor offers.
231
294
 
232
295
  == License
233
296
 
data/Thorfile CHANGED
@@ -1,9 +1,13 @@
1
1
  # enconding: utf-8
2
2
 
3
3
  require File.join(File.dirname(__FILE__), "lib", "thor", "version")
4
+ require 'rubygems'
4
5
  require 'thor/rake_compat'
5
6
  require 'spec/rake/spectask'
6
- require 'rdoc/task'
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ end
7
11
 
8
12
  GEM_NAME = 'thor'
9
13
  EXTRA_RDOC_FILES = ["README.rdoc", "LICENSE", "CHANGELOG.rdoc", "VERSION", "Thorfile"]
@@ -25,13 +29,15 @@ class Default < Thor
25
29
  t.rcov_dir = "rcov"
26
30
  end
27
31
 
28
- RDoc::Task.new do |rdoc|
29
- rdoc.main = "README.rdoc"
30
- rdoc.rdoc_dir = "rdoc"
31
- rdoc.title = GEM_NAME
32
- rdoc.rdoc_files.include(*EXTRA_RDOC_FILES)
33
- rdoc.rdoc_files.include('lib/**/*.rb')
34
- rdoc.options << '--line-numbers' << '--inline-source'
32
+ if defined?(RDoc)
33
+ RDoc::Task.new do |rdoc|
34
+ rdoc.main = "README.rdoc"
35
+ rdoc.rdoc_dir = "rdoc"
36
+ rdoc.title = GEM_NAME
37
+ rdoc.rdoc_files.include(*EXTRA_RDOC_FILES)
38
+ rdoc.rdoc_files.include('lib/**/*.rb')
39
+ rdoc.options << '--line-numbers' << '--inline-source'
40
+ end
35
41
  end
36
42
 
37
43
  begin
@@ -52,7 +58,7 @@ class Default < Thor
52
58
  s.bindir = "bin"
53
59
  s.executables = %w( thor rake2thor )
54
60
  s.files = s.extra_rdoc_files + Dir.glob("{bin,lib}/**/*")
55
- s.files.exclude 'spec/sandbox/**/*'
61
+ s.test_files.include 'spec/**/*'
56
62
  s.test_files.exclude 'spec/sandbox/**/*'
57
63
  end
58
64
 
data/bin/thor CHANGED
@@ -4,4 +4,5 @@
4
4
  require 'thor'
5
5
  require 'thor/runner'
6
6
 
7
+ $thor_runner = true
7
8
  Thor::Runner.start
@@ -42,7 +42,7 @@ class Thor
42
42
  # Boolean:: true if it is identical, false otherwise.
43
43
  #
44
44
  def identical?
45
- exists? && File.read(destination) == render
45
+ exists? && File.binread(destination) == render
46
46
  end
47
47
 
48
48
  # Holds the content to be added to the file.
@@ -58,7 +58,7 @@ class Thor
58
58
  def invoke!
59
59
  invoke_with_conflict_check do
60
60
  FileUtils.mkdir_p(File.dirname(destination))
61
- File.open(destination, 'w'){ |f| f.write render }
61
+ File.open(destination, 'wb') { |f| f.write render }
62
62
  end
63
63
  given_destination
64
64
  end
@@ -79,11 +79,9 @@ class Thor
79
79
  next if dirname == given_destination
80
80
  base.empty_directory(dirname, config)
81
81
  when /\.tt$/
82
- destination = base.template(file_source, file_destination[0..-4], config)
83
- @block.call(destination) if @block
82
+ destination = base.template(file_source, file_destination[0..-4], config, &@block)
84
83
  else
85
- destination = base.copy_file(file_source, file_destination, config)
86
- @block.call(destination) if @block
84
+ destination = base.copy_file(file_source, file_destination, config, &@block)
87
85
  end
88
86
  end
89
87
  end
@@ -18,12 +18,14 @@ class Thor
18
18
  #
19
19
  # copy_file "doc/README"
20
20
  #
21
- def copy_file(source, destination=nil, config={})
21
+ def copy_file(source, destination=nil, config={}, &block)
22
22
  destination ||= source
23
23
  source = File.expand_path(find_in_source_paths(source.to_s))
24
24
 
25
25
  create_file destination, nil, config do
26
- File.read(source)
26
+ content = File.binread(source)
27
+ content = block.call(content) if block
28
+ content
27
29
  end
28
30
  end
29
31
 
@@ -46,7 +48,7 @@ class Thor
46
48
  #
47
49
  def get(source, destination=nil, config={}, &block)
48
50
  source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ /^http\:\/\//
49
- render = open(source).read
51
+ render = File.binread(source)
50
52
 
51
53
  destination ||= if block_given?
52
54
  block.arity == 1 ? block.call(render) : block.call
@@ -72,13 +74,15 @@ class Thor
72
74
  #
73
75
  # template "doc/README"
74
76
  #
75
- def template(source, destination=nil, config={})
77
+ def template(source, destination=nil, config={}, &block)
76
78
  destination ||= source
77
79
  source = File.expand_path(find_in_source_paths(source.to_s))
78
80
  context = instance_eval('binding')
79
81
 
80
82
  create_file destination, nil, config do
81
- ERB.new(::File.read(source), nil, '-').result(context)
83
+ content = ERB.new(::File.binread(source), nil, '-').result(context)
84
+ content = block.call(content) if block
85
+ content
82
86
  end
83
87
  end
84
88
 
@@ -189,7 +193,7 @@ class Thor
189
193
  say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
190
194
 
191
195
  unless options[:pretend]
192
- content = File.read(path)
196
+ content = File.binread(path)
193
197
  content.gsub!(flag, *args, &block)
194
198
  File.open(path, 'wb') { |file| file.write(content) }
195
199
  end
@@ -10,7 +10,8 @@ class Thor
10
10
  # destination<String>:: Relative path to the destination root
11
11
  # data<String>:: Data to add to the file. Can be given as a block.
12
12
  # config<Hash>:: give :verbose => false to not log the status and the flag
13
- # for injection (:after or :before).
13
+ # for injection (:after or :before) or :force => true for
14
+ # insert two or more times the same content.
14
15
  #
15
16
  # ==== Examples
16
17
  #
@@ -55,7 +56,7 @@ class Thor
55
56
  replacement + '\0'
56
57
  end
57
58
 
58
- replace!(/#{flag}/, content)
59
+ replace!(/#{flag}/, content, config[:force])
59
60
  end
60
61
 
61
62
  def revoke!
@@ -69,7 +70,7 @@ class Thor
69
70
  /(#{Regexp.escape(replacement)})(.*)(#{flag})/m
70
71
  end
71
72
 
72
- replace!(regexp, content)
73
+ replace!(regexp, content, true)
73
74
  end
74
75
 
75
76
  protected
@@ -88,11 +89,13 @@ class Thor
88
89
 
89
90
  # Adds the content to the file.
90
91
  #
91
- def replace!(regexp, string)
92
+ def replace!(regexp, string, force)
92
93
  unless base.options[:pretend]
93
- content = File.read(destination)
94
- content.gsub!(regexp, string)
95
- File.open(destination, 'wb') { |file| file.write(content) }
94
+ content = File.binread(destination)
95
+ if force || !content.include?(replacement)
96
+ content.gsub!(regexp, string)
97
+ File.open(destination, 'wb') { |file| file.write(content) }
98
+ end
96
99
  end
97
100
  end
98
101
 
data/lib/thor/actions.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'thor/core_ext/file_binary_read'
2
3
 
3
4
  Dir[File.join(File.dirname(__FILE__), "actions", "*.rb")].each do |action|
4
5
  require action
@@ -38,17 +39,17 @@ class Thor
38
39
  # Add runtime options that help actions execution.
39
40
  #
40
41
  def add_runtime_options!
41
- class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime,
42
- :desc => "Run but do not make any changes"
43
-
44
42
  class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime,
45
43
  :desc => "Overwrite files that already exist"
46
44
 
47
- class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime,
48
- :desc => "Skip files that already exist"
45
+ class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime,
46
+ :desc => "Run but do not make any changes"
49
47
 
50
48
  class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime,
51
49
  :desc => "Supress status output"
50
+
51
+ class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime,
52
+ :desc => "Skip files that already exist"
52
53
  end
53
54
  end
54
55
 
data/lib/thor/base.rb CHANGED
@@ -8,6 +8,9 @@ require 'thor/task'
8
8
  require 'thor/util'
9
9
 
10
10
  class Thor
11
+ autoload :Actions, 'thor/actions'
12
+ autoload :RakeCompat, 'thor/rake_compat'
13
+
11
14
  # Shortcuts for help.
12
15
  HELP_MAPPINGS = %w(-h -? --help -D)
13
16
 
@@ -92,6 +95,20 @@ class Thor
92
95
  end
93
96
 
94
97
  module ClassMethods
98
+ attr_accessor :debugging
99
+
100
+ def attr_reader(*) #:nodoc:
101
+ no_tasks { super }
102
+ end
103
+
104
+ def attr_writer(*) #:nodoc:
105
+ no_tasks { super }
106
+ end
107
+
108
+ def attr_accessor(*) #:nodoc:
109
+ no_tasks { super }
110
+ end
111
+
95
112
  # Adds an argument to the class and creates an attr_accessor for it.
96
113
  #
97
114
  # Arguments are different from options in several aspects. The first one
@@ -347,10 +364,11 @@ class Thor
347
364
  # Default way to start generators from the command line.
348
365
  #
349
366
  def start(given_args=ARGV, config={})
367
+ self.debugging = given_args.include?("--debug")
350
368
  config[:shell] ||= Thor::Base.shell.new
351
369
  yield
352
370
  rescue Thor::Error => e
353
- if given_args.include?("--debug")
371
+ if debugging
354
372
  raise e
355
373
  else
356
374
  config[:shell].error e.message
@@ -361,48 +379,43 @@ class Thor
361
379
  protected
362
380
 
363
381
  # Prints the class options per group. If an option does not belong to
364
- # any group, it uses the ungrouped name value. This method provide to
365
- # hooks to add extra options, one of them if the third argument called
366
- # extra_group that should be a hash in the format :group => Array[Options].
367
- #
368
- # The second is by returning a lambda used to print values. The lambda
369
- # requires two options: the group name and the array of options.
382
+ # any group, it's printed as Class option.
370
383
  #
371
- def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
372
- groups = {}
373
-
384
+ def class_options_help(shell, groups={}) #:nodoc:
385
+ # Group options by group
374
386
  class_options.each do |_, value|
375
387
  groups[value.group] ||= []
376
388
  groups[value.group] << value
377
389
  end
378
390
 
379
- printer = proc do |group_name, options|
380
- list = []
381
- padding = options.collect{ |o| o.aliases.size }.max.to_i * 4
391
+ # Deal with default group
392
+ global_options = groups.delete(nil) || []
393
+ print_options(shell, global_options)
382
394
 
383
- options.each do |option|
384
- item = [ option.usage(padding) ]
385
- item.push(option.description ? "# #{option.description}" : "")
395
+ # Print all others
396
+ groups.each do |group_name, options|
397
+ print_options(shell, options, group_name)
398
+ end
399
+ end
386
400
 
387
- list << item
388
- list << [ "", "# Default: #{option.default}" ] if option.show_default?
389
- end
401
+ # Receives a set of options and print them.
402
+ def print_options(shell, options, group_name=nil)
403
+ return if options.empty?
390
404
 
391
- unless list.empty?
392
- shell.say(group_name ? "#{group_name} options:" : "Options:")
393
- shell.print_table(list, :ident => 2)
394
- shell.say ""
395
- end
396
- end
405
+ list = []
406
+ padding = options.collect{ |o| o.aliases.size }.max.to_i * 4
397
407
 
398
- # Deal with default group
399
- global_options = groups.delete(nil) || []
400
- printer.call(ungrouped_name, global_options) if global_options
408
+ options.each do |option|
409
+ item = [ option.usage(padding) ]
410
+ item.push(option.description ? "# #{option.description}" : "")
401
411
 
402
- # Print all others
403
- groups = extra_group.merge(groups) if extra_group
404
- groups.each(&printer)
405
- printer
412
+ list << item
413
+ list << [ "", "# Default: #{option.default}" ] if option.show_default?
414
+ end
415
+
416
+ shell.say(group_name ? "#{group_name} options:" : "Options:")
417
+ shell.print_table(list, :ident => 2)
418
+ shell.say ""
406
419
  end
407
420
 
408
421
  # Raises an error if the word given is a Thor reserved word.
@@ -0,0 +1,9 @@
1
+ class File #:nodoc:
2
+
3
+ unless File.respond_to?(:binread)
4
+ def self.binread(file)
5
+ File.open(file, 'rb') { |f| f.read }
6
+ end
7
+ end
8
+
9
+ end
data/lib/thor/group.rb CHANGED
@@ -1,8 +1,9 @@
1
+ require 'thor/base'
2
+
1
3
  # Thor has a special class called Thor::Group. The main difference to Thor class
2
4
  # is that it invokes all tasks at once. It also include some methods that allows
3
5
  # invocations to be done at the class method, which are not available to Thor
4
6
  # tasks.
5
- #
6
7
  class Thor::Group
7
8
  class << self
8
9
  # The descrition for this Thor::Group. If none is provided, but a source root
@@ -41,16 +42,12 @@ class Thor::Group
41
42
  # ==== Options
42
43
  # short:: When true, shows only usage.
43
44
  #
44
- def help(shell, options={})
45
- if options[:short]
46
- shell.say banner
47
- else
48
- shell.say "Usage:"
49
- shell.say " #{banner}"
50
- shell.say
51
- class_options_help(shell)
52
- shell.say self.desc if self.desc
53
- end
45
+ def help(shell)
46
+ shell.say "Usage:"
47
+ shell.say " #{banner}\n"
48
+ shell.say
49
+ class_options_help(shell)
50
+ shell.say self.desc if self.desc
54
51
  end
55
52
 
56
53
  # Stores invocations for this class merging with superclass values.
@@ -177,15 +174,11 @@ class Thor::Group
177
174
  # Overwrite class options help to allow invoked generators options to be
178
175
  # shown recursively when invoking a generator.
179
176
  #
180
- def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
181
- group_options = {}
182
-
183
- get_options_from_invocations(group_options, class_options) do |klass|
184
- klass.send(:get_options_from_invocations, group_options, class_options)
177
+ def class_options_help(shell, groups={}) #:nodoc:
178
+ get_options_from_invocations(groups, class_options) do |klass|
179
+ klass.send(:get_options_from_invocations, groups, class_options)
185
180
  end
186
-
187
- group_options.merge!(extra_group) if extra_group
188
- super(shell, ungrouped_name, group_options)
181
+ super(shell, groups)
189
182
  end
190
183
 
191
184
  # Get invocations array and merge options from invocations. Those
@@ -218,13 +211,27 @@ class Thor::Group
218
211
  end
219
212
  end
220
213
 
214
+ # Returns tasks ready to be printed.
215
+ def printable_tasks(*)
216
+ item = []
217
+ item << banner
218
+ item << (desc ? "# #{desc.gsub(/\s+/m,' ')}" : "")
219
+ [item]
220
+ end
221
+
221
222
  protected
222
223
 
223
224
  # The banner for this class. You can customize it if you are invoking the
224
225
  # thor class by another ways which is not the Thor::Runner.
225
226
  #
226
227
  def banner
227
- "#{self.namespace} #{self.arguments.map {|a| a.usage }.join(' ')}"
228
+ base = $thor_runner ? "thor" : File.basename($0.split(" ").first)
229
+ "#{base} #{self_task.formatted_usage(self, false)}"
230
+ end
231
+
232
+ # Represents the whole class as a task.
233
+ def self_task #:nodoc:
234
+ Thor::Task::Dynamic.new(self.namespace, class_options)
228
235
  end
229
236
 
230
237
  def baseclass #:nodoc:
@@ -241,23 +248,25 @@ class Thor::Group
241
248
 
242
249
  protected
243
250
 
244
- # Shortcut to invoke with padding and block handling. Use internally by
245
- # invoke and invoke_from_option class methods.
246
- #
247
- def _invoke_for_class_method(klass, task=nil, *args, &block) #:nodoc:
248
- shell.padding += 1
249
-
250
- result = if block_given?
251
- if block.arity == 2
252
- block.call(self, klass)
253
- else
254
- block.call(self, klass, task)
255
- end
256
- else
257
- invoke klass, task, *args
251
+ # Shortcut to invoke with padding and block handling. Use internally by
252
+ # invoke and invoke_from_option class methods.
253
+ def _invoke_for_class_method(klass, task=nil, *args, &block) #:nodoc:
254
+ shell.padding += 1
255
+
256
+ result = if block_given?
257
+ case block.arity
258
+ when 3
259
+ block.call(self, klass, task)
260
+ when 2
261
+ block.call(self, klass)
262
+ when 1
263
+ instance_exec(klass, &block)
258
264
  end
259
-
260
- shell.padding -= 1
261
- result
265
+ else
266
+ invoke klass, task, *args
262
267
  end
268
+
269
+ shell.padding -= 1
270
+ result
271
+ end
263
272
  end