diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 741f2b093d09d..0a35172a60050 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -119,8 +119,9 @@ endforeach() add_dependencies(genreflex rootcling) add_dependencies(rootcint rootcint) - if (TARGET Gui) ROOT_EXECUTABLE(rootbrowse src/rootbrowse.cxx LIBRARIES RIO Core Rint Gui) endif() ROOT_EXECUTABLE(rootls src/rootls.cxx LIBRARIES RIO Tree Core Rint ROOTNTuple) + +ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/main/src/optparse.hxx b/main/src/optparse.hxx new file mode 100644 index 0000000000000..7be28ec5e615e --- /dev/null +++ b/main/src/optparse.hxx @@ -0,0 +1,382 @@ +// \file optparse.hxx +/// +/// Small utility to parse cmdline options. +/// +/// Usage: +/// ~~~{.cpp} +/// MyAppOpts ParseArgs(const char **args, int nArgs) { +/// ROOT::RCmdLineOpts opts; +/// // will parse '-c VAL', '--compress VAL' or '--compress=VAL' +/// opts.AddFlag({"-c", "--compress"}, RCmdLineOpts::EFlagType::kWithArg); +/// // will toggle a switch '--recreate' (no args). +/// opts.AddFlag({"--recreate"}); +/// opts.AddFlag({"-o"}, RCmdLineOpts::EFlagType::kWithArg); +/// +/// // NOTE: `args` should not contain the program name! It should usually be `argc + 1`. +/// // For example `main(char **argv, int argc)` might call this function as: +/// // `ParseArgs(const_cast(argv) + 1, argc - 1);` +/// opts.Parse(args, nArgs); +/// +/// // Check for errors: +/// for (const auto &err : opts.GetErrors()) { /* print errors ... */ } +/// if (!opts.GetErrors().empty()) return {}; +/// +/// // Convert the parsed options from string if necessary: +/// MyAppOpts myOpts; +/// // switch (boolean flag): +/// myOpts.fRecreate = opts.GetSwitch("recreate"); +/// // string flag: +/// myOpts.fOutput = opts.GetFlagValue("o"); +/// // integer flag: +/// myOpts.fCompression = opts.GetFlagValueAs("compress"); // (could also have used "c" instead of "compress") +/// // positional arguments: +/// myOpts.fArgs = opts.GetArgs(); +/// +/// return myOpts; +/// } +/// ~~~ +/// +/// ## Additional Notes +/// If all the short flags you pass (those starting with a single `-`) are 1 character long, the parser will accept +/// grouped flags like "-abc" as equivalent to "-a -b -c". The last flag in the group may also accept an argument, in +/// which case "-abc foo" will count as "-a -b -c foo" where "foo" is the argument to "-c". +/// +/// Multiple repeated flags, like `-vvv` are not supported. +/// +/// The string "--" is treated as the positional argument separator: all strings after it will be treated as positional +/// arguments even if they start with "-". +/// +/// \author Giacomo Parolini +/// \date 2025-10-09 + +#ifndef ROOT_OptParse +#define ROOT_OptParse + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ROOT { + +class RCmdLineOpts { +public: + enum class EFlagType { + kSwitch, + kWithArg + }; + + struct RFlag { + std::string fName; + std::string fValue; + std::string fHelp; + }; + +private: + std::vector fFlags; + std::vector fArgs; + // If true, many short flags may be grouped: "-abc" == "-a -b -c". + // This is automatically true if all short flags given are 1 character long, otherwise it's false. + bool fAllowFlagGrouping = true; + + struct RExpectedFlag { + EFlagType fFlagType = EFlagType::kSwitch; + std::string fName; + std::string fHelp; + // If >= 0, this flag is an alias of the RExpectedFlag at index fAlias. + int fAlias = -1; + bool fShort = false; + + std::string AsStr() const { return std::string(fShort ? "-" : "--") + fName; } + }; + std::vector fExpectedFlags; + std::vector fErrors; + + const RExpectedFlag *GetExpectedFlag(std::string_view name) const + { + for (const auto &flag : fExpectedFlags) { + if (flag.fName == name) + return &flag; + } + return nullptr; + } + +public: + const std::vector &GetErrors() const { return fErrors; } + const std::vector &GetArgs() const { return fArgs; } + const std::vector &GetFlags() const { return fFlags; } + + /// Conveniency method to print any errors to `stream`. + /// \return true if any error was printed + bool ReportErrors(std::ostream &stream = std::cerr) const + { + for (const auto &err : fErrors) + stream << err << "\n"; + return !fErrors.empty(); + } + + /// Defines a new flag (either a switch or a flag with argument). The flag may be referred to as any of the + /// values inside `aliases` (e.g. { "-h", "--help" }). All strings inside `aliases` must start with `-` or `--` + /// and be at least 1 character long (aside the dashes). + /// Flags starting with a single `-` are considered "short", regardless of their actual length. + /// If all short flags are 1 character long, they may be collapsed into one and parsed as individual flags + /// (meaning a string like "-fgk" will be parsed as "-f -g -k") and the final flag may have a following argument. + /// This does NOT happen if any short flag is longer than 1 character, to avoid ambiguity. + void AddFlag(std::initializer_list aliases, EFlagType type = EFlagType::kSwitch, + std::string_view help = "") + { + int aliasIdx = -1; + for (auto f : aliases) { + auto prefixLen = f.find_first_not_of('-'); + if (prefixLen != 1 && prefixLen != 2) + throw std::invalid_argument(std::string("Invalid flag `") + std::string(f) + + "`: flags must start with '-' or '--'"); + if (f.size() == prefixLen) + throw std::invalid_argument("Flag name cannot be empty"); + + fAllowFlagGrouping = fAllowFlagGrouping && (prefixLen > 1 || f.size() == 2); + + RExpectedFlag expected; + expected.fFlagType = type; + expected.fName = f.substr(prefixLen); + expected.fHelp = help; + expected.fAlias = aliasIdx; + expected.fShort = prefixLen == 1; + fExpectedFlags.push_back(expected); + if (aliasIdx < 0) + aliasIdx = fExpectedFlags.size() - 1; + } + } + + /// If `name` refers to a previously-defined switch (i.e. a boolean flag), gets its value. + /// \throws std::invalid_argument if the flag was undefined or defined as a flag with arguments + bool GetSwitch(std::string_view name) const + { + const auto *exp = GetExpectedFlag(name); + if (!exp) + throw std::invalid_argument(std::string("Flag `") + std::string(name) + "` is not expected"); + if (exp->fFlagType != EFlagType::kSwitch) + throw std::invalid_argument(std::string("Flag `") + std::string(name) + "` is not a switch"); + + std::string_view lookedUpName = name; + if (exp->fAlias >= 0) + lookedUpName = fExpectedFlags[exp->fAlias].fName; + + for (const auto &f : fFlags) { + if (f.fName == lookedUpName) + return true; + } + return false; + } + + /// If `name` refers to a previously-defined non-switch flag, gets its value. + /// \throws std::invalid_argument if the flag was undefined or defined as a switch flag + std::string_view GetFlagValue(std::string_view name) const + { + const auto *exp = GetExpectedFlag(name); + if (!exp) + throw std::invalid_argument(std::string("Flag `") + std::string(name) + "` is not expected"); + if (exp->fFlagType != EFlagType::kWithArg) + throw std::invalid_argument(std::string("Flag `") + std::string(name) + + "` is a switch, use GetSwitch()"); + + std::string_view lookedUpName = name; + if (exp->fAlias >= 0) + lookedUpName = fExpectedFlags[exp->fAlias].fName; + + for (const auto &f : fFlags) { + if (f.fName == lookedUpName) + return f.fValue; + } + return ""; + } + + // Tries to retrieve the flag value as a type T. + // The only supported types are integral and floating point types. + // \return A value of type T if the flag is present and convertible + // \return nullopt if the flag is not there + // \throws std::invalid_argument if the flag is there but not convertible. + template + std::optional GetFlagValueAs(std::string_view name) const + { + static_assert(std::is_integral_v || std::is_floating_point_v); + + if (auto val = GetFlagValue(name); !val.empty()) { + // NOTE: on paper std::from_chars is supported since C++17, however some compilers don't properly support it + // (e.g. it's not available at all on MacOS < 26 and only the integer overload is available in AlmaLinux 8). + // There is also no compiler define that we can use to determine the availability, so we just use it only + // from C++20 and hope for the best. +#if __cplusplus >= 202002L && !defined(R__MACOSX) + T converted; + auto res = std::from_chars(val.data(), val.data() + val.size(), converted); + if (res.ptr == val.data() + val.size() && res.ec == std::errc{}) { + return converted; + } else { + std::stringstream err; + err << "Failed to parse flag `" << name << "` with value `" << val << "`"; + if constexpr (std::is_integral_v) + err << " as an integer.\n"; + else + err << " as a floating point number.\n"; + + if (res.ec == std::errc::result_out_of_range) + throw std::out_of_range(err.str()); + else + throw std::invalid_argument(err.str()); + } +#else + std::conditional_t, long long, long double> converted; + std::size_t unconvertedPos; + if constexpr (std::is_integral_v) { + converted = std::stoll(std::string(val), &unconvertedPos); + } else { + converted = std::stold(std::string(val), &unconvertedPos); + } + + const bool isOor = converted > std::numeric_limits::max(); + if (unconvertedPos != val.size() || isOor) { + std::stringstream err; + err << "Failed to parse flag `" << name << "` with value `" << val << "`"; + if constexpr (std::is_integral_v) + err << " as an integer.\n"; + else + err << " as a floating point number.\n"; + + if (isOor) + throw std::out_of_range(err.str()); + else + throw std::invalid_argument(err.str()); + } + + return converted; +#endif + } + return std::nullopt; + } + + void Parse(const char **args, std::size_t nArgs) + { + bool forcePositional = false; + + std::vector argStr; + + for (std::size_t i = 0; i < nArgs && fErrors.empty(); ++i) { + const char *arg = args[i]; + + if (strcmp(arg, "--") == 0) { + forcePositional = true; + continue; + } + + bool isFlag = !forcePositional && arg[0] == '-'; + if (!isFlag) { + // positional argument + fArgs.push_back(arg); + } else { + ++arg; + // Parse long or short flag and its argument into `argStr` / `nxtArgStr`. + // Note that `argStr` may contain multiple flags in case of grouped short flags (in which case nxtArgStr + // refers only to the last one). + argStr.clear(); + std::string_view nxtArgStr; + bool nxtArgIsTentative = true; + if (arg[0] == '-') { + // long flag + ++arg; + const char *eq = strchr(arg, '='); + if (eq) { + argStr.push_back(std::string_view(arg, eq - arg)); + nxtArgStr = std::string_view(eq + 1); + nxtArgIsTentative = false; + } else { + argStr.push_back(std::string_view(arg)); + if (i < nArgs - 1 && args[i + 1][0] != '-') { + nxtArgStr = args[i + 1]; + ++i; + } + } + } else { + // short flag. + // If flag grouping is active, all flags except the last one will have an implicitly empty argument. + auto argLen = strlen(arg); + while (fAllowFlagGrouping && argLen > 1) { + argStr.push_back(std::string_view{arg, 1}); + ++arg, --argLen; + } + + argStr.push_back(std::string_view(arg)); + if (i < nArgs - 1 && args[i + 1][0] != '-') { + nxtArgStr = args[i + 1]; + ++i; + } + } + + for (auto j = 0u; j < argStr.size(); ++j) { + std::string_view argS = argStr[j]; + const auto *exp = GetExpectedFlag(argS); + if (!exp) { + fErrors.push_back(std::string("Unknown flag: ") + args[j]); + break; + } + + std::string_view nxtArg = (j == argStr.size() - 1) ? nxtArgStr : ""; + + RCmdLineOpts::RFlag flag; + flag.fHelp = exp->fHelp; + // If the flag is an alias (e.g. long version of a short one), save its name as the aliased one, so we + // can fetch the value later by using any of the aliases. + if (exp->fAlias < 0) + flag.fName = argS; + else + flag.fName = fExpectedFlags[exp->fAlias].fName; + + // Check for duplicate flags + auto existingIt = + std::find_if(fFlags.begin(), fFlags.end(), [&flag](const auto &f) { return f.fName == flag.fName; }); + if (existingIt != fFlags.end()) { + std::string err = std::string("Flag ") + exp->AsStr() + " appeared more than once"; + if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg) + err += " with the value: " + existingIt->fValue; + fErrors.push_back(err); + break; + } + + // Check that arguments are what we expect. + if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg) { + if (!nxtArg.empty()) { + flag.fValue = nxtArg; + } else { + fErrors.push_back("Missing argument for flag " + exp->AsStr()); + } + } else { + if (!nxtArg.empty()) { + if (nxtArgIsTentative) + --i; + else + fErrors.push_back("Flag " + exp->AsStr() + " does not expect an argument"); + } + } + + if (!fErrors.empty()) + break; + + fFlags.push_back(flag); + } + + if (!fErrors.empty()) + break; + } + } + } +}; + +} // namespace ROOT + +#endif diff --git a/main/src/rootbrowse.cxx b/main/src/rootbrowse.cxx index 40c427e584cf2..29d79df70e5fd 100644 --- a/main/src/rootbrowse.cxx +++ b/main/src/rootbrowse.cxx @@ -6,6 +6,8 @@ /// \date 2025-08-21 #include +#include "optparse.hxx" + #include #include #include @@ -55,68 +57,41 @@ struct RootBrowseArgs { kLong }; EPrintUsage fPrintHelp = EPrintUsage::kNo; - std::string_view fWeb; - std::string_view fFileName; + std::string fWeb; + std::string fFileName; }; static RootBrowseArgs ParseArgs(const char **args, int nArgs) { RootBrowseArgs outArgs; - bool forcePositional = false; + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-h", "--help"}); + opts.AddFlag({"-w", "--web"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + opts.AddFlag({"-wf", "--webOff"}); - for (int i = 0; i < nArgs; ++i) { - const char *arg = args[i]; + opts.Parse(args, nArgs); - if (strcmp(arg, "--") == 0) { - forcePositional = true; - continue; - } + if (opts.ReportErrors()) { + outArgs.fPrintHelp = RootBrowseArgs::EPrintUsage::kShort; + return outArgs; + } - bool isFlag = !forcePositional && arg[0] == '-'; - if (isFlag) { - ++arg; - // Parse long or short flag and its argument into `argStr` / `nxtArgStr`. - std::string_view argStr, nxtArgStr; - if (arg[0] == '-') { - ++arg; - // long flag: may be either of the form `--web off` or `--web=off` - const char *eq = strchr(arg, '='); - if (eq) { - argStr = std::string_view(arg, eq - arg); - nxtArgStr = std::string_view(eq + 1); - } else { - argStr = std::string_view(arg); - if (i < nArgs - 1 && args[i + 1][0] != '-') { - nxtArgStr = args[i + 1]; - ++i; - } - } - } else { - // short flag (note that it might be more than 1 character long, like `-wf`) - argStr = std::string_view(arg); - if (i < nArgs - 1 && args[i + 1][0] != '-') { - nxtArgStr = args[i + 1]; - ++i; - } - } - - if (argStr == "w" || argStr == "web") { - outArgs.fWeb = nxtArgStr.empty() ? "on" : nxtArgStr; - } else if (argStr == "h" || argStr == "help") { - outArgs.fPrintHelp = RootBrowseArgs::EPrintUsage::kLong; - break; - } else if (argStr == "wf" || argStr == "webOff") { - outArgs.fWeb = "off"; - } - - } else if (!outArgs.fFileName.empty()) { - outArgs.fPrintHelp = RootBrowseArgs::EPrintUsage::kShort; - break; - } else { - outArgs.fFileName = arg; - } + if (opts.GetSwitch("help")) { + outArgs.fPrintHelp = RootBrowseArgs::EPrintUsage::kLong; + return outArgs; } + if (auto web = opts.GetFlagValue("web"); !web.empty()) + outArgs.fWeb = web; + + if (opts.GetSwitch("webOff")) + outArgs.fWeb = "off"; + + if (opts.GetArgs().empty()) + outArgs.fPrintHelp = RootBrowseArgs::EPrintUsage::kShort; + else + outArgs.fFileName = opts.GetArgs()[0]; + return outArgs; } diff --git a/main/src/rootls.cxx b/main/src/rootls.cxx index 573d34f36391c..7e29b3f11ff39 100644 --- a/main/src/rootls.cxx +++ b/main/src/rootls.cxx @@ -22,6 +22,7 @@ #include #include +#include "optparse.hxx" #include "wildcards.hpp" #include @@ -158,7 +159,7 @@ struct RootLsSource { }; struct RootLsArgs { - enum Flags { + enum EFlags { kNone = 0x0, kOneColumn = 0x1, kLongListing = 0x2, @@ -167,7 +168,7 @@ struct RootLsArgs { kRecursiveListing = 0x10, }; - enum class PrintUsage { + enum class EPrintUsage { kNo, kShort, kLong @@ -175,7 +176,7 @@ struct RootLsArgs { std::uint32_t fFlags = 0; std::vector fSources; - PrintUsage fPrintUsageAndExit = PrintUsage::kNo; + EPrintUsage fPrintUsageAndExit = EPrintUsage::kNo; }; struct V2i { @@ -677,79 +678,40 @@ static RootLsTree GetMatchingPathsInFile(std::string_view fileName, std::string_ return nodeTree; } -static bool MatchShortFlag(char arg, char matched, RootLsArgs::Flags flagVal, std::uint32_t &outFlags) +static RootLsArgs ParseArgs(const char **args, int nArgs) { - if (arg == matched) { - outFlags |= flagVal; - return true; + RootLsArgs outArgs; + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-h", "--help"}); + opts.AddFlag({"-1", "--oneColumn"}); + opts.AddFlag({"-l", "--longListing"}); + opts.AddFlag({"-t", "--treeListing"}); + opts.AddFlag({"-R", "--rntupleListing"}); + opts.AddFlag({"-r", "--recursiveListing"}); + + opts.Parse(args, nArgs); + for (const auto &err : opts.GetErrors()) + R__LOG_ERROR(RootLsChannel()) << err; + + if (opts.GetSwitch("help")) { + outArgs.fPrintUsageAndExit = RootLsArgs::EPrintUsage::kLong; + return outArgs; } - return false; -} -static bool MatchLongFlag(const char *arg, const char *matched, RootLsArgs::Flags flagVal, std::uint32_t &outFlags) -{ - if (strcmp(arg, matched) == 0) { - outFlags |= flagVal; - return true; + if (!opts.GetErrors().empty() || opts.GetArgs().empty()) { + outArgs.fPrintUsageAndExit = RootLsArgs::EPrintUsage::kShort; + return outArgs; } - return false; -} -static RootLsArgs ParseArgs(const char **args, int nArgs) -{ - RootLsArgs outArgs; - std::vector sourceArgs; - - // First match all flags, then process positional arguments (since we need the flags to properly process them). - for (int i = 0; i < nArgs; ++i) { - const char *arg = args[i]; - if (arg[0] == '-') { - ++arg; - if (arg[0] == '-') { - // long flag - ++arg; - bool matched = MatchLongFlag(arg, "oneColumn", RootLsArgs::kOneColumn, outArgs.fFlags) || - MatchLongFlag(arg, "longListing", RootLsArgs::kLongListing, outArgs.fFlags) || - MatchLongFlag(arg, "treeListing", RootLsArgs::kTreeListing, outArgs.fFlags) || - MatchLongFlag(arg, "recursiveListing", RootLsArgs::kRecursiveListing, outArgs.fFlags) || - MatchLongFlag(arg, "rntupleListing", RootLsArgs::kRNTupleListing, outArgs.fFlags); - if (!matched) { - if (strcmp(arg, "help") == 0) { - outArgs.fPrintUsageAndExit = RootLsArgs::PrintUsage::kLong; - } else { - R__LOG_ERROR(RootLsChannel()) << "unrecognized argument: --" << arg << "\n"; - if (outArgs.fPrintUsageAndExit == RootLsArgs::PrintUsage::kNo) - outArgs.fPrintUsageAndExit = RootLsArgs::PrintUsage::kShort; - } - } - } else { - // short flag - while (*arg) { - bool matched = MatchShortFlag(*arg, '1', RootLsArgs::kOneColumn, outArgs.fFlags) || - MatchShortFlag(*arg, 'l', RootLsArgs::kLongListing, outArgs.fFlags) || - MatchShortFlag(*arg, 't', RootLsArgs::kTreeListing, outArgs.fFlags) || - MatchShortFlag(*arg, 'r', RootLsArgs::kRecursiveListing, outArgs.fFlags) || - MatchShortFlag(*arg, 'R', RootLsArgs::kRNTupleListing, outArgs.fFlags); - if (!matched) { - if (*arg == 'h') { - outArgs.fPrintUsageAndExit = RootLsArgs::PrintUsage::kLong; - } else { - R__LOG_ERROR(RootLsChannel()) << "unrecognized argument: -" << *arg << "\n"; - if (outArgs.fPrintUsageAndExit == RootLsArgs::PrintUsage::kNo) - outArgs.fPrintUsageAndExit = RootLsArgs::PrintUsage::kShort; - } - } - ++arg; - } - } - } else { - sourceArgs.push_back(i); - } - } + outArgs.fFlags |= opts.GetSwitch("oneColumn") * RootLsArgs::kOneColumn; + outArgs.fFlags |= opts.GetSwitch("longListing") * RootLsArgs::kLongListing; + outArgs.fFlags |= opts.GetSwitch("treeListing") * RootLsArgs::kTreeListing; + outArgs.fFlags |= opts.GetSwitch("recursiveListing") * RootLsArgs::kRecursiveListing; + outArgs.fFlags |= opts.GetSwitch("rntupleListing") * RootLsArgs::kRNTupleListing; // Positional arguments - for (int argIdx : sourceArgs) { - const char *arg = args[argIdx]; + for (const auto &argStr : opts.GetArgs()) { + const char *arg = argStr.c_str(); RootLsSource &newSource = outArgs.fSources.emplace_back(); // Handle known URI prefixes @@ -784,9 +746,9 @@ int main(int argc, char **argv) gErrorIgnoreLevel = kError; auto args = ParseArgs(const_cast(argv) + 1, argc - 1); - if (args.fPrintUsageAndExit != RootLsArgs::PrintUsage::kNo) { + if (args.fPrintUsageAndExit != RootLsArgs::EPrintUsage::kNo) { std::cerr << "usage: rootls [-1hltr] FILE [FILE ...]\n"; - if (args.fPrintUsageAndExit == RootLsArgs::PrintUsage::kLong) { + if (args.fPrintUsageAndExit == RootLsArgs::EPrintUsage::kLong) { std::cerr << kLongHelp; return 0; } diff --git a/main/test/CMakeLists.txt b/main/test/CMakeLists.txt new file mode 100644 index 0000000000000..1bdc8f1f2ab78 --- /dev/null +++ b/main/test/CMakeLists.txt @@ -0,0 +1 @@ +ROOT_ADD_GTEST(optparse optparse_test.cxx) diff --git a/main/test/optparse_test.cxx b/main/test/optparse_test.cxx new file mode 100644 index 0000000000000..71722c1bd017c --- /dev/null +++ b/main/test/optparse_test.cxx @@ -0,0 +1,478 @@ +#include +#include "../src/optparse.hxx" + +#include + +TEST(OptParse, OptParseNull) +{ + ROOT::RCmdLineOpts opts; + opts.Parse(nullptr, 0); + EXPECT_TRUE(opts.GetErrors().empty()); +} + +TEST(OptParse, OptParseEmpty) +{ + ROOT::RCmdLineOpts opts; + + const char *args[] = {""}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); +} + +TEST(OptParse, OptParseNoExpected) +{ + ROOT::RCmdLineOpts opts; + + const char *args[] = {"-h"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Unknown flag: -h"}); +} + +TEST(OptParse, OptParseBoolean) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-h", "--help"}); + opts.AddFlag({"-f"}); + opts.AddFlag({"-g"}); + + const char *args[] = {"-h", "-f"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_TRUE(opts.GetSwitch("h")); + EXPECT_TRUE(opts.GetSwitch("help")); + EXPECT_THROW(opts.GetFlagValue("h"), std::invalid_argument); + EXPECT_THROW(opts.GetSwitch("-h"), std::invalid_argument); + EXPECT_THROW(opts.GetSwitch("--help"), std::invalid_argument); + EXPECT_TRUE(opts.GetSwitch("f")); + EXPECT_FALSE(opts.GetSwitch("g")); +} + +TEST(OptParse, OptParseString) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"--foo", "bar"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetSwitch("foo"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("foo"), "bar"); +} + +TEST(OptParse, OptParseStringEq) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"--foo=bar"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetSwitch("foo"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("foo"), "bar"); +} + +TEST(OptParse, OptParseStringShort) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-f", "--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-f", "bar"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetSwitch("foo"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("foo"), "bar"); +} + +TEST(OptParse, OptParseStringShortMultiChar) +{ + ROOT::RCmdLineOpts opts; + // Short flags may have multiple characters (the "shortness" comes from the fact that it starts with a single `-`). + // Note that it's legal to define a short and long flag with the same name. + opts.AddFlag({"-foo", "--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-foo", "bar"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetSwitch("foo"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("foo"), "bar"); +} + +TEST(OptParse, OptParseStringShortWrong) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-f", "--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-f=bar"}; + opts.Parse(args, std::size(args)); + + // NOTE: in this case `-f=bar` was parsed as grouped short flags + EXPECT_EQ(opts.GetErrors(), std::vector{"Missing argument for flag -f"}); + EXPECT_THROW(opts.GetSwitch("foo"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("foo"), ""); +} + +TEST(OptParse, OptParseStringShortWrong2) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-foo=bar"}; + opts.Parse(args, std::size(args)); + + // NOTE: in this case `-foo=bar` was parsed as a single short flag (the `=` is only valid for long flags) + EXPECT_EQ(opts.GetErrors(), std::vector{"Unknown flag: -foo=bar"}); + EXPECT_THROW(opts.GetSwitch("foo"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("foo"), ""); +} + +TEST(OptParse, OptParseMissingArg) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + opts.AddFlag({"-b"}); + + const char *args[] = {"--foo", "-b"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Missing argument for flag --foo"}); + EXPECT_EQ(opts.GetFlagValue("foo"), ""); + EXPECT_FALSE(opts.GetSwitch("b")); +} + +TEST(OptParse, OptParseMissingArg2) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"--foo"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Missing argument for flag --foo"}); + EXPECT_EQ(opts.GetFlagValue("foo"), ""); +} + +TEST(OptParse, OptParseExtraArg) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}); + + const char *args[] = {"--foo=bar"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Flag --foo does not expect an argument"}); + EXPECT_FALSE(opts.GetSwitch("foo")); +} + +TEST(OptParse, OptParseRepeatedFlagsEqual) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}); + + const char *args[] = {"--foo", "bar", "baz", "--foo"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Flag --foo appeared more than once"}); +} + +TEST(OptParse, OptParseRepeatedFlagsAliased) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo", "-f", "-foo"}); + + const char *args[] = {"--foo", "bar", "-f"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Flag -f appeared more than once"}); +} + +TEST(OptParse, OptParseRepeatedFlagsWithArg) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo", "-f", "-foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"--foo", "bar", "-f", "baz"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Flag -f appeared more than once with the value: bar"}); +} + +TEST(OptParse, OptParseRepeatedFlagsShort) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo", "-f"}); + + const char *args[] = {"-ff"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Flag -f appeared more than once"}); +} + +TEST(OptParse, OptParseGroupedFlags) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}); + opts.AddFlag({"-c"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-abc", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_TRUE(opts.GetSwitch("a")); + EXPECT_TRUE(opts.GetSwitch("b")); + EXPECT_THROW(opts.GetSwitch("c"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("c"), "d"); +} + +TEST(OptParse, OptParseNonGroupedFlags) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}); + opts.AddFlag({"-c"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + // This non-monocharacter short flags prevents flag grouping + opts.AddFlag({"-wf"}); + + const char *args[] = {"-abc", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Unknown flag: -abc"}); +} + +TEST(OptParse, OptParseGroupedFlagsMissingArg) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}); + opts.AddFlag({"-c"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-cab", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Missing argument for flag -c"}); +} + +TEST(OptParse, OptParseGroupedFlagsMissingArg2) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + opts.AddFlag({"-c"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-abc", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Missing argument for flag -b"}); +} + +TEST(OptParse, PositionalArgs) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "c", "-a", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_TRUE(opts.GetSwitch("a")); + EXPECT_EQ(opts.GetFlagValue("b"), "c"); + EXPECT_EQ(opts.GetArgs(), std::vector{"d"}); +} + +TEST(OptParse, OnlyPositionalArgs) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"a", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_FALSE(opts.GetSwitch("a")); + EXPECT_EQ(opts.GetFlagValue("b"), ""); + EXPECT_EQ(opts.GetArgs(), std::vector({"a", "d"})); +} + +TEST(OptParse, PositionalSeparator) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-a", "--", "-b", "d"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_TRUE(opts.GetSwitch("a")); + EXPECT_EQ(opts.GetFlagValue("b"), ""); + EXPECT_EQ(opts.GetArgs(), std::vector({"-b", "d"})); +} + +TEST(OptParse, PositionalSeparator2) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"--", "-b", "-a"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_FALSE(opts.GetSwitch("a")); + EXPECT_EQ(opts.GetFlagValue("b"), ""); + EXPECT_EQ(opts.GetArgs(), std::vector({"-b", "-a"})); +} + +TEST(OptParse, OnlyPositionalSeparator) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"--"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_FALSE(opts.GetSwitch("a")); + EXPECT_EQ(opts.GetFlagValue("b"), ""); + EXPECT_TRUE(opts.GetArgs().empty()); +} + +TEST(OptParse, PositionalSeparatorAsArg) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "--"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetErrors(), std::vector{"Missing argument for flag -b"}); +} + +TEST(OptParse, ParseFlagAsInt) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "42"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_EQ(opts.GetFlagValueAs("b"), 42); + EXPECT_EQ(opts.GetFlagValue("b"), "42"); + EXPECT_FLOAT_EQ(opts.GetFlagValueAs("b").value(), 42.f); +} + +TEST(OptParse, ParseFlagAsIntInvalid) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "42a"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetFlagValueAs("b"), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("b"), "42a"); +} + +TEST(OptParse, ParseFlagAsIntInvalid2) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "4.2"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetFlagValueAs("b"), std::invalid_argument); +} + +TEST(OptParse, ParseFlagAsIntOutOfRange) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "2000000"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetFlagValueAs("b"), std::out_of_range); +} + +TEST(OptParse, ParseFlagAsFloat) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", ".2"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_FLOAT_EQ(opts.GetFlagValueAs("b").value(), .2f); + EXPECT_THROW(opts.GetFlagValueAs("b").value(), std::invalid_argument); + EXPECT_EQ(opts.GetFlagValue("b"), ".2"); +} + +TEST(OptParse, ParseFlagAsFloatInvalid) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"-b", "42as"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_THROW(opts.GetFlagValueAs("b"), std::invalid_argument); +} + +TEST(OptParse, ParseFlagAsNumericNotThere) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-b"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + const char *args[] = {"bb"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_EQ(opts.GetFlagValueAs("b"), std::nullopt); + EXPECT_EQ(opts.GetFlagValueAs("b"), std::nullopt); + EXPECT_EQ(opts.GetFlagValue("b"), ""); +} + +TEST(OptParse, PositionalAtFirstPlace) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + opts.AddFlag({"-abc"}); + + const char *args[] = {"somename", "--foo", "bar", "-abc"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_EQ(opts.GetFlagValue("foo"), "bar"); + EXPECT_EQ(opts.GetSwitch("abc"), true); + EXPECT_EQ(opts.GetArgs(), std::vector{"somename"}); +} + +TEST(OptParse, PositionalMixedWithFlags) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--noarg"}); + + const char *args[] = {"somename", "--noarg", "bar"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_EQ(opts.GetSwitch("noarg"), true); + EXPECT_EQ(opts.GetArgs(), std::vector({"somename", "bar"})); +}