[llvm] [CommandLine] Better report unknown subcommands (PR #74811)
Igor Kudrin via llvm-commits
llvm-commits at lists.llvm.org
Tue Dec 12 22:02:15 PST 2023
https://github.com/igorkudrin updated https://github.com/llvm/llvm-project/pull/74811
>From c09a851d4685997a28dfda3b64c50ed4678b890e Mon Sep 17 00:00:00 2001
From: Igor Kudrin <ikudrin at accesssoftek.com>
Date: Thu, 7 Dec 2023 22:27:37 -0800
Subject: [PATCH] [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 | 57 +++++++++++++-----
2 files changed, 95 insertions(+), 29 deletions(-)
diff --git a/llvm/lib/Support/CommandLine.cpp b/llvm/lib/Support/CommandLine.cpp
index 31f79972125da9..e894937288412c 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 762ac0ea9c36dd..a25477a134ed70 100644
--- a/llvm/unittests/Support/CommandLineTest.cpp
+++ b/llvm/unittests/Support/CommandLineTest.cpp
@@ -1347,20 +1347,20 @@ struct AutoDeleteFile {
};
static std::string interceptStdout(std::function<void()> F) {
- outs().flush(); // flush any output from previous tests
- AutoDeleteFile File;
- {
- OutputRedirector Stdout(fileno(stdout));
- if (!Stdout.Valid)
- return "";
- File.FilePath = Stdout.FilePath;
+ outs().flush(); // flush any output from previous tests
+ AutoDeleteFile File;
+ {
+ OutputRedirector Stdout(fileno(stdout));
+ if (!Stdout.Valid)
+ return "";
+ File.FilePath = Stdout.FilePath;
F();
- outs().flush();
- }
- auto Buffer = MemoryBuffer::getFile(File.FilePath);
- if (!Buffer)
- return "";
- return Buffer->get()->getBuffer().str();
+ outs().flush();
+ }
+ auto Buffer = MemoryBuffer::getFile(File.FilePath);
+ if (!Buffer)
+ return "";
+ return Buffer->get()->getBuffer().str();
}
template <void (*Func)(const cl::Option &)>
@@ -2244,4 +2244,35 @@ TEST(CommandLineTest, HelpWithSubcommands) {
cl::ResetCommandLineParser();
}
+TEST(CommandLineTest, UnknownCommands) {
+ cl::ResetCommandLineParser();
+
+ StackSubCommand SC1("foo", "Foo subcommand");
+ StackSubCommand SC2("bar", "Bar subcommand");
+ StackOption<bool> SC1Opt("put", cl::sub(SC1));
+ StackOption<bool> SC2Opt("get", cl::sub(SC2));
+ StackOption<bool> TopOpt1("peek");
+ StackOption<bool> 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
More information about the llvm-commits
mailing list