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};
0 commit comments