Skip to content

Commit

Permalink
Merge pull request #630 from kddeisz/did-you-mean
Browse files Browse the repository at this point in the history
Support did-you-mean functionality in thor
  • Loading branch information
rafaelfranca committed Nov 9, 2018
2 parents e151a21 + 3019cb5 commit b51916c
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 11 deletions.
3 changes: 1 addition & 2 deletions lib/thor/base.rb
Expand Up @@ -493,8 +493,7 @@ def public_command(*names)
alias_method :public_task, :public_command

def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc:
raise UndefinedCommandError, "Could not find command #{command.inspect} in #{namespace.inspect} namespace." if has_namespace
raise UndefinedCommandError, "Could not find command #{command.inspect}."
raise UndefinedCommandError.new(command, all_commands.keys, (namespace if has_namespace))
end
alias_method :handle_no_task_error, :handle_no_command_error

Expand Down
85 changes: 85 additions & 0 deletions lib/thor/error.rb
@@ -1,4 +1,25 @@
class Thor
Correctable =
begin
require 'did_you_mean'

module DidYouMean
# In order to support versions of Ruby that don't have keyword
# arguments, we need our own spell checker class that doesn't take key
# words. Even though this code wouldn't be hit because of the check
# above, it's still necessary because the interpreter would otherwise be
# unable to parse the file.
class NoKwargSpellChecker < SpellChecker
def initialize(dictionary)
@dictionary = dictionary
end
end
end

DidYouMean::Correctable
rescue LoadError
end

# Thor::Error is raised when it's caused by wrong usage of thor classes. Those
# errors have their backtrace suppressed and are nicely shown to the user.
#
Expand All @@ -10,6 +31,35 @@ class Error < StandardError

# Raised when a command was not found.
class UndefinedCommandError < Error
class SpellChecker
attr_reader :error

def initialize(error)
@error = error
end

def corrections
@corrections ||= spell_checker.correct(error.command).map(&:inspect)
end

def spell_checker
DidYouMean::NoKwargSpellChecker.new(error.all_commands)
end
end

attr_reader :command, :all_commands

def initialize(command, all_commands, namespace)
@command = command
@all_commands = all_commands

message = "Could not find command #{command.inspect}"
message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}."

super(message)
end

prepend Correctable if Correctable
end
UndefinedTaskError = UndefinedCommandError

Expand All @@ -22,11 +72,46 @@ class InvocationError < Error
end

class UnknownArgumentError < Error
class SpellChecker
attr_reader :error

def initialize(error)
@error = error
end

def corrections
@corrections ||=
error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect)
end

def spell_checker
@spell_checker ||=
DidYouMean::NoKwargSpellChecker.new(error.switches)
end
end

attr_reader :switches, :unknown

def initialize(switches, unknown)
@switches = switches
@unknown = unknown

super("Unknown switches #{unknown.map(&:inspect).join(', ')}")
end

prepend Correctable if Correctable
end

class RequiredArgumentMissingError < InvocationError
end

class MalformattedArgumentError < InvocationError
end

if Correctable
DidYouMean::SPELL_CHECKERS.merge!(
'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
)
end
end
2 changes: 1 addition & 1 deletion lib/thor/parser/options.rb
Expand Up @@ -127,7 +127,7 @@ def check_unknown!

# an unknown option starts with - or -- and has no more --'s afterward.
unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty?
end

protected
Expand Down
9 changes: 8 additions & 1 deletion spec/base_spec.rb
Expand Up @@ -262,6 +262,13 @@ def hello
end.to raise_error(Thor::UndefinedCommandError, 'Could not find command "what" in "my_script" namespace.')
end

it "suggests commands that are similar if there is a typo" do
expected = "Could not find command \"paintz\" in \"barn\" namespace.\n"
expected << "Did you mean? \"paint\"" if Thor::Correctable

expect(capture(:stderr) { Barn.start(%w(paintz)) }).to eq(expected)
end

it "does not steal args" do
args = %w(foo bar --force true)
MyScript.start(args)
Expand All @@ -271,7 +278,7 @@ def hello
it "checks unknown options" do
expect(capture(:stderr) do
MyScript.start(%w(foo bar --force true --unknown baz))
end.strip).to eq("Unknown switches '--unknown'")
end.strip).to eq("Unknown switches \"--unknown\"")
end

it "checks unknown options except specified" do
Expand Down
6 changes: 5 additions & 1 deletion spec/parser/options_spec.rb
Expand Up @@ -113,7 +113,11 @@ def remaining
it "raises an error for unknown switches" do
create :foo => "baz", :bar => :required
parse("--bar", "baz", "--baz", "unknown")
expect { check_unknown! }.to raise_error(Thor::UnknownArgumentError, "Unknown switches '--baz'")

expected = "Unknown switches \"--baz\""
expected << "\nDid you mean? \"--bar\"" if Thor::Correctable

expect { check_unknown! }.to raise_error(Thor::UnknownArgumentError, expected)
end

it "skips leading non-switches" do
Expand Down
12 changes: 6 additions & 6 deletions spec/thor_spec.rb
Expand Up @@ -182,7 +182,7 @@ def exec(*args)
it "does not accept if first non-option looks like an option, but only refuses that invalid option" do
expect(capture(:stderr) do
my_script2.start(%w[exec --foo command --bar])
end.strip).to eq("Unknown switches '--foo'")
end.strip).to eq("Unknown switches \"--foo\"")
end

it "still accepts options that are given before non-options" do
Expand All @@ -196,7 +196,7 @@ def exec(*args)
it "does not accept when non-option looks like an option and is after real options" do
expect(capture(:stderr) do
my_script2.start(%w[exec --verbose --foo])
end.strip).to eq("Unknown switches '--foo'")
end.strip).to eq("Unknown switches \"--foo\"")
end

it "still accepts options that require a value" do
Expand Down Expand Up @@ -236,25 +236,25 @@ def checked(*args)
it "does not accept if non-option that looks like an option is before the arguments" do
expect(capture(:stderr) do
my_script.start(%w[checked --foo command --bar])
end.strip).to eq("Unknown switches '--foo, --bar'")
end.strip).to eq("Unknown switches \"--foo\", \"--bar\"")
end

it "does not accept if non-option that looks like an option is after an argument" do
expect(capture(:stderr) do
my_script.start(%w[checked command --foo --bar])
end.strip).to eq("Unknown switches '--foo, --bar'")
end.strip).to eq("Unknown switches \"--foo\", \"--bar\"")
end

it "does not accept when non-option that looks like an option is after real options" do
expect(capture(:stderr) do
my_script.start(%w[checked --verbose --foo])
end.strip).to eq("Unknown switches '--foo'")
end.strip).to eq("Unknown switches \"--foo\"")
end

it "does not accept when non-option that looks like an option is before real options" do
expect(capture(:stderr) do
my_script.start(%w[checked --foo --verbose])
end.strip).to eq("Unknown switches '--foo'")
end.strip).to eq("Unknown switches \"--foo\"")
end

it "still accepts options that require a value" do
Expand Down

0 comments on commit b51916c

Please sign in to comment.