1 module jcli.core.flags;
2
3 enum ArgConfig : ArgFlags
4 {
5 ///
6 none,
7
8 /// If the argument appears multiple times, the last value provided will take effect.
9 canRedefine = ArgFlags._canRedefineBit | ArgFlags._multipleBit | ArgFlags._namedArgumentBit,
10
11 /// If not given a value, will have its default value and not trigger an error.
12 /// Missing (even named) arguments trigger an error by default.
13 optional = ArgFlags._optionalBit,
14
15 /// The opposite of optional.
16 required = ArgFlags._requiredBit,
17
18 /// The name of the argument is case insensitive.
19 /// Aka "--STUFF" will work in place of "--stuff".
20 caseInsensitive = ArgFlags._caseInsensitiveBit,
21
22 /// Example: `-a -a` gives 2.
23 accumulate = ArgFlags._multipleBit | ArgFlags._countBit | ArgFlags._namedArgumentBit,
24
25 /// The type of the field must be an array of some sort.
26 /// Example: `-a b -a c` gives an the array ["b", "c"]
27 aggregate = ArgFlags._multipleBit | ArgFlags._aggregateBit | ArgFlags._namedArgumentBit,
28
29 /// When an argument name is specified multiple times, count how many there are.
30 /// Example: `-vvv` gives the count of 3.
31 repeatableName = ArgFlags._repeatableNameBit | ArgFlags._countBit | ArgFlags._namedArgumentBit,
32
33 /// Allow an argument name to appear without a value.
34 /// Example: `--flag` would parse as `true`.
35 parseAsFlag = ArgFlags._parseAsFlagBit | ArgFlags._optionalBit | ArgFlags._namedArgumentBit,
36
37 /// Can be used instead of ArgPositional UDA.
38 positional = ArgFlags._positionalArgumentBit,
39
40 /// Can be used instead of ArgNamed UDA.
41 named = ArgFlags._namedArgumentBit,
42 }
43 unittest
44 {
45 static foreach (name; __traits(allMembers, ArgConfig))
46 {{
47 ArgConfig flags = __traits(getMember, ArgConfig, name);
48 string validation = getArgumentFlagsValidationMessage(flags);
49 assert(validation is null);
50 }}
51 }
52
53 private
54 {
55 /// Shows which other flags a feature is incompatible with.
56 /// The flags should only declare this regarding the flags after them.
57 struct IncompatibleWithAnyOf
58 {
59 ArgFlags value;
60 }
61
62 /// Shows which other flags a feature is requred to be accompanied with.
63 struct RequiresOneOrMoreOf
64 {
65 ArgFlags value;
66 }
67
68 /// Shows which flags a feature requres exactly one of.
69 struct RequiresExactlyOneOf
70 {
71 ArgFlags value;
72 }
73
74 /// Shows which other flags a feature is requres with.
75 struct RequiresAllOf
76 {
77 ArgFlags value;
78 }
79 }
80
81 package(jcli):
82
83 enum ArgFlags
84 {
85 ///
86 none,
87
88 /// If not given a value, will have its default value and not trigger an error.
89 _optionalBit = 1 << 0,
90
91 /// An argument with the same name may appear multiple times.
92 @RequiresExactlyOneOf(_countBit | _canRedefineBit | _aggregateBit)
93 _multipleBit = 1 << 1,
94
95 /// Allow an argument name to appear without a value.
96 @IncompatibleWithAnyOf(_multipleBit)
97 @RequiresAllOf(_optionalBit)
98 _parseAsFlagBit = 1 << 2,
99
100 /// Implies that the field should get the number of occurences of the argument.
101 @RequiresOneOrMoreOf(_multipleBit | _repeatableNameBit)
102 _countBit = 1 << 3,
103
104 /// The name of the argument is case insensitive.
105 /// Aka "--STUFF" will work in place of "--stuff".
106 _caseInsensitiveBit = 1 << 4,
107
108 /// If the argument appears multiple times, the last value provided will take effect.
109 @IncompatibleWithAnyOf(_countBit)
110 @RequiresAllOf(_multipleBit)
111 _canRedefineBit = 1 << 5,
112
113 /// When an argument name is specified multiple times, count how many there are.
114 /// Example: `-vvv` gives the count of 3.
115 @IncompatibleWithAnyOf(_parseAsFlagBit | _canRedefineBit)
116 @RequiresAllOf(_countBit)
117 _repeatableNameBit = 1 << 6,
118
119 /// Put all matched values in an array.
120 @IncompatibleWithAnyOf(_parseAsFlagBit | _countBit | _canRedefineBit)
121 _aggregateBit = 1 << 7,
122
123 /// The opposite of optional. Can usually be inferred.
124 @IncompatibleWithAnyOf(_optionalBit | _parseAsFlagBit)
125 _requiredBit = 1 << 8,
126
127 /// Whether the required bit or optional bit was given explicitly by the user
128 /// or inferred by the system.
129 _inferedOptionalityBit = 1 << 9,
130
131 /// Meets the requirements of having their optionality being changed.
132 /// This bit is kind of pointless so I will probably remove it.
133 @RequiresAllOf(_inferedOptionalityBit)
134 _mayChangeOptionalityWithoutBreakingThingsBit = 1 << 10,
135
136 /// Whether is positional.
137 /// On the user side, it is usually provided via UDAs.
138 @IncompatibleWithAnyOf(
139 _multipleBit
140 | _parseAsFlagBit
141 | _countBit
142 | _canRedefineBit
143 | _repeatableNameBit
144 | _aggregateBit)
145 _positionalArgumentBit = 1 << 11,
146
147 /// Whether is positional.
148 /// On the user side, it is usually provided via UDAs.
149 @IncompatibleWithAnyOf(_positionalArgumentBit)
150 _namedArgumentBit = 1 << 12,
151
152 /// Whether a field is of struct type, containing subfields, which are also arguments.
153 /// This means all fields of that nested struct are to be considered arguments, if marked as such.
154 /// I could force the user to
155 // _nestedStructBit = 1 << 13,
156 }
157
158 bool areArgumentFlagsValid(ArgFlags flags) pure @safe
159 {
160 // for not just call into get message, but this can be optimized a little bit later.
161 return getArgumentFlagsValidationMessage(flags) is null;
162 }
163
164 // string toFlagsString(ArgFlags flags) pure @safe
165 // {
166 // return jcli.core.utils.toFlagsString(flags);
167 // }
168
169 alias ArgFlagsInfo = _ArgFlagsInfo!(ArgFlags);
170
171 import jcli.core.utils : toFlagsString;
172 import std.bitmanip : bitsSet;
173 import std.conv : to;
174
175 string getArgumentFlagsValidationMessage(ArgFlags flags) pure @safe
176 {
177 foreach (size_t index; bitsSet(flags))
178 {
179 const currentFlag = cast(ArgFlags)(1 << index);
180 {
181 const f = ArgFlagsInfo.incompatible[index];
182 if (flags.hasEither(f))
183 {
184 return "The flag " ~ currentFlag.toFlagsString ~ " is incompatible with " ~ f.toFlagsString;
185 }
186 }
187 {
188 const f = ArgFlagsInfo.requiresAll[index];
189 if (flags.doesNotHave(f))
190 {
191 return "The flag " ~ currentFlag.toFlagsString ~ " requires all of " ~ f.toFlagsString;
192 }
193 }
194 {
195 const f = ArgFlagsInfo.requiresOneOrMore[index];
196 if (f != 0 && flags.doesNotHaveEither(f))
197 {
198 return "The flag " ~ currentFlag.toFlagsString ~ " requires one or more of " ~ f.toFlagsString;
199 }
200 }
201 {
202 const f = ArgFlagsInfo.requiresExactlyOne[index];
203 if (f != 0)
204 {
205 auto matches = flags & f;
206
207 import std.range : walkLength;
208 auto numMatches = bitsSet(matches).walkLength;
209
210 if (numMatches != 1)
211 {
212 return "The flag " ~ currentFlag.toFlagsString ~ " requires exactly one of " ~ f.toFlagsString
213 ~ ". Were matched all of the following: " ~ matches.toFlagsString;
214 }
215 }
216 }
217 }
218
219 return null;
220 }
221
222 /// Returns the validation message that describes in what way
223 /// any pairs of flags were incompatible.
224 /// We only do compile-time assertions, which is why this function
225 /// just concatenates strings to return the error message (basically, it shouldn't matter).
226 string getArgumentConfigFlagsIncompatibilityValidationMessage(ArgConfig[] flagsToTest)
227 {
228 foreach (index, f1; flagsToTest)
229 {
230 foreach (f2; flagsToTest[index + 1 .. $])
231 {
232 if (f1 == f2)
233 continue;
234
235 foreach (bitIndex; bitsSet(f1))
236 {
237 auto incompatibleForThisBit = ArgFlagsInfo.incompatible[bitIndex];
238
239 if (incompatibleForThisBit & f1)
240 {
241 return "The high-level config flag " ~ f1.to!string
242 ~ " is invalid, the low level parts are incompatible. The bit "
243 ~ (cast(ArgFlags)(1 << bitIndex)).to!string
244 ~ " is incompatible with " ~ incompatibleForThisBit.toFlagsString;
245 }
246
247 auto problematicFlags = cast(ArgFlags) (incompatibleForThisBit & f2);
248 if (problematicFlags)
249 {
250 return "The high-level config flag " ~ f1.to!string
251 ~ " is incompatible with " ~ f2.to!string
252 ~ ". The problematic flags are " ~ problematicFlags.toFlagsString;
253 }
254 }
255 }
256 }
257 return null;
258 }
259
260 enum CommandFlags
261 {
262 none = 0,
263
264 /// No command attribute was found.
265 noCommandAttribute = 1 << 0,
266
267 /// The command attribute was deduced from its simple form, aka @("description").
268 stringAttribute = 1 << 1,
269
270 /// The command was set from a Command or CommandDefault attribute
271 commandAttribute = 1 << 2,
272
273 /// e.g. @Command("ab", "cd") instead of @Command.
274 givenValue = 1 << 3,
275
276 ///
277 explicitlyDefault = 1 << 4,
278 }
279
280 import jcli.core.utils : FlagsHelpers;
281 mixin FlagsHelpers!ArgFlags;
282 mixin FlagsHelpers!CommandFlags;
283
284 @safe nothrow @nogc pure const
285 unittest
286 {
287 with (ArgFlags)
288 {
289 ArgFlags a = _aggregateBit | _caseInsensitiveBit;
290 ArgFlags b = _canRedefineBit;
291 assert(doesNotHave(a, b));
292 assert(doesNotHave(a, b | _caseInsensitiveBit));
293 assert(has(a, _caseInsensitiveBit));
294 assert(hasEither(a, b | _caseInsensitiveBit));
295 assert(hasEither(a, _caseInsensitiveBit));
296 assert(doesNotHaveEither(a, b));
297 }
298 }
299
300
301
302 private template _ArgFlagsInfo(Flags)
303 {
304 immutable Flags[argFlagCount] incompatible = getFlagsInfo!IncompatibleWithAnyOf;
305 immutable Flags[argFlagCount] requiresAll = getFlagsInfo!RequiresAllOf;
306 immutable Flags[argFlagCount] requiresExactlyOne = getFlagsInfo!RequiresExactlyOneOf;
307 immutable Flags[argFlagCount] requiresOneOrMore = getFlagsInfo!RequiresOneOrMoreOf;
308
309 enum argFlagCount =
310 (){
311 import std.algorithm;
312 import std.range;
313 return Flags.max.bitsSet.array[$ - 1] + 1;
314 }();
315
316 Flags[argFlagCount] getFlagsInfo(TUDA)()
317 {
318 Flags[argFlagCount] result;
319
320 static foreach (memberName; __traits(allMembers, Flags))
321 {{
322 size_t index = __traits(getMember, Flags, memberName).bitsSet.front;
323 static foreach (uda; __traits(getAttributes, __traits(getMember, Flags, memberName)))
324 {
325 static if (is(typeof(uda) == TUDA))
326 {
327 result[index] = uda.value;
328 }
329 }
330 }}
331
332 return result;
333 }
334 }