3737// / ~~~
3838// /
3939// / ## 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".
4340// /
44- // / Multiple repeated flags, like `-vvv`, are supported but must explicitly be marked as such:
41+ // / ### Flag grouping
42+ // / By default, if all the short flags you pass (those starting with a single `-`) are 1 character long, the parser will
43+ // / accept grouped flags like "-abc" as equivalent to "-a -b -c". The last flag in the group may also accept an
44+ // / argument, in which case "-abc foo" will count as "-a -b -c foo" where "foo" is the argument to "-c". If you want to
45+ // / disable flag grouping, use:
46+ // /
47+ // / ~~~{.cpp}
48+ // / ROOT::RCmdLineOpts opts({ EFlagTreatment::kSimple });
49+ // / ~~~
50+ // /
51+ // / ### Repeated flags
52+ // / Multiple repeated flags, like `-vvv`, are supported but must explicitly be marked as such on a per-flag basis:
4553// / ~~~{.cpp}
4654// / opts.AddFlag({"-v"}, RCmdLineOpts::EFlagType::kSwitch, "", RCmdLineOpts::kFlagAllowMultiple);
4755// / ~~~
4856// / This works both for switches and flags with arguments. `GetSwitch` returns the number of times a specific flag
4957// / appeared; for flags with arguments `GetFlagValues` and `GetFlagValuesAs<T>` can be used to access the values as
5058// / vectors.
5159// /
60+ // / ### Positional argument separator
5261// / The string "--" is treated as the positional argument separator: all strings after it will be treated as positional
5362// / arguments even if they start with "-".
5463// /
64+ // / ## Prefix flags (aka no space between flag and argument)
65+ // / If you need your flags to support the syntax "-fXYZ" where "-f" is your flag and "XYZ" its argument, you can enable
66+ // / that per-flag by using `RCmdLineOpts::kFlagPrefixArg`:
67+ // /
68+ // / ~~~{.cpp}
69+ // / opts.AddFlag({"-I", "--include"}, RCmdLineOpts::EFlagType::kWithArg, RCmdLineOpts::kFlagPrefixArg });
70+ // / ~~~
71+ // /
72+ // / (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.
75+ // /
5576// / \author Giacomo Parolini <giacomo.parolini@cern.ch>
5677// / \date 2025-10-09
5778
5879#ifndef ROOT_OptParse
5980#define ROOT_OptParse
6081
6182#include < algorithm>
83+ #include < cassert>
6284#include < charconv>
6385#include < cstring>
6486#include < cstdint>
@@ -75,6 +97,22 @@ namespace ROOT {
7597
7698class RCmdLineOpts {
7799public:
100+ enum class EFlagTreatment {
101+ // / Will result to kGrouped if you don't define any short flag longer than 1 character, otherwise kSimple.
102+ kDefault ,
103+ // / `-abc` will always be treated as the single flag "-abc"
104+ kSimple ,
105+ // / `-abc` will be treated as "-a -b -c". This is only valid for short flags.
106+ // / With this setting you cannot define short flags that are more than 1 character long nor ones that are marked
107+ // / with kFlagPrefixArg.
108+ kGrouped ,
109+ };
110+
111+ struct RSettings {
112+ // / Affects how flags are parsed (\see EFlagTreatment).
113+ EFlagTreatment fFlagTreatment ;
114+ };
115+
78116 enum class EFlagType {
79117 kSwitch ,
80118 kWithArg
@@ -90,15 +128,15 @@ public:
90128 enum EFlagOpt {
91129 // / Flag is allowed to appear multiple times (default: it's an error to see the same flag twice)
92130 kFlagAllowMultiple = 1 << 0 ,
131+ // / Flag argument can appear right after this flag without a space or equal sign in between.
132+ // / Note that marking any short flag with this disables flag grouping!
133+ kFlagPrefixArg = 1 << 1 ,
93134 };
94135
95136private:
137+ RSettings fSettings ;
96138 std::vector<RFlag> fFlags ;
97139 std::vector<std::string> fArgs ;
98- // If true, many short flags may be grouped: "-abc" == "-a -b -c".
99- // This is automatically true if all short flags given are 1 character long, otherwise it's false.
100- // (a short flag is a flag with a single `-` as its prefix).
101- bool fAllowFlagGrouping = true ;
102140
103141 struct RExpectedFlag {
104142 EFlagType fFlagType = EFlagType::kSwitch ;
@@ -116,14 +154,27 @@ private:
116154
117155 const RExpectedFlag *GetExpectedFlag (std::string_view name) const
118156 {
157+ const auto StartsWith = [](std::string_view string, std::string_view prefix) {
158+ return string.size () >= prefix.size () && string.substr (0 , prefix.size ()) == prefix;
159+ };
160+
119161 for (const auto &flag : fExpectedFlags ) {
120- if (flag.fName == name)
162+ if (flag.fOpts & kFlagPrefixArg ) {
163+ if (StartsWith (name, flag.fName )) {
164+ // NOTE: we can't have ambiguities here because we make sure that no flags share a common prefix in
165+ // AddFlag().
166+ return &flag;
167+ }
168+ } else if (flag.fName == name) {
121169 return &flag;
170+ }
122171 }
123172 return nullptr ;
124173 }
125174
126175public:
176+ explicit RCmdLineOpts (RSettings settings = {EFlagTreatment::kDefault }) : fSettings(settings) {}
177+
127178 // / Returns all parsing errors
128179 const std::vector<std::string> &GetErrors () const { return fErrors ; }
129180 // / Retrieves all positional arguments
@@ -142,6 +193,7 @@ public:
142193
143194 // / Defines a new flag (either a switch or a flag with argument).
144195 // / The flag may be referred to as any of the values inside `aliases` (e.g. { "-h", "--help" }).
196+ // / You must pass at least 1 string inside `aliases`.
145197 // / All strings inside `aliases` must start with `-` or `--` and be at least 1 character long (aside the dashes).
146198 // / Flags starting with a single `-` are considered "short", regardless of their actual length.
147199 // / If all short flags are 1 character long, they may be collapsed into one and parsed as individual flags
@@ -155,6 +207,17 @@ public:
155207 void AddFlag (std::initializer_list<std::string_view> aliases, EFlagType type = EFlagType::kSwitch ,
156208 std::string_view help = " " , std::uint32_t flagOpts = 0 )
157209 {
210+ const auto IsPrefixOf = [](std::string_view a, std::string_view b) {
211+ return a.size () < b.size () && std::equal (a.begin (), a.end (), b.begin ());
212+ };
213+
214+ if (aliases.size () == 0 )
215+ throw std::invalid_argument (" AddFlag must receive at least 1 name for the flag!" );
216+
217+ if ((flagOpts & kFlagPrefixArg ) && type != EFlagType::kWithArg )
218+ throw std::invalid_argument (" Flag `" + std::string (*aliases.begin ()) +
219+ " ` has option kFlagPrefixArg but it's a Switch, so the option makes no sense." );
220+
158221 int aliasIdx = -1 ;
159222 for (auto f : aliases) {
160223 auto prefixLen = f.find_first_not_of (' -' );
@@ -164,11 +227,43 @@ public:
164227 if (f.size () == prefixLen)
165228 throw std::invalid_argument (" Flag name cannot be empty" );
166229
167- fAllowFlagGrouping = fAllowFlagGrouping && (prefixLen > 1 || f.size () == 2 );
230+ auto flagName = f.substr (prefixLen);
231+
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.
234+ 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." );
239+
240+ if (!(flagOpts & kFlagPrefixArg ) && !(expFlag.fOpts & kFlagPrefixArg ))
241+ continue ;
242+
243+ // At least one of expFlag and f is a prefix flag: this means that they must not share a common prefix.
244+ if (((expFlag.fOpts & kFlagPrefixArg ) && IsPrefixOf (expFlag.fName , flagName)) ||
245+ ((flagOpts & kFlagPrefixArg ) && IsPrefixOf (flagName, expFlag.fName ))) {
246+ throw std::invalid_argument (" Flags `" + expFlag.AsStr () + " ` and `" + std::string (f) +
247+ " ` have a common prefix. This causes ambiguity because at least one of them "
248+ " is marked with kFlagPrefixArg." );
249+ }
250+ }
251+
252+ bool disallowsGrouping = (prefixLen == 1 && (f.size () > 2 || (flagOpts & kFlagPrefixArg )));
253+ if (disallowsGrouping) {
254+ if (fSettings .fFlagTreatment == EFlagTreatment::kDefault ) {
255+ fSettings .fFlagTreatment = EFlagTreatment::kSimple ;
256+ } else if (fSettings .fFlagTreatment == EFlagTreatment::kGrouped ) {
257+ throw std::invalid_argument (
258+ std::string (" Flags starting with a single dash must be 1 character long when `FlagTreatment == "
259+ " EFlagTreatment::kGrouped'! Cannot accept given flag `" ) +
260+ std::string (f) + " `" );
261+ }
262+ }
168263
169264 RExpectedFlag expected;
170265 expected.fFlagType = type;
171- expected.fName = f. substr (prefixLen) ;
266+ expected.fName = flagName ;
172267 expected.fHelp = help;
173268 expected.fAlias = aliasIdx;
174269 expected.fShort = prefixLen == 1 ;
@@ -218,8 +313,7 @@ public:
218313 if (!exp)
219314 throw std::invalid_argument (std::string (" Flag `" ) + std::string (name) + " ` is not expected" );
220315 if (exp->fFlagType != EFlagType::kWithArg )
221- throw std::invalid_argument (std::string (" Flag `" ) + std::string (name) +
222- " ` is a switch, use GetSwitch()" );
316+ throw std::invalid_argument (std::string (" Flag `" ) + std::string (name) + " ` is a switch, use GetSwitch()" );
223317
224318 std::string_view lookedUpName = name;
225319 if (exp->fAlias >= 0 )
@@ -312,6 +406,11 @@ public:
312406 {
313407 bool forcePositional = false ;
314408
409+ // If flag treatment is still Default by now it means we can safely group short flags (otherwise we'd have
410+ // already changed it to Simple).
411+ if (fSettings .fFlagTreatment == EFlagTreatment::kDefault )
412+ fSettings .fFlagTreatment = EFlagTreatment::kGrouped ;
413+
315414 // Contains one or more flags coming from one of the arguments (e.g. "-abc" may be split
316415 // into flags "a", "b", and "c", which will be stored in `argStr`).
317416 std::vector<std::string_view> argStr;
@@ -336,6 +435,7 @@ public:
336435 // refers only to the last one).
337436 argStr.clear ();
338437 std::string_view nxtArgStr;
438+ // If this is false `nxtArgStr` *must* refer to the next arg, otherwise it might or might not be.
339439 bool nxtArgIsTentative = true ;
340440 if (arg[0 ] == ' -' ) {
341441 // long flag
@@ -356,7 +456,7 @@ public:
356456 // short flag.
357457 // If flag grouping is active, all flags except the last one will have an implicitly empty argument.
358458 auto argLen = strlen (arg);
359- while (fAllowFlagGrouping && argLen > 1 ) {
459+ while (fSettings . fFlagTreatment == EFlagTreatment:: kGrouped && argLen > 1 ) {
360460 argStr.push_back (std::string_view{arg, 1 });
361461 ++arg, --argLen;
362462 }
@@ -370,20 +470,41 @@ public:
370470
371471 for (auto j = 0u ; j < argStr.size (); ++j) {
372472 std::string_view argS = argStr[j];
473+
373474 const auto *exp = GetExpectedFlag (argS);
374475 if (!exp) {
375476 fErrors .push_back (std::string (" Unknown flag: " ) + argOrig);
376477 break ;
377478 }
378479
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+ }
499+
379500 std::string_view nxtArg = (j == argStr.size () - 1 ) ? nxtArgStr : " " ;
380501
381502 RCmdLineOpts::RFlag flag;
382503 flag.fHelp = exp->fHelp ;
383504 // If the flag is an alias (e.g. long version of a short one), save its name as the aliased one, so we
384505 // can fetch the value later by using any of the aliases.
385506 if (exp->fAlias < 0 )
386- flag.fName = argS ;
507+ flag.fName = exp-> fName ;
387508 else
388509 flag.fName = fExpectedFlags [exp->fAlias ].fName ;
389510
0 commit comments