Skip to content

Commit acb1f1a

Browse files
committed
[main] optparse: don't treat = sign as separator for prefix flags
1 parent 0ec70bb commit acb1f1a

2 files changed

Lines changed: 108 additions & 26 deletions

File tree

main/src/optparse.hxx

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@
7070
/// ~~~
7171
///
7272
/// (see EFlagTreatment for more details). This will **disable** flag grouping globally, but allows the parser to
73-
/// interpret flags and arguments that are not separated by spaces.
74-
/// Note that this only makes sense for flags with arguments.
73+
/// interpret flags and arguments that are not separated by spaces. If prefix arg is used, the equal sign will *not*
74+
/// be considered as the key-value separator (`-Dfoo=bar` if `-D` is a prefix arg flag will be parsed as `"-D",
75+
/// "foo=bar"`). Note that this option only makes sense for flags with arguments.
7576
///
7677
/// \author Giacomo Parolini <giacomo.parolini@cern.ch>
7778
/// \date 2025-10-09
@@ -218,6 +219,10 @@ public:
218219
throw std::invalid_argument("Flag `" + std::string(*aliases.begin()) +
219220
"` has option kFlagPrefixArg but it's a Switch, so the option makes no sense.");
220221

222+
std::vector<RExpectedFlag> flagsToBeAdded;
223+
flagsToBeAdded.reserve(aliases.size());
224+
225+
const auto nCurrentFlags = fExpectedFlags.size();
221226
int aliasIdx = -1;
222227
for (auto f : aliases) {
223228
auto prefixLen = f.find_first_not_of('-');
@@ -229,13 +234,17 @@ public:
229234

230235
auto flagName = f.substr(prefixLen);
231236

232-
// Check that we're not introducing ambiguities with prefix flags. While we're at it, also check that none
233-
// of the given aliases were already added.
237+
// Check that we're not introducing ambiguities with prefix flags.
238+
// While we're at it, also check that none of the given aliases were already added. Note that it is valid to
239+
// add the same flag name as both a long and short flag (like "-foo" and "--foo") but only if they are aliases
240+
// of the same flag.
234241
for (const auto &expFlag : fExpectedFlags) {
235-
// NOTE: we're checking against the full string, not just the flag name, to allow cases like:
236-
// AddFlag({"-foo", "--foo"}).
237-
if (expFlag.AsStr() == f)
238-
throw std::invalid_argument("Flag `" + expFlag.AsStr() + "` was added multiple times.");
242+
if (expFlag.fName == flagName) {
243+
throw std::invalid_argument(
244+
"Flag `" + std::string(flagName) +
245+
"` was added multiple times. Note that adding flags with the same name but different number of `-` "
246+
"can only be done as aliases of the same call to AddFlag().");
247+
}
239248

240249
if (!(flagOpts & kFlagPrefixArg) && !(expFlag.fOpts & kFlagPrefixArg))
241250
continue;
@@ -249,6 +258,15 @@ public:
249258
}
250259
}
251260

261+
// Check that the user didn't pass the very same flag multiple times in the same AddFlag invocation, as that's
262+
// likely a mistake. This is different from the check done in the previous loop as that one only concerns
263+
// flags that were already added before this AddFlag invocation.
264+
for (const auto &expFlag : flagsToBeAdded) {
265+
if (expFlag.AsStr() == f)
266+
throw std::invalid_argument("The same flag `" + expFlag.AsStr() +
267+
"` was passed multiple times to AddFlag().");
268+
}
269+
252270
bool disallowsGrouping = (prefixLen == 1 && (f.size() > 2 || (flagOpts & kFlagPrefixArg)));
253271
if (disallowsGrouping) {
254272
if (fSettings.fFlagTreatment == EFlagTreatment::kDefault) {
@@ -268,10 +286,12 @@ public:
268286
expected.fAlias = aliasIdx;
269287
expected.fShort = prefixLen == 1;
270288
expected.fOpts = flagOpts;
271-
fExpectedFlags.push_back(expected);
289+
flagsToBeAdded.push_back(expected);
272290
if (aliasIdx < 0)
273-
aliasIdx = fExpectedFlags.size() - 1;
291+
aliasIdx = nCurrentFlags + flagsToBeAdded.size() - 1;
274292
}
293+
294+
fExpectedFlags.insert(fExpectedFlags.end(), flagsToBeAdded.begin(), flagsToBeAdded.end());
275295
}
276296

277297
/// If `name` refers to a previously-defined switch (i.e. a boolean flag), returns how many times
@@ -431,7 +451,8 @@ public:
431451
continue;
432452
}
433453

