[llvm] [CommandLine] Better report unknown subcommands (PR #74811)

Igor Kudrin via llvm-commits llvm-commits at lists.llvm.org
Thu Dec 7 22:32:20 PST 2023


https://github.com/igorkudrin created https://github.com/llvm/llvm-project/pull/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'?
```

>From 4ede8496c0b953fec04e7861eaa69ae6e8236aad 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           | 45 ++++++++++++++++------
 llvm/unittests/Support/CommandLineTest.cpp | 21 ++++++++++
 2 files changed, 54 insertions(+), 12 deletions(-)

diff --git a/llvm/lib/Support/CommandLine.cpp b/llvm/lib/Support/CommandLine.cpp
index a7e0cae8b855d..b76c2cc8be6da 100644
--- a/llvm/lib/Support/CommandLine.cpp
+++ b/llvm/lib/Support/CommandLine.cpp
@@ -425,7 +425,7 @@ class CommandLineParser {
       return nullptr;
     return Opt;
   }
-  SubCommand *LookupSubCommand(StringRef Name);
+  SubCommand *LookupSubCommand(StringRef Name, std::string &NearestString);
 };
 
 } // namespace
@@ -550,9 +550,13 @@ 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 the closest match if there is no subcommand named 'Name'.
+  SubCommand *BestMatch = nullptr;
+  unsigned BestDistance = 0;
   for (auto *S : RegisteredSubCommands) {
     if (S == &SubCommand::getAll())
       continue;
@@ -561,7 +565,19 @@ SubCommand *CommandLineParser::LookupSubCommand(StringRef Name) {
 
     if (StringRef(S->getName()) == StringRef(Name))
       return S;
+
+    unsigned Distance =
+        Name.edit_distance(S->getName(), /*AllowReplacements=*/true,
+                           /*MaxEditDistance=*/BestDistance);
+    if (!BestMatch || Distance < BestDistance) {
+      BestMatch = S;
+      BestDistance = Distance;
+    }
   }
+
+  if (BestMatch)
+    NearestString = BestMatch->getName();
+
   return &SubCommand::getTopLevel();
 }
 
@@ -1527,10 +1543,12 @@ bool CommandLineParser::ParseCommandLineOptions(int argc,
 
   int FirstArg = 1;
   SubCommand *ChosenSubCommand = &SubCommand::getTopLevel();
+  std::string NearestSubCommandString;
   if (argc >= 2 && argv[FirstArg][0] != '-') {
     // 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 +1705,24 @@ bool CommandLineParser::ParseCommandLineOptions(int argc,
     }
 
     if (!Handler) {
-      if (SinkOpts.empty()) {
+      if (!SinkOpts.empty()) {
+        for (Option *SinkOpt : SinkOpts)
+          SinkOpt->addOccurrence(i, "", StringRef(argv[i]));
+        continue;
+      } else if (i == 1 && !NearestSubCommandString.empty()) {
+        *Errs << ProgramName << ": Unknown subcommand '" << argv[i]
+              << "'.  Try: '" << argv[0] << " --help'\n"
+              << ProgramName << ": Did you mean '" << NearestSubCommandString
+              << "'?\n";
+      } else {
         *Errs << ProgramName << ": Unknown command line argument '" << argv[i]
               << "'.  Try: '" << argv[0] << " --help'\n";
-
-        if (NearestHandler) {
+        if (NearestHandler)
           // If we know a near match, report it as well.
           *Errs << ProgramName << ": Did you mean '"
                 << PrintArg(NearestHandlerString, 0) << "'?\n";
-        }
-
-        ErrorParsing = true;
-      } else {
-        for (Option *SinkOpt : SinkOpts)
-          SinkOpt->addOccurrence(i, "", StringRef(argv[i]));
       }
+      ErrorParsing = true;
       continue;
     }
 
diff --git a/llvm/unittests/Support/CommandLineTest.cpp b/llvm/unittests/Support/CommandLineTest.cpp
index 381fe70b6b481..4ad20cf5febdf 100644
--- a/llvm/unittests/Support/CommandLineTest.cpp
+++ b/llvm/unittests/Support/CommandLineTest.cpp
@@ -2206,4 +2206,25 @@ TEST(CommandLineTest, DefaultValue) {
   EXPECT_EQ(1, StrInitOption.getNumOccurrences());
 }
 
+TEST(CommandLineTest, UnknownSubcommand) {
+  cl::ResetCommandLineParser();
+
+  StackSubCommand SC1("foo", "Foo subcommand");
+  StackSubCommand SC2("bar", "Bar subcommand");
+  StackOption<bool> SC1Opt("f", cl::sub(SC1));
+  StackOption<bool> SC2Opt("b", cl::sub(SC2));
+  StackOption<bool> TopOpt("t");
+
+  const char *Args[] = {"prog", "baz", "-p"};
+  std::string Errs;
+  raw_string_ostream OS(Errs);
+  EXPECT_FALSE(
+      cl::ParseCommandLineOptions(std::size(Args), Args, StringRef(), &OS));
+  EXPECT_EQ(Errs,
+            "prog: Unknown subcommand 'baz'.  Try: 'prog --help'\n"
+            "prog: Did you mean 'bar'?\n"
+            "prog: Unknown command line argument '-p'.  Try: 'prog --help'\n"
+            "prog: Did you mean '-t'?\n");
+}
+
 } // anonymous namespace



More information about the llvm-commits mailing list