Skip to content

Commit b51916c

Browse files
authored
Merge pull request #630 from kddeisz/did-you-mean
Support did-you-mean functionality in thor
2 parents e151a21 + 3019cb5 commit b51916c

File tree

6 files changed

+106
-11
lines changed

6 files changed

+106
-11
lines changed

lib/thor/base.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,7 @@ def public_command(*names)
493493
alias_method :public_task, :public_command
494494

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

lib/thor/error.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,25 @@
11
class Thor
2+
Correctable =
3+
begin
4+
require 'did_you_mean'
5+
6+
module DidYouMean
7+
# In order to support versions of Ruby that don't have keyword
8+
# arguments, we need our own spell checker class that doesn't take key
9+
# words. Even though this code wouldn't be hit because of the check
10+
# above, it's still necessary because the interpreter would otherwise be
11+
# unable to parse the file.
12+
class NoKwargSpellChecker < SpellChecker
13+
def initialize(dictionary)
14+
@dictionary = dictionary
15+
end
16+
end
17+
end
18+
19+
DidYouMean::Correctable
20+
rescue LoadError
21+
end
22+
223
# Thor::Error is raised when it's caused by wrong usage of thor classes. Those
324
# errors have their backtrace suppressed and are nicely shown to the user.
425
#
@@ -10,6 +31,35 @@ class Error < StandardError
1031

1132
# Raised when a command was not found.
1233
class UndefinedCommandError < Error
34+
class SpellChecker
35+
attr_reader :error
36+
37+
def initialize(error)
38+
@error = error
39+
end
40+
41+
def corrections
42+
@corrections ||= spell_checker.correct(error.command).map(&:inspect)
43+
end
44+
45+
def spell_checker
46+
DidYouMean::NoKwargSpellChecker.new(error.all_commands)
47+
end
48+
end
49+
50+
attr_reader :command, :all_commands
51+
52+
def initialize(command, all_commands, namespace)
53+
@command = command
54+
@all_commands = all_commands
55+
56+
message = "Could not find command #{command.inspect}"
57+
message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}."
58+
59+
super(message)
60+
end
61+
62+
prepend Correctable if Correctable
1363
end
1464
UndefinedTaskError = UndefinedCommandError
1565

@@ -22,11 +72,46 @@ class InvocationError < Error
2272
end
2373

2474
class UnknownArgumentError < Error
75+
class SpellChecker
76+
attr_reader :error
77+
78+
def initialize(error)
79+
@error = error
80+
end
81+
82+
def corrections
83+
@corrections ||=
84+
error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect)
85+
end
86+
87+
def spell_checker
88+
@spell_checker ||=
89+
DidYouMean::NoKwargSpellChecker.new(error.switches)
90+
end
91+
end
92+
93+
attr_reader :switches, :unknown
94+
95+
def initialize(switches, unknown)
96+
@switches = switches
97+
@unknown = unknown
98+
99+
super("Unknown switches #{unknown.map(&:inspect).join(', ')}")
100+
end
101+
102+
prepend Correctable if Correctable
25103
end
26104

27105
class RequiredArgumentMissingError < InvocationError
28106
end
29107

30108
class MalformattedArgumentError < InvocationError
31109
end
110+
111+
if Correctable
112+
DidYouMean::SPELL_CHECKERS.merge!(
113+
'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
114+
'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
115+
)
116+
end
32117
end

lib/thor/parser/options.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def check_unknown!
127127

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

133133
protected

spec/base_spec.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ def hello
262262
end.to raise_error(Thor::UndefinedCommandError, 'Could not find command "what" in "my_script" namespace.')
263263
end
264264

265+
it "suggests commands that are similar if there is a typo" do
266+
expected = "Could not find command \"paintz\" in \"barn\" namespace.\n"
267+
expected << "Did you mean? \"paint\"" if Thor::Correctable
268+
269+
expect(capture(:stderr) { Barn.start(%w(paintz)) }).to eq(expected)
270+
end
271+
265272
it "does not steal args" do
266273
args = %w(foo bar --force true)
267274
MyScript.start(args)
@@ -271,7 +278,7 @@ def hello
271278
it "checks unknown options" do
272279
expect(capture(:stderr) do
273280
MyScript.start(%w(foo bar --force true --unknown baz))
274-
end.strip).to eq("Unknown switches '--unknown'")
281+
end.strip).to eq("Unknown switches \"--unknown\"")
275282
end
276283

277284
it "checks unknown options except specified" do

spec/parser/options_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ def remaining
113113
it "raises an error for unknown switches" do
114114
create :foo => "baz", :bar => :required
115115
parse("--bar", "baz", "--baz", "unknown")
116-
expect { check_unknown! }.to raise_error(Thor::UnknownArgumentError, "Unknown switches '--baz'")
116+
117+
expected = "Unknown switches \"--baz\""
118+
expected << "\nDid you mean? \"--bar\"" if Thor::Correctable
119+
120+
expect { check_unknown! }.to raise_error(Thor::UnknownArgumentError, expected)
117121
end
118122

119123
it "skips leading non-switches" do

spec/thor_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def exec(*args)
182182
it "does not accept if first non-option looks like an option, but only refuses that invalid option" do
183183
expect(capture(:stderr) do
184184
my_script2.start(%w[exec --foo command --bar])
185-
end.strip).to eq("Unknown switches '--foo'")
185+
end.strip).to eq("Unknown switches \"--foo\"")
186186
end
187187

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

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

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

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

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

260260
it "still accepts options that require a value" do

0 commit comments

Comments
 (0)