diff --git a/lib/rake/task_manager.rb b/lib/rake/task_manager.rb index cbb9f5ee2..78f4b83fb 100644 --- a/lib/rake/task_manager.rb +++ b/lib/rake/task_manager.rb @@ -59,9 +59,29 @@ def [](task_name, scopes=nil) self.lookup(task_name, scopes) or enhance_with_matching_rule(task_name) or synthesize_file_task(task_name) or - fail "Don't know how to build task '#{task_name}'" + build_task_fail(task_name) end + # If a task match wasn't found, try to offer suggestions. + # The task name must be at least 2 characters. + def build_task_fail(task_name) + msg = "Don't know how to build task '#{task_name}'" + threshold = (task_name.size * 0.3).ceil + + matches = @tasks.keys. + map {|name| [levenshtein_distance(name.downcase, task_name), name] }. + select {|distance, _| distance <= threshold }. + sort. + map(&:last) + + if matches.size > 0 + msg += ".\nDid you mean one of these? #{matches.join(", ")}" + end + + fail msg + end + private :build_task_fail + def synthesize_file_task(task_name) # :nodoc: return nil unless File.exist?(task_name) define_task(Rake::FileTask, task_name) @@ -301,6 +321,36 @@ def get_description(task) desc end + # Return a value representing the "cost" of transforming str1 into str2 + def levenshtein_distance(str1, str2) + n = str1.length + m = str2.length + return m if n.zero? + return n if m.zero? + + 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 + + x + end + class << self attr_accessor :record_task_metadata # :nodoc: TaskManager.record_task_metadata = false diff --git a/test/test_rake_task.rb b/test/test_rake_task.rb index 0416dfa8d..634806ac3 100644 --- a/test/test_rake_task.rb +++ b/test/test_rake_task.rb @@ -148,6 +148,14 @@ def test_find assert_equal "Don't know how to build task 'leaves'", ex.message end + def test_find_with_alts + task :tfind + ex = Task[:tfindx] rescue $! + + assert_match /Don\'t know how to build task \'tfindx\'/, ex.message + assert_match /Did you mean one of these\? tfind/, ex.message + end + def test_defined assert ! Task.task_defined?(:a) task :a