Skip to content

Commit 92b8b03

Browse files
authored
Merge pull request #23064 from andrew/command-did-you-mean
Suggest commands for typos
2 parents 586f1c2 + af3973f commit 92b8b03

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

Library/Homebrew/brew.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
ENV["HOMEBREW_LIBRARY_PATH"] = HOMEBREW_LIBRARY_PATH.to_s
144144
exec external_cmd_path.to_s, *ARGV
145145
else
146-
raise UsageError, "Unknown command: brew #{cmd}"
146+
raise UsageError, "Unknown command: brew #{cmd}#{Commands.suggestion_message(cmd)}"
147147
end
148148
rescue UsageError => e
149149
require "help"

Library/Homebrew/commands.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ def self.commands(external: true, aliases: false)
137137
cmds.sort
138138
end
139139

140+
sig { params(cmd: String).returns(String) }
141+
def self.suggestion_message(cmd)
142+
require "did_you_mean"
143+
144+
suggestions = DidYouMean::SpellChecker.new(dictionary: commands(external: false, aliases: true)).correct(cmd)
145+
suggestions = DidYouMean::SpellChecker.new(dictionary: commands(aliases: true)).correct(cmd) if suggestions.empty?
146+
return "" if suggestions.empty?
147+
148+
"\nDid you mean #{suggestions.to_sentence(two_words_connector: " or ", last_word_connector: " or ")}?"
149+
end
150+
140151
# An array of all tap cmd directory {Pathname}s.
141152
sig { returns(T::Array[Pathname]) }
142153
def self.tap_cmd_directories

Library/Homebrew/test/commands_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,32 @@
8686
end
8787
end
8888

89+
describe "::suggestion_message" do
90+
let(:internal_commands) { %w[doctor up upgrade] }
91+
let(:all_commands) { %w[doctor external-command up upgrade] }
92+
93+
before do
94+
allow(described_class).to receive(:commands).with(external: false, aliases: true).and_return(internal_commands)
95+
allow(described_class).to receive(:commands).with(aliases: true).and_return(all_commands)
96+
end
97+
98+
it "suggests a command for a typo" do
99+
expect(described_class.suggestion_message("upgrde")).to eq("\nDid you mean upgrade?")
100+
end
101+
102+
it "suggests a command alias for a typo" do
103+
expect(described_class.suggestion_message("upp")).to eq("\nDid you mean up?")
104+
end
105+
106+
it "falls back to external command suggestions" do
107+
expect(described_class.suggestion_message("external-comand")).to eq("\nDid you mean external-command?")
108+
end
109+
110+
it "does not suggest a command without a close match" do
111+
expect(described_class.suggestion_message("zzzzzz")).to be_empty
112+
end
113+
end
114+
89115
describe "::rebuild_internal_commands_completion_list" do
90116
it "omits internal command aliases" do
91117
mktmpdir do |repository|

0 commit comments

Comments
 (0)