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 }