From 6cb77507475f3ca9a27622e5952728e928340172 Mon Sep 17 00:00:00 2001 From: Igor Kudrin Date: Thu, 7 Dec 2023 22:27:37 -0800 Subject: [PATCH 1/2] [CommandLine] Better report unknown subcommands 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'? ``` --- llvm/lib/Support/CommandLine.cpp | 67 ++++++++++++++++------ llvm/unittests/Support/CommandLineTest.cpp | 30 ++++++++++ 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/llvm/lib/Support/CommandLine.cpp b/llvm/lib/Support/CommandLine.cpp index 31f79972125da..e894937288412 100644 --- a/llvm/lib/Support/CommandLine.cpp +++ b/llvm/lib/Support/CommandLine.cpp @@ -324,6 +324,13 @@ class CommandLineParser { return false; } + bool hasNamedSubCommands() const { + for (const auto *S : RegisteredSubCommands) + if (!S->getName().empty()) + return true; + return false; + } + SubCommand *getActiveSubCommand() { return ActiveSubCommand; } void updateArgStr(Option *O, StringRef NewName, SubCommand *SC) { @@ -425,7 +432,7 @@ class CommandLineParser { return nullptr; return Opt; } - SubCommand *LookupSubCommand(StringRef Name); + SubCommand *LookupSubCommand(StringRef Name, std::string &NearestString); }; } // namespace @@ -550,9 +557,12 @@ Option *CommandLineParser::LookupOption(SubCommand &Sub, StringRef &Arg, return I->second; } -SubCommand *CommandLineParser::LookupSubCommand(StringRef Name) { +SubCommand *CommandLineParser::LookupSubCommand(StringRef Name, + std::string &NearestString) { if (Name.empty()) return &SubCommand::getTopLevel(); + // Find a subcommand with the edit distance == 1. + SubCommand *NearestMatch = nullptr; for (auto *S : RegisteredSubCommands) { if (S == &SubCommand::getAll()) continue; @@ -561,7 +571,14 @@ SubCommand *CommandLineParser::LookupSubCommand(StringRef Name) { if (StringRef(S->getName()) == StringRef(Name)) return S; + + if (!NearestMatch && S->getName().edit_distance(Name) < 2) + NearestMatch = S; } + + if (NearestMatch) + NearestString = NearestMatch->getName(); + return &SubCommand::getTopLevel(); } @@ -1527,10 +1544,14 @@ bool CommandLineParser::ParseCommandLineOptions(int argc, int FirstArg = 1; SubCommand *ChosenSubCommand = &SubCommand::getTopLevel(); - if (argc >= 2 && argv[FirstArg][0] != '-') { + std::string NearestSubCommandString; + bool MaybeNamedSubCommand = + argc >= 2 && argv[FirstArg][0] != '-' && hasNamedSubCommands(); + if (MaybeNamedSubCommand) { // If the first argument specifies a valid subcommand, start processing // options from the second argument. - ChosenSubCommand = LookupSubCommand(StringRef(argv[FirstArg])); + ChosenSubCommand = + LookupSubCommand(StringRef(argv[FirstArg]), NearestSubCommandString); if (ChosenSubCommand != &SubCommand::getTopLevel()) FirstArg = 2; } @@ -1687,21 +1708,35 @@ bool CommandLineParser::ParseCommandLineOptions(int argc, } if (!Handler) { - if (SinkOpts.empty()) { - *Errs << ProgramName << ": Unknown command line argument '" << argv[i] - << "'. Try: '" << argv[0] << " --help'\n"; - - if (NearestHandler) { - // If we know a near match, report it as well. - *Errs << ProgramName << ": Did you mean '" - << PrintArg(NearestHandlerString, 0) << "'?\n"; - } - - ErrorParsing = true; - } else { + if (!SinkOpts.empty()) { for (Option *SinkOpt : SinkOpts) SinkOpt->addOccurrence(i, "", StringRef(argv[i])); + continue; } + + auto reportUnknownArgument = [&](bool IsArg, + StringRef NearestArgumentName) { + *Errs << ProgramName << ": Unknown " + << (IsArg ? "command line argument" : "subcommand") << " '" + << argv[i] << "'. Try: '" << argv[0] << " --help'\n"; + + if (NearestArgumentName.empty()) + return; + + *Errs << ProgramName << ": Did you mean '"; + if (IsArg) + *Errs << PrintArg(NearestArgumentName, 0); + else + *Errs << NearestArgumentName; + *Errs << "'?\n"; + }; + + if (i > 1 || !MaybeNamedSubCommand) + reportUnknownArgument(/*IsArg=*/true, NearestHandlerString); + else + reportUnknownArgument(/*IsArg=*/false, NearestSubCommandString); + + ErrorParsing = true; continue; } diff --git a/llvm/unittests/Support/CommandLineTest.cpp b/llvm/unittests/Support/CommandLineTest.cpp index 762ac0ea9c36d..ae80490a33734 100644 --- a/llvm/unittests/Support/CommandLineTest.cpp +++ b/llvm/unittests/Support/CommandLineTest.cpp @@ -2244,4 +2244,34 @@ TEST(CommandLineTest, HelpWithSubcommands) { cl::ResetCommandLineParser(); } +TEST(CommandLineTest, UnknownCommands) { + cl::ResetCommandLineParser(); + + StackSubCommand SC1("foo", "Foo subcommand"); + StackSubCommand SC2("bar", "Bar subcommand"); + StackOption SC1Opt("put", cl::sub(SC1)); + StackOption SC2Opt("get", cl::sub(SC2)); + StackOption TopOpt1("peek"); + StackOption TopOpt2("set"); + + std::string Errs; + raw_string_ostream OS(Errs); + + const char *Args1[] = {"prog", "baz", "--get"}; + EXPECT_FALSE( + cl::ParseCommandLineOptions(std::size(Args1), Args1, StringRef(), &OS)); + EXPECT_EQ(Errs, + "prog: Unknown subcommand 'baz'. Try: 'prog --help'\n" + "prog: Did you mean 'bar'?\n" + "prog: Unknown command line argument '--get'. Try: 'prog --help'\n" + "prog: Did you mean '--set'?\n"); + + // Do not show a suggestion if the subcommand is not similar to any known. + Errs.clear(); + const char *Args2[] = {"prog", "faz"}; + EXPECT_FALSE( + cl::ParseCommandLineOptions(std::size(Args2), Args2, StringRef(), &OS)); + EXPECT_EQ(Errs, "prog: Unknown subcommand 'faz'. Try: 'prog --help'\n"); +} + } // anonymous namespace From 3cbb658e0d4f3a71ca09204c57d7d109090d1312 Mon Sep 17 00:00:00 2001 From: Igor Kudrin Date: Wed, 13 Dec 2023 13:19:48 -0800 Subject: [PATCH 2/2] fixup! [CommandLine] Better report unknown subcommands uppercase lambda --- llvm/lib/Support/CommandLine.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/llvm/lib/Support/CommandLine.cpp b/llvm/lib/Support/CommandLine.cpp index e894937288412..bac48ffb5ed95 100644 --- a/llvm/lib/Support/CommandLine.cpp +++ b/llvm/lib/Support/CommandLine.cpp @@ -1714,7 +1714,7 @@ bool CommandLineParser::ParseCommandLineOptions(int argc, continue; } - auto reportUnknownArgument = [&](bool IsArg, + auto ReportUnknownArgument = [&](bool IsArg, StringRef NearestArgumentName) { *Errs << ProgramName << ": Unknown " << (IsArg ? "command line argument" : "subcommand") << " '" @@ -1732,9 +1732,9 @@ bool CommandLineParser::ParseCommandLineOptions(int argc, }; if (i > 1 || !MaybeNamedSubCommand) - reportUnknownArgument(/*IsArg=*/true, NearestHandlerString); + ReportUnknownArgument(/*IsArg=*/true, NearestHandlerString); else - reportUnknownArgument(/*IsArg=*/false, NearestSubCommandString); + ReportUnknownArgument(/*IsArg=*/false, NearestSubCommandString); ErrorParsing = true; continue;