434-
++arg;
454+
++arg; // eat first `-`.
455+
435456
// Parse long or short flag and its argument into `argStr` / `nxtArgStr`.
436457
// Note that `argStr` may contain multiple flags in case of grouped short flags (in which case nxtArgStr
437458
// refers only to the last one).
@@ -442,16 +463,31 @@ public:
442463
if (arg[0] == '-') {
443464
// long flag
444465
++arg;
445-
const char *eq = strchr(arg, '=');
446-
if (eq) {
447-
argStr.push_back(std::string_view(arg, eq - arg));
448-
nxtArgStr = std::string_view(eq + 1);
449-
nxtArgIsTentative = false;
450-
} else {
451-
argStr.push_back(std::string_view(arg));
452-
if (i < nArgs - 1 && args[i + 1][0] != '-') {
453-
nxtArgStr = args[i + 1];
454-
++i;
466+
467+
// if we have prefix flags we need to check them first, as in that case the `=` sign should not be treated
468+
// as the key-value separator.
469+
const auto argLen = strlen(arg);
470+
for (const auto &flag : fExpectedFlags) {
471+
if ((flag.fOpts & kFlagPrefixArg) && flag.fName.size() < argLen &&
472+
strncmp(arg, flag.fName.c_str(), flag.fName.size()) == 0) {
473+
argStr.push_back(std::string_view(arg, flag.fName.size()));
474+
nxtArgStr = std::string_view(arg + flag.fName.size(), argLen - flag.fName.size());
475+
break;
476+
}
477+
}
478+
479+
if (argStr.empty()) {
480+
const char *eq = strchr(arg, '=');
481+
if (eq) {
482+
argStr.push_back(std::string_view(arg, eq - arg));
483+
nxtArgStr = std::string_view(eq + 1);
484+
nxtArgIsTentative = false;
485+
} else {
486+
argStr.push_back(std::string_view(arg));
487+
if (i < nArgs - 1 && args[i + 1][0] != '-') {
488+
nxtArgStr = args[i + 1];
489+
++i;
490+
}
455491
}
456492
}
457493
} else {
@@ -470,6 +506,8 @@ public:
470506
}
471507
}
472508

509+
assert(argStr.size() < 2 || fSettings.fFlagTreatment == EFlagTreatment::kGrouped);
510+
473511
for (auto j = 0u; j < argStr.size(); ++j) {
474512
std::string_view argS = argStr[j];
475513

main/test/optparse_test.cxx

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ TEST(OptParse, PrefixMultiple)
664664
opts.Parse(args, std::size(args));
665665

666666
EXPECT_TRUE(opts.GetErrors().empty());
667-
EXPECT_EQ(opts.GetFlagValues("D"), std::vector<std::string_view>({"name", "name", "name"}));
667+
EXPECT_EQ(opts.GetFlagValues("D"), std::vector<std::string_view>({"name", "=name", "name"}));
668668
}
669669

670670
TEST(OptParse, PrefixWithEqual)
@@ -673,11 +673,24 @@ TEST(OptParse, PrefixWithEqual)
673673
opts.AddFlag({"--D"}, ROOT::RCmdLineOpts::EFlagType::kWithArg, "",
674674
ROOT::RCmdLineOpts::kFlagAllowMultiple | ROOT::RCmdLineOpts::kFlagPrefixArg);
675675

676-
const char *args[] = {"somename", "--Df=a"};
676+
const char *args[] = {"somename", "--Df=a", "--D", "f=a"};
677677
opts.Parse(args, std::size(args));
678678

679-
EXPECT_EQ(opts.GetErrors().size(), 1);
680-
EXPECT_TRUE(opts.GetFlagValues("D").empty());
679+
EXPECT_TRUE(opts.GetErrors().empty());
680+
EXPECT_EQ(opts.GetFlagValues("D"), std::vector<std::string_view>({"f=a", "f=a"}));
681+
}
682+
683+
TEST(OptParse, PrefixShortMulticharacter)
684+
{
685+
ROOT::RCmdLineOpts opts;
686+
opts.AddFlag({"-includeI"}, ROOT::RCmdLineOpts::EFlagType::kWithArg, "",
687+
ROOT::RCmdLineOpts::kFlagAllowMultiple | ROOT::RCmdLineOpts::kFlagPrefixArg);
688+
689+
const char *args[] = {"somename", "-includeI/foo/bar", "-includeI", "/foo/bar"};
690+
opts.Parse(args, std::size(args));
691+
692+
EXPECT_TRUE(opts.GetErrors().empty());
693+
EXPECT_EQ(opts.GetFlagValues("includeI"), std::vector<std::string_view>({"/foo/bar", "/foo/bar"}));
681694
}
682695

683696
TEST(OptParse, PrefixDisablesGrouping)
@@ -724,6 +737,37 @@ TEST(OptParse, DuplicateFlag)
724737
opts.AddFlag({"-a"}, ROOT::RCmdLineOpts::EFlagType::kSwitch);
725738
FAIL() << "adding the same flag twice should fail";
726739
} catch (const std::invalid_argument &ex) {
727-
EXPECT_STREQ(ex.what(), "Flag `-a` was added multiple times.");
740+
EXPECT_STREQ(ex.what(), "Flag `a` was added multiple times. Note that adding flags with the same name but "
741+
"different number of `-` can only be done as aliases of the same call to AddFlag().");
742+
}
743+
}
744+
745+
TEST(OptParse, DuplicateFlagLongShort)
746+
{
747+
ROOT::RCmdLineOpts opts;
748+
opts.AddFlag({"-a"}, ROOT::RCmdLineOpts::EFlagType::kSwitch);
749+
try {
750+
opts.AddFlag({"--a"}, ROOT::RCmdLineOpts::EFlagType::kSwitch);
751+
FAIL() << "adding the same flag twice should fail";
752+
} catch (const std::invalid_argument &ex) {
753+
EXPECT_STREQ(ex.what(), "Flag `a` was added multiple times. Note that adding flags with the same name but "
754+
"different number of `-` can only be done as aliases of the same call to AddFlag().");
755+
}
756+
}
757+
758+
TEST(OptParse, DuplicateFlagLongShortOk)
759+
{
760+
ROOT::RCmdLineOpts opts;
761+
opts.AddFlag({"-a", "--a"}, ROOT::RCmdLineOpts::EFlagType::kSwitch);
762+
}
763+
764+
TEST(OptParse, DuplicateFlagAlias)
765+
{
766+
ROOT::RCmdLineOpts opts;
767+
try {
768+
opts.AddFlag({"--a", "--a"}, ROOT::RCmdLineOpts::EFlagType::kSwitch);
769+
FAIL() << "adding the same flag twice should fail";
770+
} catch (const std::invalid_argument &ex) {
771+
EXPECT_STREQ(ex.what(), "The same flag `--a` was passed multiple times to AddFlag().");
728772
}
729773
}

0 commit comments

Comments
 (0)