Skip to content

Commit 85675a8

Browse files
committed
[main] optparse: don't treat = sign as separator for prefix flags
1 parent b841862 commit 85675a8

2 files changed

Lines changed: 185 additions & 102 deletions

File tree

main/src/optparse.hxx

Lines changed: 136 additions & 97 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
@@ -428,18 +448,35 @@ public:
428448
if (!isFlag) {
429449
// positional argument
430450
fArgs.push_back(arg);
431-
} else {
451+
continue;
452+
}
453+
454+
++arg; // eat first `-`.
455+
456+
// Parse long or short flag and its argument into `argStr` / `nxtArgStr`.
457+
// Note that `argStr` may contain multiple flags in case of grouped short flags (in which case nxtArgStr
458+
// refers only to the last one).
459+
argStr.clear();
460+
std::string_view nxtArgStr;
461+
// If this is false `nxtArgStr` *must* refer to the next arg, otherwise it might or might not be.
462+
bool nxtArgIsTentative = true;
463+
if (arg[0] == '-') {
464+
// long flag
432465
++arg;
433-
// Parse long or short flag and its argument into `argStr` / `nxtArgStr`.
434-
// Note that `argStr` may contain multiple flags in case of grouped short flags (in which case nxtArgStr
435-
// refers only to the last one).
436-
argStr.clear();
437-
std::string_view nxtArgStr;
438-
// If this is false `nxtArgStr` *must* refer to the next arg, otherwise it might or might not be.
439-
bool nxtArgIsTentative = true;
440-
if (arg[0] == '-') {
441-
// long flag
442-
++arg;
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()) {
443480
const char *eq = strchr(arg, '=');
444481
if (eq) {
445482
argStr.push_back(std::string_view(arg, eq - arg));
@@ -452,100 +489,102 @@ public:
452489
++i;
453490
}
454491
}
455-
} else {
456-
// short flag.
457-
// If flag grouping is active, all flags except the last one will have an implicitly empty argument.
458-
auto argLen = strlen(arg);
459-
while (fSettings.fFlagTreatment == EFlagTreatment::kGrouped && argLen > 1) {
460-
argStr.push_back(std::string_view{arg, 1});
461-
++arg, --argLen;
462-
}
492+
}
493+
} else {
494+
// short flag.
495+
// If flag grouping is active, all flags except the last one will have an implicitly empty argument.
496+
auto argLen = strlen(arg);
497+
while (fSettings.fFlagTreatment == EFlagTreatment::kGrouped && argLen > 1) {
498+
argStr.push_back(std::string_view{arg, 1});
499+
++arg, --argLen;
500+
}
463501

464-
argStr.push_back(std::string_view(arg));
465-
if (i < nArgs - 1 && args[i + 1][0] != '-') {
466-
nxtArgStr = args[i + 1];
467-
++i;
468-
}
502+
argStr.push_back(std::string_view(arg));
503+
if (i < nArgs - 1 && args[i + 1][0] != '-') {
504+
nxtArgStr = args[i + 1];
505+
++i;
469506
}
507+
}
470508

