[llvm] [orc_rt] adding a very simple CLI flags parser in (PR #172987)
Jared Wyles via llvm-commits
llvm-commits at lists.llvm.org
Mon Dec 22 01:14:29 PST 2025
https://github.com/jaredwy updated https://github.com/llvm/llvm-project/pull/172987
>From 2db072e783cb621abc69ab4d57f9210528ede105 Mon Sep 17 00:00:00 2001
From: jared w <jared.wyles at gmail.com>
Date: Fri, 19 Dec 2025 22:20:19 +1100
Subject: [PATCH] [orc_rt] adding a very simple CLI flags parser in for orc-rt
---
orc-rt/CMakeLists.txt | 2 +-
orc-rt/include/orc-rt-utils/CommandLine.h | 209 +++++++++++++++++++++
orc-rt/tools/orc-executor/orc-executor.cpp | 4 +
orc-rt/unittests/CMakeLists.txt | 7 +-
orc-rt/unittests/CommandLineTest.cpp | 136 ++++++++++++++
5 files changed, 354 insertions(+), 4 deletions(-)
create mode 100644 orc-rt/include/orc-rt-utils/CommandLine.h
create mode 100644 orc-rt/unittests/CommandLineTest.cpp
diff --git a/orc-rt/CMakeLists.txt b/orc-rt/CMakeLists.txt
index 95b7b852138e7..d88d165e7f747 100644
--- a/orc-rt/CMakeLists.txt
+++ b/orc-rt/CMakeLists.txt
@@ -68,7 +68,7 @@ configure_file(
#===============================================================================
if (ORC_RT_INCLUDE_DOCS)
- add_subdirectory(docs)
+ add_subdirectory(docs)
endif()
add_subdirectory(include)
diff --git a/orc-rt/include/orc-rt-utils/CommandLine.h b/orc-rt/include/orc-rt-utils/CommandLine.h
new file mode 100644
index 0000000000000..9daf126ef53af
--- /dev/null
+++ b/orc-rt/include/orc-rt-utils/CommandLine.h
@@ -0,0 +1,209 @@
+#include <algorithm>
+#include <charconv>
+#include <functional>
+#include <iomanip>
+#include <numeric>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "orc-rt/Error.h"
+
+namespace orc_rt {
+namespace detail {
+
+template <typename T> inline std::optional<T> parseValue(std::string_view Str);
+
+template <>
+inline std::optional<std::string>
+parseValue<std::string>(std::string_view Str) {
+ return std::string(Str);
+}
+
+template <>
+inline std::optional<std::string_view>
+parseValue<std::string_view>(std::string_view Str) {
+ return Str;
+}
+
+template <> inline std::optional<int> parseValue<int>(std::string_view Str) {
+ if (Str.empty())
+ return std::nullopt;
+ int Val{};
+ auto Ret = std::from_chars(Str.data(), Str.data() + Str.size(), Val);
+ if (Ret.ec != std::errc() || Ret.ptr != Str.data() + Str.size())
+ return std::nullopt;
+ return Val;
+}
+
+template <> inline std::optional<bool> parseValue<bool>(std::string_view Str) {
+ if (Str.empty())
+ return std::nullopt;
+
+ if (Str == "1")
+ return true;
+ if (Str == "0")
+ return false;
+
+ std::string Val;
+ std::transform(
+ Str.begin(), Str.end(), std::back_inserter(Val),
+ [](unsigned char C) { return static_cast<char>(std::tolower(C)); });
+
+ if (Val == "true")
+ return true;
+ if (Val == "false")
+ return false;
+
+ return std::nullopt;
+}
+} // namespace detail
+
+class CommandLineParser {
+public:
+ enum class OptionKind { Flag, Value };
+ CommandLineParser() = default;
+
+ CommandLineParser &addFlag(std::string_view Name, std::string_view Desc,
+ bool DefaultVal, bool &Val) {
+ return addValue(Name, Desc, DefaultVal, Val, OptionKind::Flag);
+ }
+
+ template <typename T>
+ CommandLineParser &addValue(std::string_view Name, std::string_view Desc,
+ T DefaultVal, T &Val,
+ OptionKind Kind = OptionKind::Value) {
+ Val = DefaultVal;
+ Opts.push_back({.Name = std::string(Name),
+ .Desc = std::string(Desc),
+ .Kind = Kind,
+ .Default = [&Val, DV = DefaultVal]() { Val = DV; },
+ .FromString = [&Val, OptName = std::string(Name)](
+ std::string_view S) -> orc_rt::Error {
+ if (auto V = detail::parseValue<T>(S)) {
+ Val = *V;
+ return orc_rt::Error::success();
+ }
+ return orc_rt::make_error<orc_rt::StringError>(
+ std::string("Invalid value for '") + OptName +
+ "': '" + std::string(S) + "'");
+ }});
+
+ return *this;
+ }
+
+ void printHelp(std::ostream &OS, std::string_view ProgramName) const {
+ OS << "Usage: " << ProgramName << " [options] [positional arguments]\n\n";
+ OS << "OPTIONS:\n";
+ size_t MaxWidth = std::accumulate(
+ Opts.begin(), Opts.end(), size_t(0), [](size_t Max, const Option &Opt) {
+ size_t Len = Opt.Name.length() + 2; // "--"
+ if (Opt.Kind == OptionKind::Value)
+ Len += 8; // "=<value>"
+ return std::max(Max, Len);
+ });
+
+ std::for_each(Opts.begin(), Opts.end(), [&](const Option &Opt) {
+ std::string FlagStr =
+ "--" + Opt.Name + (Opt.Kind == OptionKind::Value ? "=<value>" : "");
+ OS << " " << std::left << std::setw(MaxWidth + 2) << FlagStr << Opt.Desc
+ << "\n";
+ });
+ }
+
+ template <typename I> orc_rt::Error parse(I Begin, I End) {
+ std::for_each(Opts.begin(), Opts.end(),
+ [](const Option &O) { O.Default(); });
+ Positionals.clear();
+
+ bool AfterDashDash = false;
+
+ if (Begin != End)
+ Begin++;
+
+ for (auto It = Begin; It != End; ++It) {
+ std::string_view Tok(*It);
+
+ if (!AfterDashDash && Tok == "--") {
+ AfterDashDash = true;
+ continue;
+ }
+
+ if (!AfterDashDash && beginsDashes(Tok)) {
+ std::string_view K = removeLeadingDashes(Tok);
+ std::string_view V;
+ bool HasValue = false;
+
+ if (auto P = K.find('='); P != std::string_view::npos) {
+ V = K.substr(P + 1);
+ K = K.substr(0, P);
+ HasValue = true;
+ }
+
+ auto FoundOpt =
+ std::find_if(Opts.begin(), Opts.end(), [&](const Option &o) {
+ return std::string_view(o.Name) == K;
+ });
+
+ if (FoundOpt == Opts.end()) {
+ return orc_rt::make_error<orc_rt::StringError>(
+ "Unknown option '" + std::string(Tok) + "'");
+ }
+
+ if (FoundOpt->Kind == OptionKind::Flag) {
+ if (!HasValue)
+ V = "true";
+ } else if (!HasValue) {
+ if (std::next(It) == End) {
+ return orc_rt::make_error<orc_rt::StringError>(
+ "Option '" + std::string(K) + "' requires a value");
+ }
+ V = *++It;
+ }
+
+ if (auto Err = FoundOpt->FromString(V))
+ return Err;
+
+ } else {
+ Positionals.emplace_back(Tok);
+ }
+ }
+ return orc_rt::Error::success();
+ }
+
+ orc_rt::Error parse(int argc, char **argv) {
+ return parse(argv, argv + argc);
+ }
+
+ const std::vector<std::string> &positionals() const { return Positionals; }
+
+private:
+ struct Option {
+ std::string Name;
+ std::string Desc;
+ OptionKind Kind{};
+ std::function<void()> Default;
+ std::function<orc_rt::Error(std::string_view)> FromString;
+ };
+
+ std::vector<std::string> Positionals;
+ std::vector<Option> Opts;
+
+ static bool beginsDashes(std::string_view S) {
+ return !S.empty() && S.front() == '-';
+ }
+
+ static bool startsWith(std::string_view S, std::string_view P) {
+ return S.size() >= P.size() && S.compare(0, P.size(), P) == 0;
+ }
+
+ static std::string_view removeLeadingDashes(std::string_view S) {
+ if (startsWith(S, "--"))
+ return S.substr(2);
+ if (startsWith(S, "-"))
+ return S.substr(1);
+ return S;
+ }
+};
+} // namespace orc_rt
diff --git a/orc-rt/tools/orc-executor/orc-executor.cpp b/orc-rt/tools/orc-executor/orc-executor.cpp
index e83d28fa03d55..5ecb779c8258b 100644
--- a/orc-rt/tools/orc-executor/orc-executor.cpp
+++ b/orc-rt/tools/orc-executor/orc-executor.cpp
@@ -10,6 +10,10 @@
//
//===----------------------------------------------------------------------===//
+#include "orc-rt-utils/CommandLine.h"
+#include "orc-rt/Error.h"
+#include <iostream>
+
int main(int argc, char *argv[]) {
return 0;
}
diff --git a/orc-rt/unittests/CMakeLists.txt b/orc-rt/unittests/CMakeLists.txt
index 9edff560cd45c..b5a32459b4bd7 100644
--- a/orc-rt/unittests/CMakeLists.txt
+++ b/orc-rt/unittests/CMakeLists.txt
@@ -2,13 +2,13 @@ add_custom_target(OrcRTUnitTests)
set_target_properties(OrcRTUnitTests PROPERTIES FOLDER "orc-rt/Tests")
if (NOT TARGET llvm_gtest)
- message(WARNING "orc-rt unittests disabled due to GTest being unavailable; "
+ message(WARNING "orc-rt unittests disabled due to GTest being unavailable; "
"Try LLVM_INSTALL_GTEST=ON for the LLVM build")
- return ()
+ return ()
endif ()
function(add_orc_rt_unittest test_dirname)
- add_unittest(OrcRTUnitTests ${test_dirname} ${ARGN})
+ add_unittest(OrcRTUnitTests ${test_dirname} ${ARGN})
endfunction()
add_orc_rt_unittest(CoreTests
@@ -16,6 +16,7 @@ add_orc_rt_unittest(CoreTests
BitmaskEnumTest.cpp
CallableTraitsHelperTest.cpp
EndianTest.cpp
+ CommandLineTest.cpp
ErrorTest.cpp
ErrorExceptionInteropTest.cpp
ExecutorAddressTest.cpp
diff --git a/orc-rt/unittests/CommandLineTest.cpp b/orc-rt/unittests/CommandLineTest.cpp
new file mode 100644
index 0000000000000..ecbe7023a516b
--- /dev/null
+++ b/orc-rt/unittests/CommandLineTest.cpp
@@ -0,0 +1,136 @@
+#include "orc-rt-utils/CommandLine.h"
+#include "orc-rt/Error.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+using namespace orc_rt;
+
+class CommandLineParserTest : public ::testing::Test {
+protected:
+ std::string Host;
+ int Port = 0;
+ bool Verbose = false;
+ bool Help = false;
+ CommandLineParser Parser;
+
+ void SetUp() override {
+ // Configure the parser with the specific options requested
+ Parser.addValue("host", "Hostname", std::string("localhost"), Host);
+ Parser.addFlag("help", "Display this help message", false, Help);
+ Parser.addValue("port", "Port number", 8080, Port);
+ Parser.addFlag("verbose", "Enable verbose logging", false, Verbose);
+ }
+};
+
+TEST_F(CommandLineParserTest, NoopTest) {
+ CommandLineParser Parser;
+ const char *Argv[] = {"appname"};
+ auto Err = Parser.parse(1, const_cast<char **>(Argv));
+ EXPECT_TRUE(!Err);
+}
+
+// TODO(wyles): improve expected failure tests
+TEST_F(CommandLineParserTest, ValueRequired) {
+ const char *Argv[] = {"appname", "--host"};
+ auto Err = Parser.parse(std::begin(Argv), std::end(Argv));
+ if (!Err) {
+ ADD_FAILURE() << "--host requires a value, shouldn't succeed.";
+ } else {
+ orc_rt::consumeError(std::move(Err));
+ }
+}
+
+TEST_F(CommandLineParserTest, UnknownOption) {
+ const char *Argv[] = {"appname", "--unknown=foo"};
+ auto Err = Parser.parse(std::begin(Argv), std::end(Argv));
+
+ if (!Err) {
+ ADD_FAILURE() << "unknown option, shouldn't succeed.";
+ } else {
+ orc_rt::consumeError(std::move(Err));
+ }
+}
+
+TEST_F(CommandLineParserTest, InvalidInteger) {
+ const char *Argv[] = {"appname", "--port=not_a_number"};
+ auto Err = Parser.parse(std::begin(Argv), std::end(Argv));
+
+ if (!Err) {
+ ADD_FAILURE() << "Invalid integer, shouldn't succeed.";
+ } else {
+ orc_rt::consumeError(std::move(Err));
+ }
+}
+
+TEST_F(CommandLineParserTest, ParseFullConfiguration) {
+ const char *Argv[] = {"appname", "--host=example.com", "--port=8080",
+ "--verbose=true"};
+
+ cantFail(Parser.parse(std::begin(Argv), std::end(Argv)));
+
+ EXPECT_EQ(Host, "example.com");
+ EXPECT_EQ(Port, 8080);
+ EXPECT_EQ(Verbose, true);
+}
+
+TEST_F(CommandLineParserTest, ValueWithoutEqualSign) {
+ std::vector<const char *> Argv = {"appname", "--port", "12345", "--host",
+ "example.com"};
+
+ cantFail(Parser.parse(std::begin(Argv), std::end(Argv)));
+ EXPECT_EQ(Port, 12345);
+ EXPECT_EQ(Host, "example.com");
+}
+
+TEST_F(CommandLineParserTest, DoubleDashTerminatesOptionParsingWithArgvArgc) {
+ bool Verbose = false;
+ int Port = 0;
+
+ CommandLineParser Parser;
+ Parser.addFlag("verbose", "enable verbose mode", false, Verbose)
+ .addValue("port", "port number", 8080, Port);
+
+ const char *Argv[] = {"appname", "--verbose=true", "--", "--not-an-option",
+ "file.txt"};
+ cantFail(Parser.parse(static_cast<int>(std::size(Argv)),
+ const_cast<char **>(Argv)));
+
+ EXPECT_TRUE(Verbose);
+ EXPECT_EQ(Port, 8080);
+
+ const auto &Pos = Parser.positionals();
+ ASSERT_EQ(Pos.size(), 2u);
+ EXPECT_EQ(Pos[0], "--not-an-option");
+ EXPECT_EQ(Pos[1], "file.txt");
+}
+
+TEST_F(CommandLineParserTest, PrintHelpFunctionalAlignment) {
+ std::string OutputDir = "";
+ Parser.addValue("output-dir", "Directory for output files",
+ std::string("/tmp"), OutputDir);
+
+ Parser.addFlag("help", "Display this help message", false, Help);
+
+ std::stringstream SS;
+ Parser.printHelp(SS, "appname");
+ std::string Result = SS.str();
+
+ auto GetDescriptionColumn = [&](std::string_view SearchTerm) -> size_t {
+ size_t Pos = Result.find(SearchTerm);
+ if (Pos == std::string::npos)
+ return 0;
+ size_t LineStart = Result.rfind('\n', Pos);
+ return (LineStart == std::string::npos) ? Pos : (Pos - LineStart - 1);
+ };
+
+ size_t HelpCol = GetDescriptionColumn("Display this help");
+ size_t PortCol = GetDescriptionColumn("Port number");
+ size_t DirCol = GetDescriptionColumn("Directory for output files");
+
+ ASSERT_NE(HelpCol, 0);
+ EXPECT_EQ(HelpCol, PortCol) << "Help and Port descriptions are not aligned!";
+ EXPECT_EQ(PortCol, DirCol)
+ << "Port and OutputDir descriptions are not aligned!";
+
+ EXPECT_TRUE(Result.find("--output-dir=<value>") != std::string::npos);
+}
More information about the llvm-commits
mailing list