Lots going on
[lyrix.git] / vendor / rails / railties / lib / rails_generator / commands.rb
blobdb9a087f29e8ef84f6edf2c8f1f63a7d1a9f7894
1 require 'delegate'
2 require 'optparse'
3 require 'fileutils'
4 require 'tempfile'
5 require 'erb'
7 module Rails
8   module Generator
9     module Commands
10       # Here's a convenient way to get a handle on generator commands.
11       # Command.instance('destroy', my_generator) instantiates a Destroy
12       # delegate of my_generator ready to do your dirty work.
13       def self.instance(command, generator)
14         const_get(command.to_s.camelize).new(generator)
15       end
17       # Even more convenient access to commands.  Include Commands in
18       # the generator Base class to get a nice #command instance method
19       # which returns a delegate for the requested command.
20       def self.included(base)
21         base.send(:define_method, :command) do |command|
22           Commands.instance(command, self)
23         end
24       end
27       # Generator commands delegate Rails::Generator::Base and implement
28       # a standard set of actions.  Their behavior is defined by the way
29       # they respond to these actions: Create brings life; Destroy brings
30       # death; List passively observes.
31       #
32       # Commands are invoked by replaying (or rewinding) the generator's
33       # manifest of actions.  See Rails::Generator::Manifest and
34       # Rails::Generator::Base#manifest method that generator subclasses
35       # are required to override.
36       #
37       # Commands allows generators to "plug in" invocation behavior, which
38       # corresponds to the GoF Strategy pattern.
39       class Base < DelegateClass(Rails::Generator::Base)
40         # Replay action manifest.  RewindBase subclass rewinds manifest.
41         def invoke!
42           manifest.replay(self)
43         end
45         def dependency(generator_name, args, runtime_options = {})
46           logger.dependency(generator_name) do
47             self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke!
48           end
49         end
51         # Does nothing for all commands except Create.
52         def class_collisions(*class_names)
53         end
55         # Does nothing for all commands except Create.
56         def readme(*args)
57         end
59         protected
60           def migration_directory(relative_path)
61             directory(@migration_directory = relative_path)
62           end
64           def existing_migrations(file_name)
65             Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/)
66           end
68           def migration_exists?(file_name)
69             not existing_migrations(file_name).empty?
70           end
72           def current_migration_number
73             Dir.glob("#{@migration_directory}/[0-9]*.rb").inject(0) do |max, file_path|
74               n = File.basename(file_path).split('_', 2).first.to_i
75               if n > max then n else max end
76             end
77           end
79           def next_migration_number
80             current_migration_number + 1
81           end
83           def next_migration_string(padding = 3)
84             "%.#{padding}d" % next_migration_number
85           end
87           def gsub_file(relative_destination, regexp, *args, &block)
88             path = destination_path(relative_destination)
89             content = File.read(path).gsub(regexp, *args, &block)
90             File.open(path, 'wb') { |file| file.write(content) }
91           end
93         private
94           # Ask the user interactively whether to force collision.
95           def force_file_collision?(destination, src, dst, file_options = {}, &block)
96             $stdout.print "overwrite #{destination}? [Ynaqd] "
97             case $stdin.gets
98               when /d/i
99                 Tempfile.open(File.basename(destination), File.dirname(dst)) do |temp|
100                   temp.write render_file(src, file_options, &block)
101                   temp.rewind
102                   $stdout.puts `#{diff_cmd} #{dst} #{temp.path}`
103                 end
104                 puts "retrying"
105                 raise 'retry diff'
106               when /a/i
107                 $stdout.puts "forcing #{spec.name}"
108                 options[:collision] = :force
109               when /q/i
110                 $stdout.puts "aborting #{spec.name}"
111                 raise SystemExit
112               when /n/i then :skip
113               else :force
114             end
115           rescue
116             retry
117           end
119           def diff_cmd
120             ENV['RAILS_DIFF'] || 'diff -u'
121           end
123           def render_template_part(template_options)
124             # Getting Sandbox to evaluate part template in it
125             part_binding = template_options[:sandbox].call.sandbox_binding
126             part_rel_path = template_options[:insert]
127             part_path = source_path(part_rel_path)
129             # Render inner template within Sandbox binding
130             rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding)
131             begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id])
132             end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id])
133             begin_mark + rendered_part + end_mark
134           end
136           def template_part_mark(name, id)
137             "<!--[#{name}:#{id}]-->\n"
138           end
139       end
141       # Base class for commands which handle generator actions in reverse, such as Destroy.
142       class RewindBase < Base
143         # Rewind action manifest.
144         def invoke!
145           manifest.rewind(self)
146         end
147       end
150       # Create is the premier generator command.  It copies files, creates
151       # directories, renders templates, and more.
152       class Create < Base
154         # Check whether the given class names are already taken by
155         # Ruby or Rails.  In the future, expand to check other namespaces
156         # such as the rest of the user's app.
157         def class_collisions(*class_names)
158           class_names.flatten.each do |class_name|
159             # Convert to string to allow symbol arguments.
160             class_name = class_name.to_s
162             # Skip empty strings.
163             next if class_name.strip.empty?
165             # Split the class from its module nesting.
166             nesting = class_name.split('::')
167             name = nesting.pop
169             # Extract the last Module in the nesting.
170             last = nesting.inject(Object) { |last, nest|
171               break unless last.const_defined?(nest)
172               last.const_get(nest)
173             }
175             # If the last Module exists, check whether the given
176             # class exists and raise a collision if so.
177             if last and last.const_defined?(name.camelize)
178               raise_class_collision(class_name)
179             end
180           end
181         end
183         # Copy a file from source to destination with collision checking.
184         #
185         # The file_options hash accepts :chmod and :shebang and :collision options.
186         # :chmod sets the permissions of the destination file:
187         #   file 'config/empty.log', 'log/test.log', :chmod => 0664
188         # :shebang sets the #!/usr/bin/ruby line for scripts
189         #   file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
190         # :collision sets the collision option only for the destination file: 
191         #   file 'settings/server.yml', 'config/server.yml', :collision => :skip
192         #
193         # Collisions are handled by checking whether the destination file
194         # exists and either skipping the file, forcing overwrite, or asking
195         # the user what to do.
196         def file(relative_source, relative_destination, file_options = {}, &block)
197           # Determine full paths for source and destination files.
198           source              = source_path(relative_source)
199           destination         = destination_path(relative_destination)
200           destination_exists  = File.exists?(destination)
202           # If source and destination are identical then we're done.
203           if destination_exists and identical?(source, destination, &block)
204             return logger.identical(relative_destination) 
205           end
207           # Check for and resolve file collisions.
208           if destination_exists
210             # Make a choice whether to overwrite the file.  :force and
211             # :skip already have their mind made up, but give :ask a shot.
212             choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask
213               when :ask   then force_file_collision?(relative_destination, source, destination, file_options, &block)
214               when :force then :force
215               when :skip  then :skip
216               else raise "Invalid collision option: #{options[:collision].inspect}"
217             end
219             # Take action based on our choice.  Bail out if we chose to
220             # skip the file; otherwise, log our transgression and continue.
221             case choice
222               when :force then logger.force(relative_destination)
223               when :skip  then return(logger.skip(relative_destination))
224               else raise "Invalid collision choice: #{choice}.inspect"
225             end
227           # File doesn't exist so log its unbesmirched creation.
228           else
229             logger.create relative_destination
230           end
232           # If we're pretending, back off now.
233           return if options[:pretend]
235           # Write destination file with optional shebang.  Yield for content
236           # if block given so templaters may render the source file.  If a
237           # shebang is requested, replace the existing shebang or insert a
238           # new one.
239           File.open(destination, 'wb') do |dest|
240             dest.write render_file(source, file_options, &block)
241           end
243           # Optionally change permissions.
244           if file_options[:chmod]
245             FileUtils.chmod(file_options[:chmod], destination)
246           end
248           # Optionally add file to subversion
249           system("svn add #{destination}") if options[:svn]
250         end
252         # Checks if the source and the destination file are identical. If
253         # passed a block then the source file is a template that needs to first
254         # be evaluated before being compared to the destination.
255         def identical?(source, destination, &block)
256           return false if File.directory? destination
257           source      = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source)
258           destination = IO.read(destination)
259           source == destination
260         end
262         # Generate a file for a Rails application using an ERuby template.
263         # Looks up and evalutes a template by name and writes the result.
264         #
265         # The ERB template uses explicit trim mode to best control the
266         # proliferation of whitespace in generated code.  <%- trims leading
267         # whitespace; -%> trims trailing whitespace including one newline.
268         #
269         # A hash of template options may be passed as the last argument.
270         # The options accepted by the file are accepted as well as :assigns,
271         # a hash of variable bindings.  Example:
272         #   template 'foo', 'bar', :assigns => { :action => 'view' }
273         #
274         # Template is implemented in terms of file.  It calls file with a
275         # block which takes a file handle and returns its rendered contents.
276         def template(relative_source, relative_destination, template_options = {})
277           file(relative_source, relative_destination, template_options) do |file|
278             # Evaluate any assignments in a temporary, throwaway binding.
279             vars = template_options[:assigns] || {}
280             b = binding
281             vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }
283             # Render the source file with the temporary binding.
284             ERB.new(file.read, nil, '-').result(b)
285           end
286         end
288         def complex_template(relative_source, relative_destination, template_options = {})
289           options = template_options.dup
290           options[:assigns] ||= {}
291           options[:assigns]['template_for_inclusion'] = render_template_part(template_options)
292           template(relative_source, relative_destination, options)
293         end
295         # Create a directory including any missing parent directories.
296         # Always directories which exist.
297         def directory(relative_path)
298           path = destination_path(relative_path)
299           if File.exists?(path)
300             logger.exists relative_path
301           else
302             logger.create relative_path
303             unless options[:pretend]
304               FileUtils.mkdir_p(path)
305               
306               # Subversion doesn't do path adds, so we need to add
307               # each directory individually.
308               # So stack up the directory tree and add the paths to
309               # subversion in order without recursion.
310               if options[:svn]
311                 stack=[relative_path]
312                 until File.dirname(stack.last) == stack.last # dirname('.') == '.'
313                   stack.push File.dirname(stack.last)
314                 end
315                 stack.reverse_each do |rel_path|
316                   svn_path = destination_path(rel_path)
317                   system("svn add -N #{svn_path}") unless File.directory?(File.join(svn_path, '.svn'))
318                 end
319               end
320             end
321           end
322         end
324         # Display a README.
325         def readme(*relative_sources)
326           relative_sources.flatten.each do |relative_source|
327             logger.readme relative_source
328             puts File.read(source_path(relative_source)) unless options[:pretend]
329           end
330         end
332         # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
333         def migration_template(relative_source, relative_destination, template_options = {})
334           migration_directory relative_destination
335           migration_file_name = template_options[:migration_file_name] || file_name
336           raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name)
337           template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options)
338         end
340         def route_resources(*resources)
341           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
342           sentinel = 'ActionController::Routing::Routes.draw do |map|'
344           logger.route "map.resources #{resource_list}"
345           unless options[:pretend]
346             gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
347               "#{match}\n  map.resources #{resource_list}\n"
348             end
349           end
350         end
352         private
353           def render_file(path, options = {})
354             File.open(path, 'rb') do |file|
355               if block_given?
356                 yield file
357               else
358                 content = ''
359                 if shebang = options[:shebang]
360                   content << "#!#{shebang}\n"
361                   if line = file.gets
362                     content << "line\n" if line !~ /^#!/
363                   end
364                 end
365                 content << file.read
366               end
367             end
368           end
370           # Raise a usage error with an informative WordNet suggestion.
371           # Thanks to Florian Gross (flgr).
372           def raise_class_collision(class_name)
373             message = <<end_message
374   The name '#{class_name}' is reserved by Ruby on Rails.
375   Please choose an alternative and run this generator again.
376 end_message
377             if suggest = find_synonyms(class_name)
378               message << "\n  Suggestions:  \n\n"
379               message << suggest.join("\n")
380             end
381             raise UsageError, message
382           end
384           SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/cgi-bin/webwn2.0?stage=2&word=%s&posnumber=1&searchtypenumber=2&senses=&showglosses=1"
386           # Look up synonyms on WordNet.  Thanks to Florian Gross (flgr).
387           def find_synonyms(word)
388             require 'open-uri'
389             require 'timeout'
390             timeout(5) do
391               open(SYNONYM_LOOKUP_URI % word) do |stream|
392                 data = stream.read.gsub("&nbsp;", " ").gsub("<BR>", "")
393                 data.scan(/^Sense \d+\n.+?\n\n/m)
394               end
395             end
396           rescue Exception
397             return nil
398           end
399       end
402       # Undo the actions performed by a generator.  Rewind the action
403       # manifest and attempt to completely erase the results of each action.
404       class Destroy < RewindBase
405         # Remove a file if it exists and is a file.
406         def file(relative_source, relative_destination, file_options = {})
407           destination = destination_path(relative_destination)
408           if File.exists?(destination)
409             logger.rm relative_destination
410             unless options[:pretend]
411               if options[:svn]
412                 # If the file has been marked to be added
413                 # but has not yet been checked in, revert and delete
414                 if options[:svn][relative_destination]
415                   system("svn revert #{destination}")
416                   FileUtils.rm(destination)
417                 else
418                 # If the directory is not in the status list, it
419                 # has no modifications so we can simply remove it
420                   system("svn rm #{destination}")
421                 end  
422               else
423                 FileUtils.rm(destination)
424               end
425             end
426           else
427             logger.missing relative_destination
428             return
429           end
430         end
432         # Templates are deleted just like files and the actions take the
433         # same parameters, so simply alias the file method.
434         alias_method :template, :file
436         # Remove each directory in the given path from right to left.
437         # Remove each subdirectory if it exists and is a directory.
438         def directory(relative_path)
439           parts = relative_path.split('/')
440           until parts.empty?
441             partial = File.join(parts)
442             path = destination_path(partial)
443             if File.exists?(path)
444               if Dir[File.join(path, '*')].empty?
445                 logger.rmdir partial
446                 unless options[:pretend]
447                   if options[:svn]
448                     # If the directory has been marked to be added
449                     # but has not yet been checked in, revert and delete
450                     if options[:svn][relative_path]
451                       system("svn revert #{path}")
452                       FileUtils.rmdir(path)
453                     else
454                     # If the directory is not in the status list, it
455                     # has no modifications so we can simply remove it
456                       system("svn rm #{path}")
457                     end
458                   else
459                     FileUtils.rmdir(path)
460                   end
461                 end
462               else
463                 logger.notempty partial
464               end
465             else
466               logger.missing partial
467             end
468             parts.pop
469           end
470         end
472         def complex_template(*args)
473           # nothing should be done here
474         end
476         # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
477         def migration_template(relative_source, relative_destination, template_options = {})
478           migration_directory relative_destination
480           migration_file_name = template_options[:migration_file_name] || file_name
481           unless migration_exists?(migration_file_name)
482             puts "There is no migration named #{migration_file_name}"
483             return
484           end
487           existing_migrations(migration_file_name).each do |file_path|
488             file(relative_source, file_path, template_options)
489           end
490         end
492         def route_resources(*resources)
493           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
494           look_for = "\n  map.resources #{resource_list}\n"
495           logger.route "map.resources #{resource_list}"
496           gsub_file 'config/routes.rb', /(#{look_for})/mi, ''
497         end
498       end
501       # List a generator's action manifest.
502       class List < Base
503         def dependency(generator_name, args, options = {})
504           logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})"
505         end
507         def class_collisions(*class_names)
508           logger.class_collisions class_names.join(', ')
509         end
511         def file(relative_source, relative_destination, options = {})
512           logger.file relative_destination
513         end
515         def template(relative_source, relative_destination, options = {})
516           logger.template relative_destination
517         end
519         def complex_template(relative_source, relative_destination, options = {})
520           logger.template "#{options[:insert]} inside #{relative_destination}"
521         end
523         def directory(relative_path)
524           logger.directory "#{destination_path(relative_path)}/"
525         end
527         def readme(*args)
528           logger.readme args.join(', ')
529         end
530         
531         def migration_template(relative_source, relative_destination, options = {})
532           migration_directory relative_destination
533           logger.migration_template file_name
534         end
536         def route_resources(*resources)
537           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
538           logger.route "map.resources #{resource_list}"
539         end
540       end
542       # Update generator's action manifest.
543       class Update < Create
544         def file(relative_source, relative_destination, options = {})
545           # logger.file relative_destination
546         end
548         def template(relative_source, relative_destination, options = {})
549           # logger.template relative_destination
550         end
552         def complex_template(relative_source, relative_destination, template_options = {})
554            begin
555              dest_file = destination_path(relative_destination)
556              source_to_update = File.readlines(dest_file).join
557            rescue Errno::ENOENT
558              logger.missing relative_destination
559              return
560            end
562            logger.refreshing "#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"
564            begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id]))
565            end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id]))
567            # Refreshing inner part of the template with freshly rendered part.
568            rendered_part = render_template_part(template_options)
569            source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part)
571            File.open(dest_file, 'w') { |file| file.write(source_to_update) }
572         end
574         def directory(relative_path)
575           # logger.directory "#{destination_path(relative_path)}/"
576         end
577       end
579     end
580   end