471-
for (auto j = 0u; j < argStr.size(); ++j) {
472-
std::string_view argS = argStr[j];
509+
assert(argStr.size() < 2 || fSettings.fFlagTreatment == EFlagTreatment::kGrouped);
473510

474-
const auto *exp = GetExpectedFlag(argS);
475-
if (!exp) {
511+
for (auto j = 0u; j < argStr.size(); ++j) {
512+
std::string_view argS = argStr[j];
513+
514+
const auto *exp = GetExpectedFlag(argS);
515+
if (!exp) {
516+
fErrors.push_back(std::string("Unknown flag: ") + argOrig);
517+
break;
518+
}
519+
520+
// In Prefix mode, check if the returned expected flag is shorter than `argS`. This can mean two things:
521+
// - if `nxtArgIsTentative == false` then this flag was followed by an equal sign, and in that case
522+
// the intention is interpreted as "I want this flag's argument to be whatever follows the equal sign",
523+
// which means we treat this as an unknown flag;
524+
// - otherwise, we use the rest of `argS` as the argument to the flag.
525+
// More concretely: if the user added flag "-D" and argS is "-Dfoo=bar", we parse it as
526+
// {flag: "-Dfoo", arg: "bar"}, rather than {flag: "-D", arg: "foo=bar"}.
527+
if ((exp->fOpts & kFlagPrefixArg) && argS.size() > exp->fName.size()) {
528+
if (nxtArgIsTentative) {
529+
i -= !nxtArgStr.empty(); // if we had already picked a candidate next arg, undo that.
530+
nxtArgStr = argS.substr(exp->fName.size());
531+
nxtArgIsTentative = false;
532+
} else {
476533
fErrors.push_back(std::string("Unknown flag: ") + argOrig);
477534
break;
478535
}
536+
} else {
537+
assert(exp->fName.size() == argS.size());
538+
}
479539

480-
// In Prefix mode, check if the returned expected flag is shorter than `argS`. This can mean two things:
481-
// - if `nxtArgIsTentative == false` then this flag was followed by an equal sign, and in that case
482-
// the intention is interpreted as "I want this flag's argument to be whatever follows the equal sign",
483-
// which means we treat this as an unknown flag;
484-
// - otherwise, we use the rest of `argS` as the argument to the flag.
485-
// More concretely: if the user added flag "-D" and argS is "-Dfoo=bar", we parse it as
486-
// {flag: "-Dfoo", arg: "bar"}, rather than {flag: "-D", arg: "foo=bar"}.
487-
if ((exp->fOpts & kFlagPrefixArg) && argS.size() > exp->fName.size()) {
488-
if (nxtArgIsTentative) {
489-
i -= !nxtArgStr.empty(); // if we had already picked a candidate next arg, undo that.
490-
nxtArgStr = argS.substr(exp->fName.size());
491-
nxtArgIsTentative = false;
492-
} else {
493-
fErrors.push_back(std::string("Unknown flag: ") + argOrig);
494-
break;
495-
}
496-
} else {
497-
assert(exp->fName.size() == argS.size());
498-
}
540+
std::string_view nxtArg = (j == argStr.size() - 1) ? nxtArgStr : "";
499541

500-
std::string_view nxtArg = (j == argStr.size() - 1) ? nxtArgStr : "";
501-
502-
RCmdLineOpts::RFlag flag;
503-
flag.fHelp = exp->fHelp;
504-
// If the flag is an alias (e.g. long version of a short one), save its name as the aliased one, so we
505-
// can fetch the value later by using any of the aliases.
506-
if (exp->fAlias < 0)
507-
flag.fName = exp->fName;
508-
else
509-
flag.fName = fExpectedFlags[exp->fAlias].fName;
510-
511-
// Check for duplicate flags
512-
if (!(exp->fOpts & kFlagAllowMultiple)) {
513-
auto existingIt = std::find_if(fFlags.begin(), fFlags.end(),
514-
[&flag](const auto &f) { return f.fName == flag.fName; });
515-
if (existingIt != fFlags.end()) {
516-
std::string err = std::string("Flag ") + exp->AsStr() + " appeared more than once";
517-
if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg)
518-
err += " with the value: " + existingIt->fValue;
519-
fErrors.push_back(err);
520-
break;
521-
}
542+
RCmdLineOpts::RFlag flag;
543+
flag.fHelp = exp->fHelp;
544+
// If the flag is an alias (e.g. long version of a short one), save its name as the aliased one, so we
545+
// can fetch the value later by using any of the aliases.
546+
if (exp->fAlias < 0)
547+
flag.fName = exp->fName;
548+
else
549+
flag.fName = fExpectedFlags[exp->fAlias].fName;
550+
551+
// Check for duplicate flags
552+
if (!(exp->fOpts & kFlagAllowMultiple)) {
553+
auto existingIt =
554+
std::find_if(fFlags.begin(), fFlags.end(), [&flag](const auto &f) { return f.fName == flag.fName; });
555+
if (existingIt != fFlags.end()) {
556+
std::string err = std::string("Flag ") + exp->AsStr() + " appeared more than once";
557+
if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg)
558+
err += " with the value: " + existingIt->fValue;
559+
fErrors.push_back(err);
560+
break;
522561
}
562+
}
523563

524-
// Check that arguments are what we expect.
525-
if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg) {
526-
if (!nxtArg.empty()) {
527-
flag.fValue = nxtArg;
528-
} else {
529-
fErrors.push_back("Missing argument for flag " + exp->AsStr());
530-
}
564+
// Check that arguments are what we expect.
565+
if (exp->fFlagType == RCmdLineOpts::EFlagType::kWithArg) {
566+
if (!nxtArg.empty()) {
567+
flag.fValue = nxtArg;
531568
} else {
532-
if (!nxtArg.empty()) {
533-
if (nxtArgIsTentative)
534-
--i;
535-
else
536-
fErrors.push_back("Flag " + exp->AsStr() + " does not expect an argument");
537-
}
569+
fErrors.push_back("Missing argument for flag " + exp->AsStr());
570+
}
571+
} else {
572+
if (!nxtArg.empty()) {
573+
if (nxtArgIsTentative)
574+
--i;
575+
else
576+
fErrors.push_back("Flag " + exp->AsStr() + " does not expect an argument");
538577
}
539-
540-
if (!fErrors.empty())
541-
break;
542-
543-
fFlags.push_back(flag);
544578
}
545579

546580
if (!fErrors.empty())
547581
break;
582+
583+
fFlags.push_back(flag);
548584
}
585+
586+
if (!fErrors.empty())
587+
break;
549588
}
550589
}
551590
};

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)