Skip to content

Commit 96210cc

Browse files
committed
[main] Introduce cmdline option parser to share some code between executables
1 parent 5f26aa6 commit 96210cc

File tree

6 files changed

+891
-124
lines changed

6 files changed

+891
-124
lines changed

main/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,9 @@ endforeach()
119119
add_dependencies(genreflex rootcling)
120120
add_dependencies(rootcint rootcint)
121121

122-
123122
if (TARGET Gui)
124123
ROOT_EXECUTABLE(rootbrowse src/rootbrowse.cxx LIBRARIES RIO Core Rint Gui)
125124
endif()
126125
ROOT_EXECUTABLE(rootls src/rootls.cxx LIBRARIES RIO Tree Core Rint ROOTNTuple)
126+
127+
ROOT_ADD_TEST_SUBDIRECTORY(test)

main/src/optparse.hxx

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
// \file optparse.hxx
2+
///
3+
/// Small utility to parse cmdline options.
4+
///
5+
/// Usage:
6+
/// ~~~{.cpp}
7+
/// MyAppOpts ParseArgs(const char **args, int nArgs) {
8+
/// ROOT::RCmdLineOpts opts;
9+
/// // will parse '-c VAL', '--compress VAL' or '--compress=VAL'
10+
/// opts.AddFlag({"-c", "--compress"}, RCmdLineOpts::EFlagType::kWithArg);
11+
/// // will toggle a switch '--recreate' (no args).
12+
/// opts.AddFlag({"--recreate"});
13+
/// opts.AddFlag({"-o"}, RCmdLineOpts::EFlagType::kWithArg);
14+
///
15+
/// // NOTE: `args` should not contain the program name! It should usually be `argc + 1`.
16+
/// // For example `main(char **argv, int argc)` might call this function as:
17+
/// // `ParseArgs(const_cast<const char**>(argv) + 1, argc - 1);`
18+
/// opts.Parse(args, nArgs);
19+
///
20+
/// // Check for errors:
21+
/// for (const auto &err : opts.GetErrors()) { /* print errors ... */ }
22+
/// if (!opts.GetErrors().empty()) return {};
23+
///
24+
/// // Convert the parsed options from string if necessary:
25+
/// MyAppOpts myOpts;
26+
/// // switch (boolean flag):
27+
/// myOpts.fRecreate = opts.GetSwitch("recreate");
28+
/// // string flag:
29+
/// myOpts.fOutput = opts.GetFlagValue("o");
30+
/// // integer flag:
31+
/// myOpts.fCompression = opts.GetFlagValueAs<int>("compress"); // (could also have used "c" instead of "compress")
32+
/// // positional arguments:
33+
/// myOpts.fArgs = opts.GetArgs();
34+
///
35+
/// return myOpts;
36+
/// }
37+
/// ~~~
38+
///
39+
/// ## Additional Notes
40+
/// If all the short flags you pass (those starting with a single `-`) are 1 character long, the parser will accept
41+
/// grouped flags like "-abc" as equivalent to "-a -b -c". The last flag in the group may also accept an argument, in
42+
/// which case "-abc foo" will count as "-a -b -c foo" where "foo" is the argument to "-c".
43+
///
44+
/// Multiple repeated flags, like `-vvv` are not supported.
45+
///
46+
/// The string "--" is treated as the positional argument separator: all strings after it will be treated as positional
47+
/// arguments even if they start with "-".
48+
///
49+
/// \author Giacomo Parolini <giacomo.parolini@cern.ch>
50+
/// \date 2025-10-09
51+
52+
#ifndef ROOT_OptParse
53+
#define ROOT_OptParse
54+
55+
#include <algorithm>
56+
#include <charconv>
57+
#include <cstring>
58+
#include <iostream>
59+
#include <optional>
60+
#include <sstream>
61+
#include <stdexcept>
62+
#include <string>
63+
#include <string_view>
64+
#include <vector>
65+
66+
namespace ROOT {
67+
68+
class RCmdLineOpts {
69+
public:
70+
enum class EFlagType {
71+
kSwitch,
72+
kWithArg
73+
};
74+
75+
struct RFlag {
76+
std::string fName;
77+
std::string fValue;
78+
std::string fHelp;
79+
};
80+
81+
private:
82+
std::vector<RFlag> fFlags;
83+
std::vector<std::string> fArgs;
84+
// If true, many short flags may be grouped: "-abc" == "-a -b -c".
85+
// This is automatically true if all short flags given are 1 character long, otherwise it's false.
86+
bool fAllowFlagGrouping = true;
87+
88+
struct RExpectedFlag {
89+
EFlagType fFlagType = EFlagType::kSwitch;
90+
std::string fName;
91+
std::string fHelp;
92+
// If >= 0, this flag is an alias of the RExpectedFlag at index fAlias.
93+
int fAlias = -1;
94+
bool fShort = false;
95+
96+
std::string AsStr() const { return std::string(fShort ? "-" : "--") + fName; }
97+
};
98+
std::vector<RExpectedFlag> fExpectedFlags;
99+
std::vector<std::string> fErrors;
100+
101+
const RExpectedFlag *GetExpectedFlag(std::string_view name) const
102+
{
103+
for (const auto &flag : fExpectedFlags) {
104+
if (flag.fName == name)
105+
return &flag;
106+
}
107+
return nullptr;
108+
}
109+
110+
public:
111+
const std::vector<std::string> &GetErrors() const { return fErrors; }
112+
const std::vector<std::string> &GetArgs() const { return fArgs; }
113+
const std::vector<RFlag> &GetFlags() const { return fFlags; }
114+
115+
/// Conveniency method to print any errors to `stream`.
116+
/// \return true if any error was printed
117+
bool ReportErrors(std::ostream &stream = std::cerr) const
118+
{
119+
for (const auto &err : fErrors)
120+
stream << err << "\n";
121+
return !fErrors.empty();
122+
}
123+
124+
/// Defines a new flag (either a switch or a flag with argument). The flag may be referred to as any of the
125+
/// values inside `aliases` (e.g. { "-h", "--help" }). All strings inside `aliases` must start with `-` or `--`
126+
/// and be at least 1 character long (aside the dashes).
127+
/// Flags starting with a single `-` are considered "short", regardless of their actual length.
128+
/// If all short flags are 1 character long, they may be collapsed into one and parsed as individual flags
129+
/// (meaning a string like "-fgk" will be parsed as "-f -g -k") and the final flag may have a following argument.
130+
/// This does NOT happen if any short flag is longer than 1 character, to avoid ambiguity.
131+
void AddFlag(std::initializer_list<std::string_view> aliases, EFlagType type = EFlagType::kSwitch,
132+
std::string_view help = "")
133+
{
134+
int aliasIdx = -1;
135+
for (auto f : aliases) {
136+
auto prefixLen = f.find_first_not_of('-');
137+
if (prefixLen != 1 && prefixLen != 2)
138+
throw std::invalid_argument(std::string("Invalid flag `") + std::string(f) +
139+
"`: flags must start with '-' or '--'");
140+
if (f.size() == prefixLen)
141+
throw std::invalid_argument("Flag name cannot be empty");
142+
143+
fAllowFlagGrouping = fAllowFlagGrouping && (prefixLen > 1 || f.size() == 2);
144+
145+
RExpectedFlag expected;
146+
expected.fFlagType = type;
147+
expected.fName = f.substr(prefixLen);
148+
expected.fHelp = help;
149+
expected.fAlias = aliasIdx;
150+
expected.fShort = prefixLen == 1;
151+
fExpectedFlags.push_back(expected);
152+
if (aliasIdx < 0)
153+
aliasIdx = fExpectedFlags.size() - 1;
154+
}
155+
}
156+
157+
/// If `name` refers to a previously-defined switch (i.e. a boolean flag), gets its value.
158+
/// \throws std::invalid_argument if the flag was undefined or defined as a flag with arguments
159+
bool GetSwitch(std::string_view name) const
160+
{
161+
const auto *exp = GetExpectedFlag(name);
162+
if (!exp)
163+
throw std::invalid_argument(std::string("Flag `") + std::string(name) + "` is not expected");
164+
if (exp->fFlagType != EFlagType::kSwitch)
165+
throw std::invalid_argument(std::string("Flag `") + std::string(name) + "` is not a switch");
166+
167+
std::string_view lookedUpName = name;
168+
if (exp->fAlias >= 0)
169+
lookedUpName = fExpectedFlags[exp->fAlias].fName;
170+
171+
for (const auto &f : fFlags) {
172+
if (f.fName == lookedUpName)
173+
return true;
174+
}
175+
return false;
176+
}
177+
178+
/// If `name` refers to a previously-defined non-switch flag, gets its value.
179+
/// \throws std::invalid_argument if the flag was undefined or defined as a switch flag
180+
std::string_view GetFlagValue(std::string_view name) const
181+
{
182+
const auto *exp = GetExpectedFlag(name);
183+
if (!exp)
184+
throw std::invalid_argument(std::string("Flag `") + std::string(name) + "` is not expected");
185+
if (exp->fFlagType != EFlagType::kWithArg)
186+
throw std::invalid_argument(std::string("Flag `") + std::string(name) +
187+
"` is a switch, use GetSwitch()");
188+
189+
std::string_view lookedUpName = name;
190+
if (exp->fAlias >= 0)
191+
lookedUpName = fExpectedFlags[exp->fAlias].fName;
192+
193+
for (const auto &f : fFlags) {
194+
if (f.fName == lookedUpName)
195+
return f.fValue;
196+
}
197+
return "";
198+
}
199+
200+
// Tries to retrieve the flag value as a type T.
201+
// The only supported types are integral and floating point types.
202+
// \return A value of type T if the flag is present and convertible
203+
// \return nullopt if the flag is not there
204+
// \throws std::invalid_argument if the flag is there but not convertible.
205+
template <typename T>
206+
std::optional<T> GetFlagValueAs(std::string_view name) const
207+
{
208+
static_assert(std::is_integral_v<T> || std::is_floating_point_v<T>);
209+
210+
if (auto val = GetFlagValue(name); !val.empty()) {
211+
T converted;
212+
auto res = std::from_chars(val.data(), val.data() + val.size(), converted);
213+
if (res.ptr == val.end() && res.ec == std::errc{}) {
214+
return converted;
215+
} else {
216+
std::stringstream err;
217+
err << "Failed to parse flag `" << name << "` with value `" << val << "`";
218+
if constexpr (std::is_integral_v<T>)
219+
err << " as an integer.\n";
220+
else
221+
err << " as a floating point number.\n";
222+
223+
if (res.ec == std::errc::result_out_of_range)
224+
throw std::out_of_range(err.str());
225+
else
226+
throw std::invalid_argument(err.str());
227+
}
228+
}
229+
return std::nullopt;
230+
}
231+
232+
void Parse(const char **args, std::size_t nArgs)
233+
{
234+
bool forcePositional = false;
235+
236+
std::vector<std::string_view> argStr;
237+
238+
for (std::size_t i = 0; i < nArgs && fErrors.empty(); ++i) {
239+
const char *arg = args[i];
240+
241+
if (strcmp(arg, "--") == 0) {
242+
forcePositional = true;
243+
continue;
244+
}
245+
246+
bool isFlag = !forcePositional && arg[0] == '-';
247+
if (!isFlag) {
248+
// positional argument
249+
fArgs.push_back(arg);
250+
} else {
251+
++arg;
252+
// Parse long or short flag and its argument into `argStr` / `nxtArgStr`.
253+
// Note that `argStr` may contain multiple flags in case of grouped short flags (in which case nxtArgStr
254+
// refers only to the last one).
255+
argStr.clear();
256+
std::string_view nxtArgStr;
257+
bool nxtArgIsTentative = true;
258+
if (arg[0] == '-') {
259+
// long flag
260+
++arg;
261+
const char *eq = strchr(arg, '=');
262+
if (eq) {
263+
argStr.push_back(std::string_view(arg, eq - arg));
264+
nxtArgStr = std::string_view(eq + 1);
265+
nxtArgIsTentative = false;
266+
} else {
267+
argStr.push_back(std::string_view(arg));
268+
if (i < nArgs - 1 && args[i + 1][0] != '-') {
269+
nxtArgStr = args[i + 1];
270+
++i;
271+
}
272+
}
273+
} else {
274+
// short flag.
275+
// If flag grouping is active, all flags except the last one will have an implicitly empty argument.
276+
auto argLen = strlen(arg);
277+
while (fAllowFlagGrouping && argLen > 1) {
278+
argStr.push_back(std::string_view{arg, 1});
279+
++arg, --argLen;
280+
}
281+
282+
argStr.push_back(std::string_view(arg));
283+
if (i < nArgs - 1 && args[i + 1][0] != '-') {
284+
nxtArgStr = args[i + 1];
285+
++i;
286+
}
287+
}
288+
289+
for (auto j = 0u; j < argStr.size(); ++j) {
290+
std::string_view argS = argStr[j];
291+
const auto *exp = GetExpectedFlag(argS);
292+
if (!exp) {
293+
fErrors.push_back(std::string("Unknown flag: ") + args[j]);
294+
break;
295+
}
296+
297+
std::string_view nxtArg = (j == argStr.size() - 1) ? nxtArgStr : "";
298+
299+
RCmdLineOpts::RFlag flag;
300+
flag.fHelp = exp->fHelp;
301+
// If the flag is an alias (e.g. long version of a short one), save its name as the aliased one, so we
302+
// can fetch the value later by using any of the aliases.
303+
if (exp->fAlias < 0)
304+
flag.fName = argS;
305+
else
306+
flag.fName = fExpectedFlags[exp->fAlias].fName;
307+
308+
// Check for duplicate flags
309+
auto existingIt =
310+
std::find_if(fFlags.begin(), fFlags.end(), [&flag](const auto &f) { return f.fName == flag.fName; });
311+
if (existingIt != fFlags.end()) {
312+
std::string err = std::string("Flag ") + exp->AsStr() + " appeared more than once";
313+
if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg)
314+
err += " with the value: " + existingIt->fValue;
315+
fErrors.push_back(err);
316+
break;
317+
}
318+
319+
// Check that arguments are what we expect.
320+
if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg) {
321+
if (!nxtArg.empty()) {
322+
flag.fValue = nxtArg;
323+
} else {
324+
fErrors.push_back("Missing argument for flag " + exp->AsStr());
325+
}
326+
} else {
327+
if (!nxtArg.empty()) {
328+
if (nxtArgIsTentative)
329+
--i;
330+
else
331+
fErrors.push_back("Flag " + exp->AsStr() + " does not expect an argument");
332+
}
333+
}
334+
335+
if (!fErrors.empty())
336+
break;
337+
338+
fFlags.push_back(flag);
339+
}
340+
341+
if (!fErrors.empty())
342+
break;
343+
}
344+
}
345+
}
346+
};
347+
348+
} // namespace ROOT
349+
350+
#endif

0 commit comments

Comments
 (0)