Skip to content

Commit f359eab

Browse files
Add spell suggestions to parse_options_baset
This will provide some suggestions if a command line option is unknown, but similar to an option that *does* exist.
1 parent 15b3eec commit f359eab

File tree

4 files changed

+90
-1
lines changed

4 files changed

+90
-1
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CORE
2+
dummy.c
3+
--traec
4+
did you mean --trace
5+
^EXIT=64$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This checks that we get a useful suggestion when we make a typo on the
10+
commandline

src/util/cmdline.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Author: Daniel Kroening, [email protected]
88

99
#include "cmdline.h"
1010

11+
#include <util/edit_distance.h>
1112
#include <util/exception_utils.h>
1213
#include <util/invariant.h>
1314

@@ -250,6 +251,66 @@ cmdlinet::option_namest cmdlinet::option_names() const
250251
return option_namest{*this};
251252
}
252253

254+
std::vector<std::string>
255+
cmdlinet::get_argument_suggestions(const std::string &unknown_argument)
256+
{
257+
struct suggestiont
258+
{
259+
std::size_t distance;
260+
std::string suggestion;
261+
262+
bool operator<(const suggestiont &other) const
263+
{
264+
return distance < other.distance;
265+
}
266+
};
267+
268+
auto argument_suggestions = std::vector<suggestiont>{};
269+
// We allow 3 errors here. This can lead to the output being a bit chatty,
270+
// which we mitigate by reducing suggestions to those with the minimum distance
271+
// further down below
272+
const auto argument_matcher = levenshtein_automatont{unknown_argument, 3};
273+
for(const auto &option : options)
274+
{
275+
if(option.islong)
276+
{
277+
const auto long_name = "--" + option.optstring;
278+
if(auto distance = argument_matcher.get_edit_distance(long_name))
279+
{
280+
argument_suggestions.push_back({distance.value(), long_name});
281+
}
282+
}
283+
if(!option.islong)
284+
{
285+
const auto short_name = std::string{"-"} + option.optchar;
286+
if(auto distance = argument_matcher.get_edit_distance(short_name))
287+
{
288+
argument_suggestions.push_back({distance.value(), short_name});
289+
}
290+
}
291+
}
292+
293+
auto final_suggestions = std::vector<std::string>{};
294+
if(!argument_suggestions.empty())
295+
{
296+
// we only want to keep suggestions with the minimum distance
297+
// because otherwise they become quickly too noisy to be useful
298+
auto min = std::min_element(
299+
argument_suggestions.begin(), argument_suggestions.end());
300+
INVARIANT(
301+
min != argument_suggestions.end(),
302+
"there is a minimum because it's not empty");
303+
for(auto const &suggestion : argument_suggestions)
304+
{
305+
if(suggestion.distance == min->distance)
306+
{
307+
final_suggestions.push_back(suggestion.suggestion);
308+
}
309+
}
310+
}
311+
return final_suggestions;
312+
}
313+
253314
cmdlinet::option_namest::option_names_iteratort::option_names_iteratort(
254315
const cmdlinet *command_line,
255316
std::size_t index)

src/util/cmdline.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ class cmdlinet
9494
cmdlinet();
9595
virtual ~cmdlinet();
9696

97+
std::vector<std::string>
98+
get_argument_suggestions(const std::string &unknown_argument);
99+
97100
protected:
98101
struct optiont
99102
{

src/util/parse_options.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,22 @@ void parse_options_baset::usage_error()
5555
void parse_options_baset::unknown_option_msg()
5656
{
5757
if(!cmdline.unknown_arg.empty())
58-
log.error() << "Unknown option: " << cmdline.unknown_arg << messaget::eom;
58+
{
59+
log.error() << "Unknown option: " << cmdline.unknown_arg;
60+
auto const suggestions =
61+
cmdline.get_argument_suggestions(cmdline.unknown_arg);
62+
if(!suggestions.empty())
63+
{
64+
log.error() << ", did you mean ";
65+
if(suggestions.size() > 1)
66+
{
67+
log.error() << "one of ";
68+
}
69+
join_strings(log.error(), suggestions.begin(), suggestions.end(), ", ");
70+
log.error() << "?";
71+
}
72+
log.error() << messaget::eom;
73+
}
5974
}
6075

6176
int parse_options_baset::main()

0 commit comments

Comments
 (0)