[llvm] [LLVM][Option] Support multiline option parsing (PR #171214)

Danny Mösch via llvm-commits llvm-commits at lists.llvm.org
Tue Jan 6 09:36:19 PST 2026


https://github.com/SimplyDanny updated https://github.com/llvm/llvm-project/pull/171214

>From 8e9a43abfea934f23c8c18f3c3e82374d6cd2b26 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Danny=20M=C3=B6sch?= <danny.moesch at icloud.com>
Date: Mon, 8 Dec 2025 23:07:49 +0100
Subject: [PATCH] [LLVM][Option] Support multiline option parsing

Consider arguments as belonging even if they are defined on
(multiple) lines after the introducing argument. Empty lines
between arguments are ignored as well.
---
 llvm/lib/Option/OptTable.cpp                |   2 +-
 llvm/lib/Option/Option.cpp                  | 107 +++++++++++++-----
 llvm/unittests/Option/OptionParsingTest.cpp | 117 ++++++++++++++++++++
 3 files changed, 196 insertions(+), 30 deletions(-)

diff --git a/llvm/lib/Option/OptTable.cpp b/llvm/lib/Option/OptTable.cpp
index 065036cedc2ae..4e8e59db8335c 100644
--- a/llvm/lib/Option/OptTable.cpp
+++ b/llvm/lib/Option/OptTable.cpp
@@ -537,7 +537,7 @@ InputArgList OptTable::internalParseArgs(
   MissingArgIndex = MissingArgCount = 0;
   unsigned Index = 0, End = ArgArr.size();
   while (Index < End) {
-    // Ingore nullptrs, they are response file's EOL markers
+    // Ignore nullptrs, they are response file's EOL markers.
     if (Args.getArgString(Index) == nullptr) {
       ++Index;
       continue;
diff --git a/llvm/lib/Option/Option.cpp b/llvm/lib/Option/Option.cpp
index 738f75bb41e68..d77f9fac64596 100644
--- a/llvm/lib/Option/Option.cpp
+++ b/llvm/lib/Option/Option.cpp
@@ -109,6 +109,36 @@ bool Option::matches(OptSpecifier Opt) const {
   return false;
 }
 
+/// Describes how to treat `nullptr` argument strings produced for
+/// newlines and empty lines when advancing through the argument
+/// list.
+enum class NextArgStringBehavior {
+  /// Skip over `nullptr` entries and return the next non-null
+  /// argument string, if any.
+  SkipEmpty,
+  /// Treat a `nullptr` entry as a hard boundary and stop
+  /// advancing. This is used for options that should not
+  /// consume arguments past such boundaries (for example,
+  /// RemainingArgs* options that must not cross @file
+  /// segments).
+  StopAtNull,
+};
+
+/// Advances to and returns the next argument string according to the given
+/// behavior.
+static const char *advanceToNextArgString(
+    const ArgList &Args, unsigned &Index,
+    NextArgStringBehavior Behavior = NextArgStringBehavior::SkipEmpty) {
+  while (Index <= Args.getNumInputArgStrings()) {
+    if (const char *ArgStr = Args.getArgString(Index - 1))
+      return ArgStr;
+    if (Behavior == NextArgStringBehavior::StopAtNull)
+      return nullptr;
+    ++Index;
+  }
+  return nullptr;
+}
+
 std::unique_ptr<Arg> Option::acceptInternal(const ArgList &Args,
                                             StringRef Spelling,
                                             unsigned &Index) const {
@@ -152,31 +182,41 @@ std::unique_ptr<Arg> Option::acceptInternal(const ArgList &Args,
 
     return A;
   }
-  case SeparateClass:
+  case SeparateClass: {
     // Matches iff this is an exact match.
     if (SpellingSize != ArgStringSize)
       return nullptr;
 
+    unsigned StartIndex = Index;
     Index += 2;
-    if (Index > Args.getNumInputArgStrings() ||
-        Args.getArgString(Index - 1) == nullptr)
+
+    const char *ArgString = advanceToNextArgString(Args, Index);
+    if (!ArgString)
       return nullptr;
 
-    return std::make_unique<Arg>(*this, Spelling, Index - 2,
-                                 Args.getArgString(Index - 1));
+    return std::make_unique<Arg>(*this, Spelling, StartIndex, ArgString);
+  }
   case MultiArgClass: {
     // Matches iff this is an exact match.
     if (SpellingSize != ArgStringSize)
       return nullptr;
 
-    Index += 1 + getNumArgs();
-    if (Index > Args.getNumInputArgStrings())
-      return nullptr;
+    unsigned StartIndex = Index;
+    Index += 2;
+
+    SmallVector<const char *, 4> Values;
+    for (unsigned i = 0; i < getNumArgs(); ++i) {
+      const char *ArgString = advanceToNextArgString(Args, Index);
+      if (!ArgString)
+        return nullptr;
+      Values.push_back(ArgString);
+      if (i + 1 < getNumArgs())
+        ++Index;
+    }
 
-    auto A = std::make_unique<Arg>(*this, Spelling, Index - 1 - getNumArgs(),
-                                   Args.getArgString(Index - getNumArgs()));
+    auto A = std::make_unique<Arg>(*this, Spelling, StartIndex, Values[0]);
     for (unsigned i = 1; i != getNumArgs(); ++i)
-      A->getValues().push_back(Args.getArgString(Index - getNumArgs() + i));
+      A->getValues().push_back(Values[i]);
     return A;
   }
   case JoinedOrSeparateClass: {
@@ -187,32 +227,39 @@ std::unique_ptr<Arg> Option::acceptInternal(const ArgList &Args,
     }
 
     // Otherwise it must be separate.
+    unsigned StartIndex = Index;
     Index += 2;
-    if (Index > Args.getNumInputArgStrings() ||
-        Args.getArgString(Index - 1) == nullptr)
+
+    const char *ArgString = advanceToNextArgString(Args, Index);
+    if (!ArgString)
       return nullptr;
 
-    return std::make_unique<Arg>(*this, Spelling, Index - 2,
-                                 Args.getArgString(Index - 1));
+    return std::make_unique<Arg>(*this, Spelling, StartIndex, ArgString);
   }
-  case JoinedAndSeparateClass:
+  case JoinedAndSeparateClass: {
     // Always matches.
+    unsigned StartIndex = Index;
+    const char *JoinedValue = Args.getArgString(StartIndex) + SpellingSize;
     Index += 2;
-    if (Index > Args.getNumInputArgStrings() ||
-        Args.getArgString(Index - 1) == nullptr)
+
+    const char *SeparateValue = advanceToNextArgString(Args, Index);
+    if (!SeparateValue)
       return nullptr;
 
-    return std::make_unique<Arg>(*this, Spelling, Index - 2,
-                                 Args.getArgString(Index - 2) + SpellingSize,
-                                 Args.getArgString(Index - 1));
+    return std::make_unique<Arg>(*this, Spelling, StartIndex, JoinedValue,
+                                 SeparateValue);
+  }
   case RemainingArgsClass: {
     // Matches iff this is an exact match.
     if (SpellingSize != ArgStringSize)
       return nullptr;
-    auto A = std::make_unique<Arg>(*this, Spelling, Index++);
-    while (Index < Args.getNumInputArgStrings() &&
-           Args.getArgString(Index) != nullptr)
-      A->getValues().push_back(Args.getArgString(Index++));
+    auto A = std::make_unique<Arg>(*this, Spelling, Index);
+    Index += 2;
+    while (const char *ArgString = advanceToNextArgString(
+               Args, Index, NextArgStringBehavior::StopAtNull)) {
+      A->getValues().push_back(ArgString);
+      Index++;
+    }
     return A;
   }
   case RemainingArgsJoinedClass: {
@@ -221,10 +268,12 @@ std::unique_ptr<Arg> Option::acceptInternal(const ArgList &Args,
       // An inexact match means there is a joined arg.
       A->getValues().push_back(Args.getArgString(Index) + SpellingSize);
     }
-    Index++;
-    while (Index < Args.getNumInputArgStrings() &&
-           Args.getArgString(Index) != nullptr)
-      A->getValues().push_back(Args.getArgString(Index++));
+    Index += 2;
+    while (const char *ArgString = advanceToNextArgString(
+               Args, Index, NextArgStringBehavior::StopAtNull)) {
+      A->getValues().push_back(ArgString);
+      Index++;
+    }
     return A;
   }
 
diff --git a/llvm/unittests/Option/OptionParsingTest.cpp b/llvm/unittests/Option/OptionParsingTest.cpp
index 3da015e343eb9..10ed60d476621 100644
--- a/llvm/unittests/Option/OptionParsingTest.cpp
+++ b/llvm/unittests/Option/OptionParsingTest.cpp
@@ -559,3 +559,120 @@ USAGE: usage
                   multiple lines in it
 )");
 }
+
+TYPED_TEST(OptTableTest, ResponseFileNullptrMarkers) {
+  TypeParam T;
+  unsigned MAI, MAC;
+
+  // This simulates a response file with:
+  //   -C
+  //   value
+  const char *Args1[] = {"-C", nullptr, "value"};
+  InputArgList AL = T.ParseArgs(Args1, MAI, MAC);
+  EXPECT_TRUE(AL.hasArg(OPT_C));
+  EXPECT_EQ("value", AL.getLastArgValue(OPT_C));
+  EXPECT_EQ(0U, MAC);
+
+  // This simulates blank lines in a response file:
+  //   -C
+  //
+  //
+  //   value
+  const char *Args2[] = {"-C", nullptr, nullptr, nullptr, "value"};
+  InputArgList AL2 = T.ParseArgs(Args2, MAI, MAC);
+  EXPECT_TRUE(AL2.hasArg(OPT_C));
+  EXPECT_EQ("value", AL2.getLastArgValue(OPT_C));
+  EXPECT_EQ(0U, MAC);
+
+  // This simulates a response file with -E takes 2 arguments:
+  //   -E
+  //   arg1
+  //   arg2
+  const char *Args3[] = {"-E", nullptr, "arg1", nullptr, "arg2"};
+  InputArgList AL3 = T.ParseArgs(Args3, MAI, MAC);
+  EXPECT_TRUE(AL3.hasArg(OPT_E));
+  std::vector<std::string> EValues = AL3.getAllArgValues(OPT_E);
+  ASSERT_EQ(2U, EValues.size());
+  EXPECT_EQ("arg1", EValues[0]);
+  EXPECT_EQ("arg2", EValues[1]);
+  EXPECT_EQ(0U, MAC);
+
+  // -F can be either joined or separate, test the separate case:
+  //   -F
+  //   value
+  const char *Args4[] = {"-F", nullptr, "value"};
+  InputArgList AL4 = T.ParseArgs(Args4, MAI, MAC);
+  EXPECT_TRUE(AL4.hasArg(OPT_F));
+  EXPECT_EQ("value", AL4.getLastArgValue(OPT_F));
+  EXPECT_EQ(0U, MAC);
+
+  // Test that missing argument is still detected correctly with missing
+  // arguments at the end.
+  const char *Args5[] = {"-C", nullptr, nullptr};
+  InputArgList AL5 = T.ParseArgs(Args5, MAI, MAC);
+  EXPECT_FALSE(AL5.hasArg(OPT_C));
+  EXPECT_GT(MAC, 0U);
+  EXPECT_EQ(0U, MAI);
+
+  // -G joined to "joined" and followed by "separate":
+  //   -Gjoined
+  //
+  //   separate
+  const char *Args7[] = {"-Gjoined", nullptr, nullptr, "separate"};
+  InputArgList AL7 = T.ParseArgs(Args7, MAI, MAC);
+  EXPECT_TRUE(AL7.hasArg(OPT_G));
+  EXPECT_EQ("joined", AL7.getLastArgValue(OPT_G));
+  EXPECT_EQ(2U, AL7.getAllArgValues(OPT_G).size());
+  EXPECT_EQ("joined", AL7.getAllArgValues(OPT_G)[0]);
+  EXPECT_EQ("separate", AL7.getAllArgValues(OPT_G)[1]);
+  EXPECT_EQ(0U, MAC);
+
+  // -slurp consumes arguments up to the first nullptr marker,
+  // treating it as a boundary between segments:
+  //   -slurp
+  //   arg1
+  //
+  //   arg2
+  const char *Args8[] = {"-slurp", "arg1", nullptr, nullptr, "arg2"};
+  InputArgList AL8 = T.ParseArgs(Args8, MAI, MAC);
+  EXPECT_TRUE(AL8.hasArg(OPT_Slurp));
+  std::vector<std::string> SlurpValues = AL8.getAllArgValues(OPT_Slurp);
+  ASSERT_EQ(1U, SlurpValues.size());
+  EXPECT_EQ("arg1", SlurpValues[0]);
+  EXPECT_EQ(0U, MAC);
+
+  // -slurpjoined consumes joined arg and arguments up to the
+  // first nullptr marker:
+  //   -slurpjoinedjoined
+  //   arg1
+  //
+  //   arg2
+  const char *Args9[] = {"-slurpjoinedjoined", "arg1", nullptr, nullptr,
+                         "arg2"};
+  InputArgList AL9 = T.ParseArgs(Args9, MAI, MAC);
+  EXPECT_TRUE(AL9.hasArg(OPT_SlurpJoined));
+  std::vector<std::string> SlurpJoinedValues =
+      AL9.getAllArgValues(OPT_SlurpJoined);
+  ASSERT_EQ(2U, SlurpJoinedValues.size());
+  EXPECT_EQ("joined", SlurpJoinedValues[0]);
+  EXPECT_EQ("arg1", SlurpJoinedValues[1]);
+  EXPECT_EQ(0U, MAC);
+}
+
+TYPED_TEST(OptTableTest, ResponseFileIntegration) {
+  TypeParam T;
+  unsigned MAI, MAC;
+
+  const char *ExpandedArgs[] = {
+      "-A",     nullptr, "-C", nullptr, nullptr,
+      "cvalue", nullptr, "-F", nullptr, "fvalue",
+  };
+
+  InputArgList AL = T.ParseArgs(ExpandedArgs, MAI, MAC);
+  EXPECT_TRUE(AL.hasArg(OPT_A));
+  EXPECT_TRUE(AL.hasArg(OPT_C));
+  EXPECT_TRUE(AL.hasArg(OPT_F));
+  EXPECT_EQ("cvalue", AL.getLastArgValue(OPT_C));
+  EXPECT_EQ("fvalue", AL.getLastArgValue(OPT_F));
+  EXPECT_EQ(0U, MAC);
+}



More information about the llvm-commits mailing list