1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/railties/lib/rails/generators.rb
schneems 72f45ba292 Emit suggested generator names when not found
When someone types in a generator command it currently outputs all generators. Instead we can attempt to find a subtle mis-spelling by running all generator names through a levenshtein_distance algorithm provided by rubygems. 

So now a failure looks like this:

```ruby
$ rails generate migratioooons
Could not find generator 'migratioooons'. Maybe you meant 'migration' or 'integration_test' or 'generator'
Run `rails generate --help` for more options.
```

If the suggestions are bad we leave the user with the hint to run `rails generate --help` to see all commands.
2014-06-04 16:28:43 -05:00

373 lines
11 KiB
Ruby

activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require 'thor/group'
require 'active_support'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/inflections'
module Rails
module Generators
autoload :Actions, 'rails/generators/actions'
autoload :ActiveModel, 'rails/generators/active_model'
autoload :Base, 'rails/generators/base'
autoload :Migration, 'rails/generators/migration'
autoload :NamedBase, 'rails/generators/named_base'
autoload :ResourceHelpers, 'rails/generators/resource_helpers'
autoload :TestCase, 'rails/generators/test_case'
mattr_accessor :namespace
DEFAULT_ALIASES = {
rails: {
actions: '-a',
orm: '-o',
javascripts: '-j',
javascript_engine: '-je',
resource_controller: '-c',
scaffold_controller: '-c',
stylesheets: '-y',
stylesheet_engine: '-se',
template_engine: '-e',
test_framework: '-t'
},
test_unit: {
fixture_replacement: '-r',
}
}
DEFAULT_OPTIONS = {
rails: {
assets: true,
force_plural: false,
helper: true,
integration_tool: nil,
javascripts: true,
javascript_engine: :js,
orm: false,
resource_controller: :controller,
resource_route: true,
scaffold_controller: :scaffold_controller,
stylesheets: true,
stylesheet_engine: :css,
test_framework: false,
template_engine: :erb
}
}
def self.configure!(config) #:nodoc:
no_color! unless config.colorize_logging
aliases.deep_merge! config.aliases
options.deep_merge! config.options
fallbacks.merge! config.fallbacks
templates_path.concat config.templates
templates_path.uniq!
hide_namespaces(*config.hidden_namespaces)
end
def self.templates_path #:nodoc:
@templates_path ||= []
end
def self.aliases #:nodoc:
@aliases ||= DEFAULT_ALIASES.dup
end
def self.options #:nodoc:
@options ||= DEFAULT_OPTIONS.dup
end
# Hold configured generators fallbacks. If a plugin developer wants a
# generator group to fallback to another group in case of missing generators,
# they can add a fallback.
#
# For example, shoulda is considered a test_framework and is an extension
# of test_unit. However, most part of shoulda generators are similar to
# test_unit ones.
#
# Shoulda then can tell generators to search for test_unit generators when
# some of them are not available by adding a fallback:
#
# Rails::Generators.fallbacks[:shoulda] = :test_unit
def self.fallbacks
@fallbacks ||= {}
end
# Remove the color from output.
def self.no_color!
Thor::Base.shell = Thor::Shell::Basic
end
# Track all generators subclasses.
def self.subclasses
@subclasses ||= []
end
# Rails finds namespaces similar to thor, it only adds one rule:
#
# Generators names must end with "_generator.rb". This is required because Rails
# looks in load paths and loads the generator just before it's going to be used.
#
# find_by_namespace :webrat, :rails, :integration
#
# Will search for the following generators:
#
# "rails:webrat", "webrat:integration", "webrat"
#
# Notice that "rails:generators:webrat" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace.
def self.find_by_namespace(name, base=nil, context=nil) #:nodoc:
lookups = []
lookups << "#{base}:#{name}" if base
lookups << "#{name}:#{context}" if context
unless base || context
unless name.to_s.include?(?:)
lookups << "#{name}:#{name}"
lookups << "rails:#{name}"
end
lookups << "#{name}"
end
lookup(lookups)
namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
lookups.each do |namespace|
klass = namespaces[namespace]
return klass if klass
end
invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
end
# Receives a namespace, arguments and the behavior to invoke the generator.
# It's used as the default entry point for generate, destroy and update
# commands.
def self.invoke(namespace, args=ARGV, config={})
names = namespace.to_s.split(':')
if klass = find_by_namespace(names.pop, names.any? && names.join(':'))
args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
klass.start(args, config)
else
options = sorted_groups.map(&:last).flatten
suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
msg = "Could not find generator '#{namespace}'. "
msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.join(" or ") }\n"
msg << "Run `rails generate --help` for more options."
puts msg
end
end
# Returns an array of generator namespaces that are hidden.
# Generator namespaces may be hidden for a variety of reasons.
# Some are aliased such as "rails:migration" and can be
# invoked with the shorter "migration", others are private to other generators
# such as "css:scaffold".
def self.hidden_namespaces
@hidden_namespaces ||= begin
orm = options[:rails][:orm]
test = options[:rails][:test_framework]
template = options[:rails][:template_engine]
css = options[:rails][:stylesheet_engine]
[
"rails",
"resource_route",
"#{orm}:migration",
"#{orm}:model",
"#{test}:controller",
"#{test}:helper",
"#{test}:integration",
"#{test}:mailer",
"#{test}:model",
"#{test}:scaffold",
"#{test}:view",
"#{template}:controller",
"#{template}:scaffold",
"#{template}:mailer",
"#{css}:scaffold",
"#{css}:assets",
"css:assets",
"css:scaffold"
]
end
end
class << self
def hide_namespaces(*namespaces)
hidden_namespaces.concat(namespaces)
end
alias hide_namespace hide_namespaces
end
# Show help message with available generators.
def self.help(command = 'generate')
puts "Usage: rails #{command} GENERATOR [args] [options]"
puts
puts "General options:"
puts " -h, [--help] # Print generator's options and usage"
puts " -p, [--pretend] # Run but do not make any changes"
puts " -f, [--force] # Overwrite files that already exist"
puts " -s, [--skip] # Skip files that already exist"
puts " -q, [--quiet] # Suppress status output"
puts
puts "Please choose a generator below."
puts
print_generators
end
def self.public_namespaces
lookup!
subclasses.map { |k| k.namespace }
end
def self.print_generators
sorted_groups.each { |b, n| print_list(b, n) }
end
def self.sorted_groups
namespaces = public_namespaces
namespaces.sort!
groups = Hash.new { |h,k| h[k] = [] }
namespaces.each do |namespace|
base = namespace.split(':').first
groups[base] << namespace
end
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, '') }
rails.delete("app")
rails.delete("plugin")
hidden_namespaces.each { |n| groups.delete(n.to_s) }
[["rails", rails]] + groups.sort.to_a
end
protected
# This code is based directly on the Text gem implementation
# Returns a value representing the "cost" of transforming str1 into str2
def self.levenshtein_distance str1, str2
s = str1
t = str2
n = s.length
m = t.length
max = n/2
return m if (0 == n)
return n if (0 == m)
return n if (n - m).abs > max
d = (0..m).to_a
x = nil
str1.each_char.each_with_index do |char1,i|
e = i+1
str2.each_char.each_with_index do |char2,j|
cost = (char1 == char2) ? 0 : 1
x = [
d[j+1] + 1, # insertion
e + 1, # deletion
d[j] + cost # substitution
].min
d[j] = e
e = x
end
d[m] = x
end
return x
end
# Prints a list of generators.
def self.print_list(base, namespaces) #:nodoc:
namespaces = namespaces.reject do |n|
hidden_namespaces.include?(n)
end
return if namespaces.empty?
puts "#{base.camelize}:"
namespaces.each do |namespace|
puts(" #{namespace}")
end
puts
end
# Try fallbacks for the given base.
def self.invoke_fallbacks_for(name, base) #:nodoc:
return nil unless base && fallbacks[base.to_sym]
invoked_fallbacks = []
Array(fallbacks[base.to_sym]).each do |fallback|
next if invoked_fallbacks.include?(fallback)
invoked_fallbacks << fallback
klass = find_by_namespace(name, fallback)
return klass if klass
end
nil
end
# Receives namespaces in an array and tries to find matching generators
# in the load path.
def self.lookup(namespaces) #:nodoc:
paths = namespaces_to_paths(namespaces)
paths.each do |raw_path|
["rails/generators", "generators"].each do |base|
path = "#{base}/#{raw_path}_generator"
begin
require path
return
rescue LoadError => e
raise unless e.message =~ /#{Regexp.escape(path)}$/
rescue Exception => e
warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
end
end
end
end
# This will try to load any generator in the load path to show in help.
def self.lookup! #:nodoc:
$LOAD_PATH.each do |base|
Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path|
begin
path = path.sub("#{base}/", "")
require path
rescue Exception
# No problem
end
end
end
end
# Convert namespaces to paths by replacing ":" for "/" and adding
# an extra lookup. For example, "rails:model" should be searched
# in both: "rails/model/model_generator" and "rails/model_generator".
def self.namespaces_to_paths(namespaces) #:nodoc:
paths = []
namespaces.each do |namespace|
pieces = namespace.split(":")
paths << pieces.dup.push(pieces.last).join("/")
paths << pieces.join("/")
end
paths.uniq!
paths
end
end
end