Skip to content

Commit de8ee03

Browse files
authored
[CommandLine] Better report unknown subcommands (#74811)
The patch improves the reporting for the first option in the command line when it looks like a subcommand name but does not match any defined. Before the patch: ``` > prog baz prog: Unknown command line argument 'baz'. Try: 'prog --help' ``` With the patch: ``` > prog baz prog: Unknown subcommand 'baz'. Try: 'prog --help' prog: Did you mean 'bar'? ```
1 parent 67d7903 commit de8ee03

File tree

2 files changed

+81
-16
lines changed

2 files changed

+81
-16
lines changed

llvm/lib/Support/CommandLine.cpp

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ class CommandLineParser {
324324
return false;
325325
}
326326

327+
bool hasNamedSubCommands() const {
328+
for (const auto *S : RegisteredSubCommands)
329+
if (!S->getName().empty())
330+
return true;
331+
return false;
332+
}
333+
327334
SubCommand *getActiveSubCommand() { return ActiveSubCommand; }
328335

329336
void updateArgStr(Option *O, StringRef NewName, SubCommand *SC) {
@@ -425,7 +432,7 @@ class CommandLineParser {
425432
return nullptr;
426433
return Opt;
427434
}
428-
SubCommand *LookupSubCommand(StringRef Name);
435+
SubCommand *LookupSubCommand(StringRef Name, std::string &NearestString);
429436
};
430437

431438
} // namespace
@@ -550,9 +557,12 @@ Option *CommandLineParser::LookupOption(SubCommand &Sub, StringRef &Arg,
550557
return I->second;
551558
}
552559

553-
SubCommand *CommandLineParser::LookupSubCommand(StringRef Name) {
560+
SubCommand *CommandLineParser::LookupSubCommand(StringRef Name,
561+
std::string &NearestString) {
554562
if (Name.empty())
555563
return &SubCommand::getTopLevel();
564+
// Find a subcommand with the edit distance == 1.
565+
SubCommand *NearestMatch = nullptr;
556566
for (auto *S : RegisteredSubCommands) {
557567
if (S == &SubCommand::getAll())
558568
continue;
@@ -561,7 +571,14 @@ SubCommand *CommandLineParser::LookupSubCommand(StringRef Name) {
561571

562572
if (StringRef(S->getName()) == StringRef(Name))
563573
return S;
574+
575+
if (!NearestMatch && S->getName().edit_distance(Name) < 2)
576+
NearestMatch = S;
564577
}
578+
579+
if (NearestMatch)
580+
NearestString = NearestMatch->getName();
581+
565582
return &SubCommand::getTopLevel();
566583
}
567584

@@ -1527,10 +1544,14 @@ bool CommandLineParser::ParseCommandLineOptions(int argc,
15271544

15281545
int FirstArg = 1;
15291546
SubCommand *ChosenSubCommand = &SubCommand::getTopLevel();
1530-
if (argc >= 2 && argv[FirstArg][0] != '-') {
1547+
std::string NearestSubCommandString;
1548+
bool MaybeNamedSubCommand =
1549+
argc >= 2 && argv[FirstArg][0] != '-' && hasNamedSubCommands();
1550+
if (MaybeNamedSubCommand) {
15311551
// If the first argument specifies a valid subcommand, start processing
15321552
// options from the second argument.
1533-
ChosenSubCommand = LookupSubCommand(StringRef(argv[FirstArg]));
1553+
ChosenSubCommand =
1554+
LookupSubCommand(StringRef(argv[FirstArg]), NearestSubCommandString);
15341555
if (ChosenSubCommand != &SubCommand::getTopLevel())
15351556
FirstArg = 2;
15361557
}
@@ -1687,21 +1708,35 @@ bool CommandLineParser::ParseCommandLineOptions(int argc,
16871708
}
16881709

16891710
if (!Handler) {
1690-
if (SinkOpts.empty()) {
1691-
*Errs << ProgramName << ": Unknown command line argument '" << argv[i]
1692-
<< "'. Try: '" << argv[0] << " --help'\n";
1693-
1694-
if (NearestHandler) {
1695-
// If we know a near match, report it as well.
1696-
*Errs << ProgramName << ": Did you mean '"
1697-
<< PrintArg(NearestHandlerString, 0) << "'?\n";
1698-
}
1699-
1700-
ErrorParsing = true;
1701-
} else {
1711+
if (!SinkOpts.empty()) {
17021712
for (Option *SinkOpt : SinkOpts)
17031713
SinkOpt->addOccurrence(i, "", StringRef(argv[i]));
1714+
continue;
17041715
}
1716+
1717+
auto ReportUnknownArgument = [&](bool IsArg,
1718+
StringRef NearestArgumentName) {
1719+
*Errs << ProgramName << ": Unknown "
1720+
<< (IsArg ? "command line argument" : "subcommand") << " '"
1721+
<< argv[i] << "'. Try: '" << argv[0] << " --help'\n";
1722+
1723+
if (NearestArgumentName.empty())
1724+
return;
1725+
1726+
*Errs << ProgramName << ": Did you mean '";
1727+
if (IsArg)
1728+
*Errs << PrintArg(NearestArgumentName, 0);
1729+
else
1730+
*Errs << NearestArgumentName;
1731+
*Errs << "'?\n";
1732+
};
1733+
1734+
if (i > 1 || !MaybeNamedSubCommand)
1735+
ReportUnknownArgument(/*IsArg=*/true, NearestHandlerString);
1736+
else
1737+
ReportUnknownArgument(/*IsArg=*/false, NearestSubCommandString);
1738+
1739+
ErrorParsing = true;
17051740
continue;
17061741
}
17071742

llvm/unittests/Support/CommandLineTest.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,4 +2244,34 @@ TEST(CommandLineTest, HelpWithSubcommands) {
22442244
cl::ResetCommandLineParser();
22452245
}
22462246

2247+
TEST(CommandLineTest, UnknownCommands) {
2248+
cl::ResetCommandLineParser();
2249+
2250+
StackSubCommand SC1("foo", "Foo subcommand");
2251+
StackSubCommand SC2("bar", "Bar subcommand");
2252+
StackOption<bool> SC1Opt("put", cl::sub(SC1));
2253+
StackOption<bool> SC2Opt("get", cl::sub(SC2));
2254+
StackOption<bool> TopOpt1("peek");
2255+
StackOption<bool> TopOpt2("set");
2256+
2257+
std::string Errs;
2258+
raw_string_ostream OS(Errs);
2259+
2260+
const char *Args1[] = {"prog", "baz", "--get"};
2261+
EXPECT_FALSE(
2262+
cl::ParseCommandLineOptions(std::size(Args1), Args1, StringRef(), &OS));
2263+
EXPECT_EQ(Errs,
2264+
"prog: Unknown subcommand 'baz'. Try: 'prog --help'\n"
2265+
"prog: Did you mean 'bar'?\n"
2266+
"prog: Unknown command line argument '--get'. Try: 'prog --help'\n"
2267+
"prog: Did you mean '--set'?\n");
2268+
2269+
// Do not show a suggestion if the subcommand is not similar to any known.
2270+
Errs.clear();
2271+
const char *Args2[] = {"prog", "faz"};
2272+
EXPECT_FALSE(
2273+
cl::ParseCommandLineOptions(std::size(Args2), Args2, StringRef(), &OS));
2274+
EXPECT_EQ(Errs, "prog: Unknown subcommand 'faz'. Try: 'prog --help'\n");
2275+
}
2276+
22472277
} // anonymous namespace

0 commit comments

Comments
 (0)