diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index e2b82baf5d2ab..3d2069d3d1d38 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -34,11 +34,6 @@ elseif(NOT WIN32) set_property(TARGET Core APPEND PROPERTY BUILD_RPATH $ORIGIN) endif() -generateHeader(Core - ${CMAKE_SOURCE_DIR}/core/base/src/root-argparse.py - ${CMAKE_BINARY_DIR}/ginclude/TApplicationCommandLineOptionsHelp.h -) - add_dependencies(Core CLING rconfigure) target_link_libraries(Core diff --git a/core/base/CMakeLists.txt b/core/base/CMakeLists.txt index e31697bcebe70..f6b8a0be390cd 100644 --- a/core/base/CMakeLists.txt +++ b/core/base/CMakeLists.txt @@ -223,6 +223,7 @@ endif() target_include_directories(Core PUBLIC $ ) +target_include_directories(Core PRIVATE ${CMAKE_SOURCE_DIR}/core/utils/inc) if(ROOT_NEED_STDCXXFS) target_link_libraries(Core PRIVATE stdc++fs) diff --git a/core/base/src/TApplication.cxx b/core/base/src/TApplication.cxx index 0c0c9515930bf..e03ee8f21fd25 100644 --- a/core/base/src/TApplication.cxx +++ b/core/base/src/TApplication.cxx @@ -44,12 +44,51 @@ TApplication (see TRint). #include "TClassEdit.h" #include "TMethod.h" #include "TDataMember.h" -#include "TApplicationCommandLineOptionsHelp.h" +#include "optparse.hxx" #include "TPRegexp.h" +#include #include #include #include +// TODO: deduplicate this with rootx.cxx +constexpr static const char *kCommandLineOptionsHelp = R"RAW( +usage: root [-b] [-x] [-e] [-n] [-t] [-q] [-l] [-config] [-h|--help] [--version] + [--notebook] [--web=] [dir] [data1.root...dataN.root] [file1.C...fileN.C] + [file1_C.so...fileN_C.so] [anyfile1..anyfileN] + + +root is an interactive interpreter of C++ code using Cling and the ROOT framework. +For more information on ROOT, please refer to https://root.cern/ +An extensive Users Guide and API Reference are available from that website. + + +OPTIONS: + -b, --batch Run in batch mode without graphics + -x, --exit-on-exceptions Exit on exceptions + -e, --execute Execute the command passed between single quotes + -n, --no-logon-logoff Do not execute logon and logoff macros as specified in .rootrc + -t, --enable-threading Enable thread-safety and implicit multi-threading (IMT) + -q, --quit-after-processing Exit after processing command line macro files + -l, --no-banner Do not show the ROOT banner + -config print ./configure options + -h, -?, --help Show summary of options + --version Show the ROOT version + --notebook Execute ROOT notebook + --web Use web-based display for graphics, browser, geometry + --web= Use the specified web-based display such as chrome, firefox, qt6 + For more options see the documentation of TROOT::SetWebDisplay() + --web=off Disable any kind of web-based display + [dir] if dir is a valid directory cd to it before executing + [data1.root...dataN.root] Open the given ROOT files; remote protocols (such as http://) are supported + [file1.C...fileN.C] Execute the ROOT macro file1.C ... fileN.C + Compilation flags as well as macro arguments can be passed, see format in https://root.cern/manual/root_macros_and_shared_libraries/ + [file1_C.so...fileN_C.so] Load and execute file1_C.so ... fileN_C.so (or .dll if on Windows) + They should be already-compiled ROOT macros (shared libraries) or: + regular user shared libraries e.g. userlib.so with a function userlib(args) + [anyfile1..anyfileN] All other arguments pointing to existing files will be checked to see if they are ROOT Files (checking the MIME type inside the file) and if they are not they will be handled as a ROOT macro file +)RAW"; + TApplication *gApplication = nullptr; Bool_t TApplication::fgGraphNeeded = kFALSE; Bool_t TApplication::fgGraphInit = kFALSE; @@ -346,8 +385,6 @@ char *TApplication::Argv(Int_t index) const void TApplication::GetOptions(Int_t *argc, char **argv) { - static char null[1] = { "" }; - fNoLog = kFALSE; fQuit = kFALSE; fFiles = nullptr; @@ -355,220 +392,239 @@ void TApplication::GetOptions(Int_t *argc, char **argv) if (!argc) return; - int i, j; - TString pwd; + // Due to --web accepting 0 or 1 arguments we can't process it with RCmdLineOpts, so do a preprocessing for it. + for (int i = 1; i < *argc; ++i) { + if (strncmp(argv[i], "--web", 5) != 0) + continue; - for (i = 1; i < *argc; i++) { - if (!strcmp(argv[i], "-?") || !strncmp(argv[i], "-h", 2) || - !strncmp(argv[i], "--help", 6)) { - fprintf(stderr, kCommandLineOptionsHelp); - Terminate(0); - } else if (!strncmp(argv[i], "--version", 9)) { - fprintf(stderr, "ROOT Version: %s\n", gROOT->GetVersion()); - fprintf(stderr, "Built for %s on %s\n", - gSystem->GetBuildArch(), - gROOT->GetGitDate()); - - fprintf(stderr, "From %s@%s\n", - gROOT->GetGitBranch(), - gROOT->GetGitCommit()); - - Terminate(0); - } else if (!strcmp(argv[i], "-config")) { - fprintf(stderr, "ROOT ./configure options:\n%s\n", gROOT->GetConfigOptions()); - Terminate(0); - } else if (!strcmp(argv[i], "-a")) { - fprintf(stderr, "ROOT splash screen is not visible with root.exe, use root instead.\n"); - Terminate(0); - } else if (!strcmp(argv[i], "-b")) { - MakeBatch(); - argv[i] = null; - } else if (!strcmp(argv[i], "-n")) { - fNoLog = kTRUE; - argv[i] = null; - } else if (!strcmp(argv[i], "-t")) { - ROOT::EnableImplicitMT(); - // EnableImplicitMT() only enables thread safety if IMT was configured; - // enable thread safety even with IMT off: - ROOT::EnableThreadSafety(); - argv[i] = null; - } else if (!strcmp(argv[i], "-q")) { - fQuit = kTRUE; - argv[i] = null; - } else if (!strcmp(argv[i], "-l")) { - // used by front-end program to not display splash screen - fNoLogo = kTRUE; - argv[i] = null; - } else if (!strcmp(argv[i], "-x")) { - fExitOnException = kExit; - argv[i] = null; - } else if (!strcmp(argv[i], "-splash")) { - // used when started by front-end program to signal that - // splash screen can be popped down (TRint::PrintLogo()) - argv[i] = null; - } else if (strncmp(argv[i], "--web", 5) == 0) { - // the web mode is requested - const char *opt = argv[i] + 5; - argv[i] = null; - gROOT->SetWebDisplay((*opt == '=') ? opt + 1 : ""); - } else if (!strcmp(argv[i], "-e")) { - argv[i] = null; - ++i; - - if ( i < *argc ) { - if (!fFiles) fFiles = new TObjArray; - TObjString *expr = new TObjString(argv[i]); - expr->SetBit(kExpression); - fFiles->Add(expr); - argv[i] = null; - } else { - Warning("GetOptions", "-e must be followed by an expression."); - } - } else if (!strcmp(argv[i], "--")) { - TObjString* macro = nullptr; - bool warnShown = false; - - if (fFiles) { - for (auto f: *fFiles) { - TObjString *file = dynamic_cast(f); - if (!file) { - if (!dynamic_cast(f)) { - Error("GetOptions()", "Inconsistent file entry (not a TObjString)!"); - if (f) - f->Dump(); - } // else we did not find the file. - continue; - } + if (argv[i][5] == '=') { + gROOT->SetWebDisplay(argv[i] + 6); + } else { + gROOT->SetWebDisplay(""); + } - if (file->TestBit(kExpression)) - continue; - if (file->String().EndsWith(".root")) - continue; - if (file->String().Contains('(')) - continue; + // Remove this flag from argc/argv, otherwise TRint's ctor will complain. + for (int j = i + 1; j < *argc; ++j) { + argv[j - 1] = argv[j]; + } + *argc -= 1; + break; + } + + ROOT::RCmdLineOpts::RSettings settings; + settings.fIgnoreUnknownFlags = true; + ROOT::RCmdLineOpts opts{settings}; + opts.AddFlag({"-b", "--batch"}); + opts.AddFlag({"-x", "--exit-on-exceptions"}); + opts.AddFlag({"-e", "--execute"}, ROOT::RCmdLineOpts::EFlagType::kWithArg, "", + ROOT::RCmdLineOpts::kFlagAllowMultiple); + opts.AddFlag({"-n", "--no-logon-logoff"}); + opts.AddFlag({"-t", "--enable-threading"}); + opts.AddFlag({"-q", "--quit-after-processing"}); + opts.AddFlag({"-l", "--no-banner"}); + opts.AddFlag({"-a"}); + opts.AddFlag({"-splash"}); // this option is ignored. + opts.AddFlag({"-config"}); + opts.AddFlag({"-h", "-?", "--help"}); + opts.AddFlag({"--version"}); + + opts.Parse(argv + 1, *argc - 1); + for (const auto &err : opts.GetErrors()) { + fprintf(stderr, "%s\n", err.c_str()); + } + if (!opts.GetErrors().empty()) + Terminate(0); - if (macro && !warnShown && (warnShown = true)) - Warning("GetOptions", "-- is used with several macros. " - "The arguments will be passed to the last one."); + if (opts.GetSwitch("help")) { + fprintf(stderr, kCommandLineOptionsHelp); + Terminate(0); + } + if (opts.GetSwitch("version")) { + fprintf(stderr, "ROOT Version: %s\n", gROOT->GetVersion()); + fprintf(stderr, "Built for %s on %s\n", + gSystem->GetBuildArch(), + gROOT->GetGitDate()); + fprintf(stderr, "From %s@%s\n", + gROOT->GetGitBranch(), + gROOT->GetGitCommit()); + Terminate(0); + } + if (opts.GetSwitch("config")) { + fprintf(stderr, "ROOT ./configure options:\n%s\n", gROOT->GetConfigOptions()); + Terminate(0); + } + if (opts.GetSwitch("a")) { + fprintf(stderr, "ROOT splash screen is not visible with root.exe, use root instead.\n"); + Terminate(0); + } + if (opts.GetSwitch("b")) { + MakeBatch(); + } + if (opts.GetSwitch("n")) { + fNoLog = kTRUE; + } + if (opts.GetSwitch("t")) { + ROOT::EnableImplicitMT(); + // EnableImplicitMT() only enables thread safety if IMT was configured; + // enable thread safety even with IMT off: + ROOT::EnableThreadSafety(); + } + if (opts.GetSwitch("q")) { + fQuit = kTRUE; + } + if (opts.GetSwitch("l")) { + // used by front-end program to not display splash screen + fNoLogo = kTRUE; + } + if (opts.GetSwitch("x")) { + fExitOnException = kExit; + } - macro = file; - } - } + for (auto cmd : opts.GetFlagValues("e")) { + if (!fFiles) fFiles = new TObjArray; + TObjString *expr = new TObjString(std::string(cmd).c_str()); + expr->SetBit(kExpression); + fFiles->Add(expr); + } - if (macro) { - argv[i] = null; - ++i; - TString& str = macro->String(); + const auto &positionalArgs = opts.GetArgs(); + const auto lastArgBeforeDashDash = opts.GetFirstPostDashDashArg().value_or(positionalArgs.size()); + + TString pwd; + + // Process all positional arguments before `--` + for (std::size_t i = 0; i < lastArgBeforeDashDash; ++i) { + std::string arg = positionalArgs[i]; + Long64_t size; + Long_t id, flags, modtime; + + auto [argPreParens, argPostParens] = ROOT::SplitAt(arg, '('); + TString expandedDir(argPreParens); + if (gSystem->ExpandPathName(expandedDir)) { + // ROOT-9959: we do not continue if we could not expand the path + continue; + } + TUrl udir(expandedDir, kTRUE); + // remove options and anchor to check the path + TString sfx = udir.GetFileAndOptions(); + TString fln = udir.GetFile(); + sfx.Replace(sfx.Index(fln), fln.Length(), ""); + // 'path' is the full URL without suffixes (options and/or anchor) + TString path = udir.GetFile(); + if (strcmp(udir.GetProtocol(), "file")) { + path = udir.GetUrl(); + path.Replace(path.Index(sfx), sfx.Length(), ""); + } - str += '('; - for (; i < *argc; i++) { - str += argv[i]; - str += ','; - argv[i] = null; + if (argPostParens.empty() && !gSystem->GetPathInfo(path.Data(), &id, &size, &flags, &modtime)) { + if ((flags & 2)) { + // if directory set it in fWorkDir + if (pwd == "") { + pwd = gSystem->WorkingDirectory(); + fWorkDir = expandedDir; + gSystem->ChangeDirectory(expandedDir); + } else if (!strcmp(gROOT->GetName(), "Rint")) { + Warning("GetOptions", "only one directory argument can be specified (%s)", expandedDir.Data()); } - str.EndsWith(",") ? str[str.Length() - 1] = ')' : str += ')'; + } else if (size > 0) { + // if file add to list of files to be processed + if (!fFiles) fFiles = new TObjArray; + fFiles->Add(new TObjString(path.Data())); } else { - Warning("GetOptions", "no macro to pass arguments to was provided. " - "Everything after the -- will be ignored."); - for (; i < *argc; i++) - argv[i] = null; - } - } else if (argv[i][0] != '-' && argv[i][0] != '+') { - Long64_t size; - Long_t id, flags, modtime; - char *arg = strchr(argv[i], '('); - if (arg) *arg = '\0'; - TString expandedDir(argv[i]); - if (gSystem->ExpandPathName(expandedDir)) { - // ROOT-9959: we do not continue if we could not expand the path - continue; + Warning("GetOptions", "file %s has size 0, skipping", expandedDir.Data()); } - TUrl udir(expandedDir, kTRUE); - // remove options and anchor to check the path - TString sfx = udir.GetFileAndOptions(); - TString fln = udir.GetFile(); - sfx.Replace(sfx.Index(fln), fln.Length(), ""); - TString path = udir.GetFile(); - if (strcmp(udir.GetProtocol(), "file")) { - path = udir.GetUrl(); - path.Replace(path.Index(sfx), sfx.Length(), ""); - } - // 'path' is the full URL without suffices (options and/or anchor) - if (arg) *arg = '('; - if (!arg && !gSystem->GetPathInfo(path.Data(), &id, &size, &flags, &modtime)) { - if ((flags & 2)) { - // if directory set it in fWorkDir - if (pwd == "") { - pwd = gSystem->WorkingDirectory(); - fWorkDir = expandedDir; - gSystem->ChangeDirectory(expandedDir); - argv[i] = null; - } else if (!strcmp(gROOT->GetName(), "Rint")) { - Warning("GetOptions", "only one directory argument can be specified (%s)", expandedDir.Data()); - } - } else if (size > 0) { - // if file add to list of files to be processed - if (!fFiles) fFiles = new TObjArray; - fFiles->Add(new TObjString(path.Data())); - argv[i] = null; + } else { + if (TString(udir.GetFile()).EndsWith(".root")) { + if (!strcmp(udir.GetProtocol(), "file")) { + // file ending on .root but does not exist, likely a typo + // warn user if plain root... + if (!strcmp(gROOT->GetName(), "Rint")) + Warning("GetOptions", "file %s not found", expandedDir.Data()); } else { - Warning("GetOptions", "file %s has size 0, skipping", expandedDir.Data()); + // remote file, give it the benefit of the doubt and add it to list of files + if (!fFiles) fFiles = new TObjArray; + fFiles->Add(new TObjString(arg.c_str())); } } else { - if (TString(udir.GetFile()).EndsWith(".root")) { - if (!strcmp(udir.GetProtocol(), "file")) { - // file ending on .root but does not exist, likely a typo - // warn user if plain root... - if (!strcmp(gROOT->GetName(), "Rint")) - Warning("GetOptions", "file %s not found", expandedDir.Data()); - } else { - // remote file, give it the benefit of the doubt and add it to list of files - if (!fFiles) fFiles = new TObjArray; - fFiles->Add(new TObjString(argv[i])); - argv[i] = null; - } + TString mode,fargs,io; + TString fname = gSystem->SplitAclicMode(expandedDir,mode,fargs,io); + char *mac; + if (!fFiles) fFiles = new TObjArray; + if ((mac = gSystem->Which(TROOT::GetMacroPath(), fname, + kReadPermission))) { + // if file add to list of files to be processed + fFiles->Add(new TObjString(arg.c_str())); + delete [] mac; } else { - TString mode,fargs,io; - TString fname = gSystem->SplitAclicMode(expandedDir,mode,fargs,io); - char *mac; - if (!fFiles) fFiles = new TObjArray; - if ((mac = gSystem->Which(TROOT::GetMacroPath(), fname, - kReadPermission))) { - // if file add to list of files to be processed - fFiles->Add(new TObjString(argv[i])); - argv[i] = null; - delete [] mac; - } else { - // if file add an invalid entry to list of files to be processed - fFiles->Add(new TNamed("NOT FOUND!", argv[i])); - // only warn if we're plain root, - // other progs might have their own params - if (!strcmp(gROOT->GetName(), "Rint")) { - Error("GetOptions", "macro %s not found", fname.Data()); - // Return 2 as the Python interpreter does in case the macro - // is not found. - Terminate(2); - } + // if file add an invalid entry to list of files to be processed + fFiles->Add(new TNamed("NOT FOUND!", arg)); + // only warn if we're plain root, + // other progs might have their own params + if (!strcmp(gROOT->GetName(), "Rint")) { + Error("GetOptions", "macro %s not found", fname.Data()); + // Return 2 as the Python interpreter does in case the macro + // is not found. + Terminate(2); } } } } - // ignore unknown options } + // Process positional arguments after `--` as arguments for the macro. + // This is only valid if we passed at least one macro and will be considered arguments for the last one passed. + if (lastArgBeforeDashDash != positionalArgs.size()) { + TObjString* macro = nullptr; + bool warnShown = false; + if (fFiles) { + for (auto f: *fFiles) { + TObjString *file = dynamic_cast(f); + if (!file) { + if (!dynamic_cast(f)) { + Error("GetOptions()", "Inconsistent file entry (not a TObjString)!"); + if (f) + f->Dump(); + } // else we did not find the file. + continue; + } + + if (file->TestBit(kExpression)) + continue; + if (file->String().EndsWith(".root")) + continue; + if (file->String().Contains('(')) + continue; + + if (macro && !warnShown) { + warnShown = true; + Warning("GetOptions", "-- is used with several macros. " + "The arguments will be passed to the last one."); + } + + macro = file; + } + } + + if (macro) { + TString& str = macro->String(); + str += '(' + ROOT::Join(",", positionalArgs.begin() + lastArgBeforeDashDash, positionalArgs.end()) + ')'; + } else { + Warning("GetOptions", "no macro to pass arguments to was provided. " + "Everything after the -- will be ignored."); + } + } + // go back to startup directory if (pwd != "") gSystem->ChangeDirectory(pwd); // remove handled arguments from argument array - j = 0; - for (i = 0; i < *argc; i++) { - if (strcmp(argv[i], "")) { - argv[j] = argv[i]; - j++; - } + int j = 1; + for (std::size_t idx : opts.GetUnprocessedArgsIndices()) { + argv[j++] = argv[idx + 1]; } - + // Last argv must be null (see https://en.cppreference.com/cpp/language/main_function) + argv[j] = nullptr; *argc = j; } diff --git a/core/foundation/inc/ROOT/StringUtils.hxx b/core/foundation/inc/ROOT/StringUtils.hxx index 35964703f55eb..60cc23aa87e35 100644 --- a/core/foundation/inc/ROOT/StringUtils.hxx +++ b/core/foundation/inc/ROOT/StringUtils.hxx @@ -19,11 +19,38 @@ #include #include #include +#include namespace ROOT { std::vector Split(std::string_view str, std::string_view delims, bool skipEmpty = false); +/// Given a string `str`, returns a pair of string views into it: the first containing the substring preceding the +/// first instance of `splitter` and the second containing the substring following it. Note that: +/// 1. the first instance of `splitter` will not appear in either string; +/// 2. if `splitter` does not appear, or appears as the last character, the second string view will be empty; +/// 3. if `splitter` appears as the first character, the first string view will be empty; +/// 4. if `str` is empty so will both views be. +/// IMPORTANT: The lifetime of the returned string views is the same as `str`. +std::pair SplitAt(std::string_view str, char splitter); + +/** + * \brief Concatenate a list of strings with a separator + * \tparam StringCollection_t Any container of strings (vector, initializer_list, ...) + * \param[in] sep Separator inbetween the strings. + * \param[in] begin Beginning of the strings container + * \param[in] end Past-the-end iterator of the strings container + * \return the sep-delimited concatenation of strings + */ +template +std::string Join(const std::string &sep, InputIt_t begin, InputIt_t end) +{ + if (begin == end) + return ""; + + return std::accumulate(std::next(begin), end, *begin, [&sep](auto const &a, auto const &b) { return a + sep + b; }); +} + /** * \brief Concatenate a list of strings with a separator * \tparam StringCollection_t Any container of strings (vector, initializer_list, ...) @@ -34,10 +61,7 @@ std::vector Split(std::string_view str, std::string_view delims, bo template std::string Join(const std::string &sep, StringCollection_t &&strings) { - if (strings.empty()) - return ""; - return std::accumulate(std::next(std::begin(strings)), std::end(strings), strings[0], - [&sep](auto const &a, auto const &b) { return a + sep + b; }); + return Join(sep, std::begin(strings), std::end(strings)); } std::string Round(double value, double error, unsigned int cutoff = 1, std::string_view delim = "#pm"); diff --git a/core/foundation/src/StringUtils.cxx b/core/foundation/src/StringUtils.cxx index f3b4fdd7aeb8a..b89a58215b5b2 100644 --- a/core/foundation/src/StringUtils.cxx +++ b/core/foundation/src/StringUtils.cxx @@ -40,6 +40,14 @@ std::vector Split(std::string_view str, std::string_view delims, bo return out; } +std::pair SplitAt(std::string_view str, char splitter) +{ + auto pos = str.find(splitter); + if (pos == std::string_view::npos) + return {str, ""}; + return {str.substr(0, pos), str.substr(pos + 1)}; +} + /** * \brief Convert (round) a value and its uncertainty to string using one or two significant digits of the error * \param error the error. If the error is negative or zero, only the value is returned with no specific rounding diff --git a/core/foundation/test/testStringUtils.cxx b/core/foundation/test/testStringUtils.cxx index 6aee05b6da38b..2264f2134f7b8 100644 --- a/core/foundation/test/testStringUtils.cxx +++ b/core/foundation/test/testStringUtils.cxx @@ -81,3 +81,14 @@ TEST(StringUtils, Round) EXPECT_EQ(ROOT::Round(-30000., 1000000000., 0), "(-0#pm1)*1e9"); EXPECT_EQ(ROOT::Round(110., 0.24, 1, "+-"), "110.0+-0.2"); } + +TEST(StringUtils, SplitAt) { + const auto p = [] (auto a, auto b) { return std::make_pair(a, b); }; + + EXPECT_EQ(ROOT::SplitAt("foo, bar", ','), p("foo", " bar")); + EXPECT_EQ(ROOT::SplitAt("foo, bar,", ','), p("foo", " bar,")); + EXPECT_EQ(ROOT::SplitAt(", bar,", ','), p("", " bar,")); + EXPECT_EQ(ROOT::SplitAt("foo,bar", ' '), p("foo,bar", "")); + EXPECT_EQ(ROOT::SplitAt("", ' '), p("", "")); + EXPECT_EQ(ROOT::SplitAt(",,,,", ','), p("", ",,,")); +} diff --git a/core/utils/inc/optparse.hxx b/core/utils/inc/optparse.hxx index fa5d6762bdbad..e234613b807de 100644 --- a/core/utils/inc/optparse.hxx +++ b/core/utils/inc/optparse.hxx @@ -111,7 +111,10 @@ public: struct RSettings { /// Affects how flags are parsed (\see EFlagTreatment). - EFlagTreatment fFlagTreatment; + EFlagTreatment fFlagTreatment = EFlagTreatment::kDefault; + bool fIgnoreUnknownFlags = false; + + RSettings() {} }; enum class EFlagType { @@ -136,8 +139,14 @@ public: private: RSettings fSettings; + /// Flags, in order of appearance std::vector fFlags; + /// Positional arguments, in order of appearance std::vector fArgs; + /// Index of the first element in fArgs that appeared after `--`. + std::optional fFirstPostDashDashArg; + /// Indices of all args passed to Parse() that were skipped. + std::vector fUnprocessedArgsIndices; struct RExpectedFlag { EFlagType fFlagType = EFlagType::kSwitch; @@ -174,7 +183,7 @@ private: } public: - explicit RCmdLineOpts(RSettings settings = {EFlagTreatment::kDefault}) : fSettings(settings) {} + explicit RCmdLineOpts(RSettings settings = {}) : fSettings(settings) {} /// Returns all parsing errors const std::vector &GetErrors() const { return fErrors; } @@ -422,7 +431,7 @@ public: return values; } - void Parse(const char **args, std::size_t nArgs) + void Parse(const char *const *const args, std::size_t nArgs) { bool forcePositional = false; @@ -435,11 +444,12 @@ public: // into flags "a", "b", and "c", which will be stored in `argStr`). std::vector argStr; - for (std::size_t i = 0; i < nArgs && fErrors.empty(); ++i) { - const char *arg = args[i]; + for (std::size_t argIndex = 0; argIndex < nArgs && fErrors.empty(); ++argIndex) { + const char *arg = args[argIndex]; const char *const argOrig = arg; + const auto argIndexOrig = argIndex; - if (strcmp(arg, "--") == 0) { + if (strcmp(arg, "--") == 0 && !forcePositional) { forcePositional = true; continue; } @@ -447,6 +457,8 @@ public: bool isFlag = !forcePositional && arg[0] == '-'; if (!isFlag) { // positional argument + if (forcePositional && !fFirstPostDashDashArg) + fFirstPostDashDashArg = fArgs.size(); fArgs.push_back(arg); continue; } @@ -484,9 +496,9 @@ public: nxtArgIsTentative = false; } else { argStr.push_back(std::string_view(arg)); - if (i < nArgs - 1 && args[i + 1][0] != '-') { - nxtArgStr = args[i + 1]; - ++i; + if (argIndex < nArgs - 1 && args[argIndex + 1][0] != '-') { + nxtArgStr = args[argIndex + 1]; + ++argIndex; } } } @@ -500,9 +512,9 @@ public: } argStr.push_back(std::string_view(arg)); - if (i < nArgs - 1 && args[i + 1][0] != '-') { - nxtArgStr = args[i + 1]; - ++i; + if (argIndex < nArgs - 1 && args[argIndex + 1][0] != '-') { + nxtArgStr = args[argIndex + 1]; + ++argIndex; } } @@ -513,8 +525,13 @@ public: const auto *exp = GetExpectedFlag(argS); if (!exp) { - fErrors.push_back(std::string("Unknown flag: ") + argOrig); - break; + if (fSettings.fIgnoreUnknownFlags) { + fUnprocessedArgsIndices.push_back(argIndexOrig); + continue; + } else { + fErrors.push_back(std::string("Unknown flag: ") + argOrig); + break; + } } // In Prefix mode, check if the returned expected flag is shorter than `argS`. This can mean two things: @@ -526,9 +543,12 @@ public: // {flag: "-Dfoo", arg: "bar"}, rather than {flag: "-D", arg: "foo=bar"}. if ((exp->fOpts & kFlagPrefixArg) && argS.size() > exp->fName.size()) { if (nxtArgIsTentative) { - i -= !nxtArgStr.empty(); // if we had already picked a candidate next arg, undo that. + argIndex -= !nxtArgStr.empty(); // if we had already picked a candidate next arg, undo that. nxtArgStr = argS.substr(exp->fName.size()); nxtArgIsTentative = false; + } else if (fSettings.fIgnoreUnknownFlags) { + fUnprocessedArgsIndices.push_back(argIndexOrig); + continue; } else { fErrors.push_back(std::string("Unknown flag: ") + argOrig); break; @@ -571,7 +591,7 @@ public: } else { if (!nxtArg.empty()) { if (nxtArgIsTentative) - --i; + --argIndex; else fErrors.push_back("Flag " + exp->AsStr() + " does not expect an argument"); } @@ -587,6 +607,15 @@ public: break; } } + + /// Returns the index of the first positional argument that appeared after `--`. If not null, it is guaranteed + /// to be less than the size of GetArgs(). + std::optional GetFirstPostDashDashArg() const { return fFirstPostDashDashArg; } + + /// Returns a list of the indices of all args passed to Parse() that were not parsed. + /// This will typically be empty, unless the RCmdLineOpts was created with `fIgnoreUnknownFlags == true`, in which + /// case this will contain all skipped unknown flags. + const std::vector &GetUnprocessedArgsIndices() const { return fUnprocessedArgsIndices; } }; } // namespace ROOT diff --git a/core/utils/test/optparse_test.cxx b/core/utils/test/optparse_test.cxx index 83be163eee82a..cd269f0d7d21b 100644 --- a/core/utils/test/optparse_test.cxx +++ b/core/utils/test/optparse_test.cxx @@ -771,3 +771,42 @@ TEST(OptParse, DuplicateFlagAlias) EXPECT_STREQ(ex.what(), "The same flag `--a` was passed multiple times to AddFlag()."); } } + +TEST(OptParse, IgnoreUnknownFlags) +{ + ROOT::RCmdLineOpts::RSettings settings; + settings.fIgnoreUnknownFlags = true; + ROOT::RCmdLineOpts opts{settings}; + + const char *args[] = {"somename", "-f", "foo", "--help"}; + opts.Parse(args, std::size(args)); + + EXPECT_TRUE(opts.GetErrors().empty()); + EXPECT_EQ(opts.GetUnprocessedArgsIndices(), std::vector({1, 3})); +} + +TEST(OptParse, MultipleDashDash) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"-a"}); + const char *args[] = {"--", "--", "--"}; + opts.Parse(args, std::size(args)); + EXPECT_EQ(opts.GetArgs(), std::vector({"--", "--"})); +} + +TEST(OptParse, PostDashDash) +{ + ROOT::RCmdLineOpts opts; + opts.AddFlag({"--foo"}); + opts.AddFlag({"-a"}); + opts.AddFlag({"--with-arg"}, ROOT::RCmdLineOpts::EFlagType::kWithArg); + + // Note that only the first `--` is interpreted as special. Also, all "flags" coming after it are considered + // positional arguments. + const char *args[] = {"--foo", "first", "-a", "second", "--with-arg", "arg", + "--", "third", "-c", "fourth", "-a", "--"}; + opts.Parse(args, std::size(args)); + + EXPECT_EQ(opts.GetArgs(), std::vector({"first", "second", "third", "-c", "fourth", "-a", "--"})); + EXPECT_EQ(opts.GetFirstPostDashDashArg(), 2); +}