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.
This commit is contained in:
schneems 2014-06-03 18:22:45 -05:00
parent 3036c4031a
commit 72f45ba292
3 changed files with 63 additions and 9 deletions

View File

@ -1,3 +1,7 @@
* Invalid `bin/rails generate` commands will now show spelling suggestions.
*Richard Schneeman*
* Add `bin/setup` script to bootstrap an application.
*Yves Senn*

View File

@ -156,8 +156,12 @@ module Rails
args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
klass.start(args, config)
else
puts "Could not find generator '#{namespace}'. Please choose a generator below."
print_generators
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
@ -220,31 +224,71 @@ module Rails
print_generators
end
def self.print_generators
def self.public_namespaces
lookup!
subclasses.map { |k| k.namespace }
end
namespaces = subclasses.map{ |k| k.namespace }
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
# Print Rails defaults first.
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, '') }
rails.delete("app")
rails.delete("plugin")
print_list("rails", rails)
hidden_namespaces.each { |n| groups.delete(n.to_s) }
groups.sort.each { |b, n| print_list(b, n) }
[["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|

View File

@ -24,7 +24,13 @@ class GeneratorsTest < Rails::Generators::TestCase
name = :unknown
output = capture(:stdout){ Rails::Generators.invoke name }
assert_match "Could not find generator '#{name}'", output
assert_match "scaffold", output
assert_match "`rails generate --help`", output
end
def test_generator_suggestions
name = :migrationz
output = capture(:stdout){ Rails::Generators.invoke name }
assert_match "Maybe you meant 'migration'", output
end
def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments