1 module jcli.commandparser.parser; 2 3 import jcli.argbinder, jcli.argparser, jcli.core, jcli.introspect; 4 import std.conv : to; 5 6 /// This is needed mostly for testing purposes 7 enum CommandParsingErrorCode 8 { 9 none = 0, 10 /// Since we don't need to test the underlying argument parser, this is a placeholder. 11 /// Ideally, we should mirror those errors here, or something like that. 12 argumentParserError = 1 << 0, 13 /// 14 tooManyPositionalArgumentsError = 1 << 1, 15 /// Either trying to bind a positional argument or a named one. 16 bindError = 1 << 2, 17 /// 18 duplicateNamedArgumentError = 1 << 3, 19 /// 20 countArgumentGivenValueError = 1 << 4, 21 /// 22 noValueForNamedArgumentError = 1 << 5, 23 /// 24 unknownNamedArgumentError = 1 << 6, 25 /// 26 tooFewPositionalArgumentsError = 1 << 7, 27 /// 28 missingNamedArgumentsError = 1 << 8, 29 } 30 31 private alias ErrorCode = CommandParsingErrorCode; 32 33 struct DefaultParseErrorHandler 34 { 35 const @safe: 36 37 bool shouldRecord(ErrorCode errorCode) 38 { 39 return true; 40 } 41 void format(T...)(ErrorCode errorCode, T args) 42 { 43 import std.stdio; 44 writefln(args); 45 } 46 } 47 48 // TODO: 49 // I think there is a solid reason to do a handler interface here. 50 // It would have the two methods like in the default handler above. 51 // The format method will have to be a vararg one. 52 // Why? To control the binary size. A couple of virtual calls for the errors will be 53 // better than generating code for this function twice, even tho meh I don't know. 54 // It isn't that big in code, but when instantiated for a large struct, it could bloat 55 // the binary size. 56 // interface IRecordError{} 57 58 /// The errors are always recorded (or ignored) via a handler object. 59 struct ParseResult(CommandType) 60 { 61 size_t errorCount; 62 // TODO: we should take the command as a ref? 63 CommandType value; 64 65 @safe nothrow @nogc pure const: 66 bool isOk() { return errorCount == 0; } 67 bool isError() { return errorCount > 0; } 68 } 69 70 struct CommandParsingContext(size_t numBitsInStorage) 71 { 72 size_t currentPositionalArgIndex = 0; 73 size_t errorCounter = 0; 74 75 static if (numBitsInStorage > 0) 76 { 77 import std.bitmanip : BitArray; 78 79 private static size_t getNumberOfSizetsNecessaryToHoldBits(size_t numBits) 80 { 81 static size_t ceilDivide(size_t a, size_t b) 82 { 83 return (a + b - 1) / b; 84 } 85 size_t numBytesToHoldBits = ceilDivide(numBits, 8); 86 size_t numSizeTsToHoldBits = ceilDivide(numBytesToHoldBits, 8); 87 return numSizeTsToHoldBits; 88 } 89 90 enum bitStorageSize = getNumberOfSizetsNecessaryToHoldBits(numBitsInStorage); 91 92 // Is 0 if a required argument has been found, otherwise 1. 93 // For non-required arguments this is always 0. 94 size_t[bitStorageSize] requiredNamedArgHasNotBeenFoundBitArrayStorage = 0; 95 size_t[bitStorageSize] namedArgHasBeenFoundBitArrayStorage = 0; 96 97 const @nogc nothrow pure: 98 99 BitArray requiredNamedArgHasNotBeenFoundBitArray() @trusted 100 { 101 return BitArray(cast(void[]) requiredNamedArgHasNotBeenFoundBitArrayStorage[], numBitsInStorage); 102 } 103 104 BitArray namedArgHasBeenFoundBitArray() @trusted 105 { 106 return BitArray(cast(void[]) namedArgHasBeenFoundBitArrayStorage[], numBitsInStorage); 107 } 108 } 109 else 110 { 111 enum size_t bitStorageSize = 0; 112 } 113 } 114 115 void resetNamedArgumentArrayStorage 116 ( 117 alias ArgumentsInfo, 118 Context : CommandParsingContext!size, size_t size 119 ) 120 ( 121 ref scope Context context 122 ) 123 @nogc nothrow pure @trusted 124 { 125 static if (size > 0) 126 { 127 context.requiredNamedArgHasNotBeenFoundBitArrayStorage = 0; 128 129 static foreach (index, arg; ArgumentsInfo.named) 130 { 131 static if (arg.flags.has(ArgFlags._requiredBit)) 132 { 133 context.requiredNamedArgHasNotBeenFoundBitArray[index] = true; 134 } 135 } 136 } 137 } 138 139 /// Reserved for later use, but kinda serves no purpose right now. 140 /// This return value should be ignored for now. 141 enum ConsumeSingleArgumentResultKind 142 { 143 _tokenizerEmptyBit = 1, 144 _doneBit = 2, 145 146 // No flag implies the token has been consumed. 147 // On error, ok is still returned. 148 // Check `context.errorCount` for the errors. 149 ok = 0, 150 151 // The parsing preemptively finished. 152 // Does not mean the tokenizer is empty. 153 done = _doneBit, 154 } 155 156 157 /// Consumes one or more tokens from the given tokenizer, 158 /// Will overconsume positional and orphan arguments. 159 ConsumeSingleArgumentResultKind consumeSingleArgumentIntoCommand 160 ( 161 alias bindArgument, 162 Context : CommandParsingContext!numBitsInStorage, size_t numBitsInStorage, 163 CommandType, 164 TArgTokenizer : ArgTokenizer!T, T, 165 TErrorHandler 166 ) 167 ( 168 ref scope Context context, 169 ref scope CommandType command, 170 ref scope TArgTokenizer tokenizer, 171 ref scope TErrorHandler errorHandler 172 ) 173 { 174 alias ArgumentInfo = jcli.introspect.CommandArgumentsInfo!CommandType; 175 alias Kind = ArgToken.Kind; 176 alias ResultKind = ConsumeSingleArgumentResultKind; 177 178 static assert(numBitsInStorage >= ArgumentInfo.named.length); 179 180 const currentArgToken = tokenizer.front; 181 182 // This has to be handled before the switch, because we pop before the switch. 183 if (currentArgToken.kind == Kind.twoDashesDelimiter) 184 { 185 static if (ArgumentInfo.takesRaw) 186 { 187 auto rawArgumentStrings = tokenizer.leftoverRange(); 188 command.getArgumentFieldRef!(ArgumentInfo.raw) = rawArgumentStrings; 189 // To the outside it looks like we have consumed all arguments. 190 tokenizer = typeof(tokenizer).init; 191 } 192 193 tokenizer.popFront(); 194 return ResultKind.done; 195 } 196 197 tokenizer.popFront(); 198 199 void recordError(T...)(ErrorCode code, auto ref T args) 200 { 201 if (errorHandler.shouldRecord(code)) 202 { 203 errorHandler.format(code, args); 204 context.errorCounter++; 205 } 206 } 207 208 // Cannot be final, since there are flags. 209 switch (currentArgToken.kind) 210 { 211 default: 212 { 213 assert(0); 214 } 215 216 // if (currentArgToken.kind & Kind.errorBit) 217 // { 218 // } 219 case Kind.error_inputAfterClosedQuote: 220 case Kind.error_malformedQuotes: 221 case Kind.error_noValueForNamedArgument: 222 case Kind.error_singleDash: 223 case Kind.error_spaceAfterAssignment: 224 case Kind.error_spaceAfterDashes: 225 case Kind.error_threeOrMoreDashes: 226 case Kind.error_unclosedQuotes: 227 { 228 // for now just log and go next 229 // TODO: better errors 230 recordError( 231 ErrorCode.argumentParserError, 232 "An error has occured in the parser: `%s`", 233 currentArgToken.kind.stringof); 234 return ResultKind.ok; 235 } 236 237 case Kind.namedArgumentValue: 238 { 239 assert(false, "This one should have been handled in the named argument section."); 240 } 241 242 // (currentArgToken.kind & (Kind._positionalArgumentBit | Kind.valueBit)) 243 // == Kind._positionalArgumentBit | Kind.valueBit 244 case Kind.namedArgumentValueOrOrphanArgument: 245 case Kind.positionalArgument: 246 // TODO: imo orphan arguments should not be treated like positional ones. 247 case Kind.orphanArgument: 248 { 249 InnerSwitch: switch (context.currentPositionalArgIndex) 250 { 251 default: 252 { 253 static if (ArgumentInfo.takesOverflow) 254 { 255 // TODO: 256 // Could allow to convert this one before appending, it works with `aggregate` already. 257 // Will have to do some flags inference and validation ideally, specifically for the overflow arg. 258 // I guess it will be beneficial to add `ArgFlags.overflowBit` and `ArgFlags.rawBit` 259 // so that every type of argument can be expressed just via flags. 260 command.getArgumentFieldRef!(ArgumentInfo.overflow) 261 ~= currentArgToken.fullSlice; 262 } 263 else 264 { 265 recordError( 266 ErrorCode.tooManyPositionalArgumentsError, 267 "Too many (%d) positional arguments detected near `%s`.", 268 context.currentPositionalArgIndex + 1, 269 currentArgToken.fullSlice); 270 } 271 break InnerSwitch; 272 } 273 static foreach (positionalIndex, positional; ArgumentInfo.positional) 274 { 275 case positionalIndex: 276 { 277 auto result = bindArgument!positional(command, currentArgToken.valueSlice); 278 if (result.isError) 279 { 280 recordError( 281 ErrorCode.bindError, 282 "An error occured while trying to bind the positional argument `%s` at index %d: " 283 ~ "%s; Error code %d.", 284 positional.identifier, positionalIndex, 285 result.error, result.errorCode); 286 } 287 break InnerSwitch; 288 } 289 } 290 } // InnerSwitch 291 292 context.currentPositionalArgIndex++; 293 return ResultKind.ok; 294 } 295 296 // currentArgToken.kind & Kind.argumentNameBit 297 case Kind.fullNamedArgumentName: 298 case Kind.shortNamedArgumentName: 299 { 300 // Check if any of the arguments matched the name 301 static foreach (namedArgIndex, namedArgInfo; ArgumentInfo.named) 302 {{ 303 if (isNameMatch!namedArgInfo(currentArgToken.nameSlice)) 304 { 305 void recordBindError(R)(in R result) 306 { 307 recordError( 308 ErrorCode.bindError, 309 "An error occured while trying to bind the named argument `%s`: " 310 ~ "%s; Error code %d.", 311 namedArgInfo.identifier, 312 result.error, result.errorCode); 313 } 314 315 static if (namedArgInfo.flags.doesNotHave(ArgFlags._multipleBit)) 316 { 317 if (context.namedArgHasBeenFoundBitArray[namedArgIndex] 318 // This error type being ignored means the user implicitly wants 319 // all of the arguments to be processed as though they had the canRedefine bit. 320 && errorHandler.shouldRecord(ErrorCode.duplicateNamedArgumentError)) 321 { 322 errorHandler.format( 323 ErrorCode.duplicateNamedArgumentError, 324 "Duplicate named argument %s.", 325 namedArgInfo.name); 326 context.errorCounter++; 327 328 // Skip its value too 329 if (!tokenizer.empty 330 && tokenizer.front.kind == Kind.namedArgumentValue) 331 { 332 tokenizer.popFront(); 333 } 334 return ResultKind.ok; 335 } 336 } 337 context.namedArgHasBeenFoundBitArray[namedArgIndex] = true; 338 339 static if (namedArgInfo.flags.has(ArgFlags._requiredBit)) 340 context.requiredNamedArgHasNotBeenFoundBitArray[namedArgIndex] = false; 341 342 static if (namedArgInfo.flags.has(ArgFlags._parseAsFlagBit)) 343 { 344 // Default to setting the field to true, since it's defined. 345 // The only scenario where it should be false is if `--arg false` is used. 346 // TODO: 347 // Allow custom flag values with a UDA. 348 // parseAsFlag should not be restricted to bool, ideally. 349 command.getArgumentFieldRef!namedArgInfo = true; 350 351 if (tokenizer.empty) 352 return ResultKind.ok; 353 354 auto nextArgToken = tokenizer.front; 355 if (nextArgToken.kind.doesNotHave(Kind.valueBit)) 356 return ResultKind.ok; 357 358 // Providing a value to a bool is optional, so to avoid producing an unwanted 359 // error, we need to white list the allowed values if it's not explicitly 360 // marked as the argument's value. 361 // 362 // Actually, no! This does not work, because custom converters exist. 363 // Imagine the user specified that bool to have a switch converter, aka on/off. 364 // We try and convert, we just swallow the error if it does not succeed. 365 // 366 // If we want to not allocate the extra error string here, we could 367 // forward the error handler to the binder, maybe?? 368 // 369 // if(nextArgToken.kind == Kind.namedArgumentValueOrOrphanArgument) 370 // continue OuterLoop; 371 372 auto bindResult = bindArgument!namedArgInfo(command, nextArgToken.valueSlice); 373 374 // So there are 3 possibilities: 375 // 1. the value was compatible with the converter, and we got true or false. 376 if (bindResult.isOk) 377 { 378 tokenizer.popFront(); 379 } 380 381 // 2. the value was not compatible with the converter, and we got an error. 382 // bindResult.isError is always true here. 383 else if ( 384 // so here we check if it had definitely been for this argument. 385 // aka this will be true when `--arg=kek` is passed, but not when `--arg kek` is passed. 386 nextArgToken.kind.doesNotHave(Kind.orphanArgumentBit)) 387 { 388 recordBindError(bindResult); 389 tokenizer.popFront(); 390 } 391 392 // 3. It's an error, but the value can be interpreted as another sort of argument. 393 // For now, we consider orphan arguments to be just positional arguments, but not for long. 394 // `--arg kek` would get to this point and ignore the `kek`. 395 return ResultKind.ok; 396 } 397 398 else static if (namedArgInfo.argument.flags.has(ArgFlags._countBit)) 399 { 400 alias TypeOfField = typeof(command.getArgumentFieldRef!namedArgInfo); 401 static assert(__traits(isArithmetic, TypeOfField)); 402 403 static if (namedArgInfo.argument.flags.has(ArgFlags._repeatableNameBit)) 404 { 405 const valueToAdd = cast(TypeOfField) currentArgToken.valueSlice.length; 406 } 407 else 408 { 409 const valueToAdd = cast(TypeOfField) 1; 410 } 411 command.getArgumentFieldRef!namedArgInfo += valueToAdd; 412 413 if (tokenizer.empty) 414 return ResultKind.ok; 415 416 auto nextArgToken = tokenizer.front; 417 if (nextArgToken.kind == Kind.namedArgumentValue) 418 { 419 recordError( 420 ErrorCode.countArgumentGivenValueError, 421 "The count argument %s cannot be given a value, got `%s`.", 422 namedArgInfo.name, 423 nextArgToken.valueSlice); 424 tokenizer.popFront(); 425 } 426 return ResultKind.ok; 427 } 428 429 else 430 { 431 static assert(namedArgInfo.flags.doesNotHave(ArgFlags._parseAsFlagBit)); 432 433 if (tokenizer.empty) 434 { 435 recordError( 436 ErrorCode.noValueForNamedArgumentError, 437 "Expected a value for the argument `%s`.", 438 namedArgInfo.name); 439 return ResultKind.ok; 440 } 441 442 auto nextArgToken = tokenizer.front; 443 if (nextArgToken.kind.doesNotHave(Kind.namedArgumentValueBit)) 444 { 445 recordError( 446 ErrorCode.noValueForNamedArgumentError, 447 "Expected a value for the argument `%s`, got `%s`.", 448 namedArgInfo.name, 449 nextArgToken.valueSlice); 450 451 // Don't skip it, because it might be the name of another option. 452 // TODO: What do we do here tho?? 453 // tokenizer.popFront(); 454 455 return ResultKind.ok; 456 } 457 458 { 459 // NOTE: ArgFlags._accumulateBit should have been handled in the binder. 460 auto result = bindArgument!namedArgInfo(command, nextArgToken.valueSlice); 461 if (result.isError) 462 recordBindError(result); 463 tokenizer.popFront(); 464 return ResultKind.ok; 465 } 466 } 467 } 468 }} // static foreach 469 470 /// TODO: conditionally allow unknown arguments 471 recordError( 472 ErrorCode.unknownNamedArgumentError, 473 "Unknown named argument `%s`.", 474 currentArgToken.fullSlice); 475 476 if (tokenizer.empty) 477 return ResultKind.ok; 478 479 if (tokenizer.front.kind == Kind.namedArgumentValue) 480 tokenizer.popFront(); 481 482 return ResultKind.ok; 483 } 484 } // TokenKindSwitch 485 } 486 487 488 void maybeReportParseErrorsFromFinalContext 489 ( 490 alias ArgumentInfo, 491 Context : CommandParsingContext!numBitsInStorage, size_t numBitsInStorage, 492 TErrorHandler 493 ) 494 ( 495 ref scope Context context, 496 ref scope TErrorHandler errorHandler 497 ) 498 { 499 if (context.currentPositionalArgIndex < ArgumentInfo.numRequiredPositionalArguments) 500 { 501 enum messageFormat = 502 (){ 503 string ret = "Expected "; 504 if (ArgumentInfo.positional.length == ArgumentInfo.numRequiredPositionalArguments) 505 ret ~= "exactly"; 506 else 507 ret ~= "at least"; 508 509 ret ~= " %d positional arguments but got only %d. The command takes the following positional arguments: "; 510 511 { 512 import std.algorithm : map; 513 import std.string : join; 514 enum argList = ArgumentInfo.positional.map!(a => a.name).join(", "); 515 ret ~= argList; 516 } 517 return ret; 518 }(); 519 520 if (errorHandler.shouldRecord(ErrorCode.tooFewPositionalArgumentsError)) 521 { 522 errorHandler.format( 523 ErrorCode.tooFewPositionalArgumentsError, 524 messageFormat, 525 ArgumentInfo.numRequiredPositionalArguments, 526 context.currentPositionalArgIndex); 527 context.errorCounter++; 528 } 529 } 530 531 static if (ArgumentInfo.named.length > 0) 532 { 533 if (context.requiredNamedArgHasNotBeenFoundBitArray.count > 0 534 && errorHandler.shouldRecord(ErrorCode.missingNamedArgumentsError)) 535 { 536 import std.array; 537 538 // May want to return the whole thing here, but I think the first thing 539 // in the pattern should be the most descriptive anyway so should be encouraged. 540 string getPattern(size_t index) 541 { 542 return ArgumentInfo.named[index].name; 543 } 544 545 auto failMessageBuilder = appender!string("The following required named arguments were not found: "); 546 auto notFoundArgumentIndexes = context.requiredNamedArgHasNotBeenFoundBitArray.bitsSet; 547 failMessageBuilder ~= getPattern(notFoundArgumentIndexes.front); 548 notFoundArgumentIndexes.popFront(); 549 550 foreach (notFoundArgumentIndex; notFoundArgumentIndexes) 551 { 552 failMessageBuilder ~= ", "; 553 failMessageBuilder ~= getPattern(notFoundArgumentIndex); 554 } 555 556 errorHandler.format( 557 ErrorCode.missingNamedArgumentsError, 558 failMessageBuilder[]); 559 context.errorCounter++; 560 } 561 } 562 } 563 564 565 /// For now, I opt to keep the parts separate, because then 566 /// the code is more parametrizable. Currently, it's not totally 567 /// clear which parts of this are needed separately. 568 template parseCommand(CommandType, alias bindArgument = jcli.argbinder.bindArgument!()) 569 { 570 alias ArgumentsInfo = jcli.introspect.CommandArgumentsInfo!CommandType; 571 alias Result = ParseResult!CommandType; 572 alias _parseCommand = .parseCommand!(CommandType, bindArgument); 573 574 static import std.stdio; 575 ParseResult!CommandType parseCommand(scope string[] args) 576 { 577 scope auto dummy = DefaultParseErrorHandler(); 578 return _parseCommand(args, dummy); 579 } 580 581 ParseResult!CommandType parseCommand(TErrorHandler) 582 ( 583 scope string[] args, 584 ref scope TErrorHandler errorHandler 585 ) 586 { 587 scope auto parser = argTokenizer(args); 588 return _parseCommand(parser, errorHandler); 589 } 590 591 ParseResult!CommandType parseCommand 592 ( 593 TErrorHandler, 594 TArgTokenizer : ArgTokenizer!T, T 595 ) 596 ( 597 ref scope TArgTokenizer tokenizer, 598 ref scope TErrorHandler errorHandler 599 ) 600 { 601 auto command = CommandType(); 602 603 CommandParsingContext!(ArgumentsInfo.named.length) context; 604 resetNamedArgumentArrayStorage!ArgumentsInfo(context); 605 606 while (!tokenizer.empty) 607 { 608 const _ = consumeSingleArgumentIntoCommand!(bindArgument)( 609 context, command, tokenizer, errorHandler); 610 } 611 612 maybeReportParseErrorsFromFinalContext!ArgumentsInfo(context, errorHandler); 613 614 return typeof(return)(context.errorCounter, command); 615 } 616 } 617 618 619 version(unittest) 620 { 621 import std.algorithm; 622 import std.array; 623 624 struct ErrorCodeHandler 625 { 626 ErrorCode errorCodes; 627 628 bool shouldRecord(ErrorCode errorCode) 629 { 630 return true; 631 } 632 633 void format(T...)(ErrorCode errorCode, T args) 634 { 635 errorCodes |= errorCode; 636 } 637 638 bool hasError(ErrorCode errorCode) 639 { 640 return (errorCodes & errorCode) == errorCode; 641 } 642 643 void clear() 644 { 645 errorCodes = ErrorCode.none; 646 } 647 } 648 649 struct IgnoreSetErrorHandler 650 { 651 ErrorCode ignoredErrorCodes; 652 ErrorCode errorCodes; 653 654 bool shouldRecord(ErrorCode errorCode) 655 { 656 return (errorCode & ignoredErrorCodes) == 0; 657 } 658 659 void format(T...)(ErrorCode errorCode, T args) 660 { 661 errorCodes |= errorCode; 662 } 663 664 bool hasError(ErrorCode errorCode) 665 { 666 return (errorCodes & errorCode) == errorCode; 667 } 668 669 void clear() 670 { 671 errorCodes = ErrorCode.none; 672 } 673 } 674 675 struct InMemoryErrorHandler 676 { 677 ErrorCode errorCodes; 678 Appender!(char[]) output; 679 680 bool shouldRecord(ErrorCode errorCode) 681 { 682 return true; 683 } 684 685 import std.format : formattedWrite; 686 void format(T...)(ErrorCode errorCode, T args) 687 { 688 errorCodes |= errorCode; 689 formattedWrite(output, args, "\n"); 690 } 691 692 bool hasError(ErrorCode errorCode) 693 { 694 return (errorCodes & errorCode) == errorCode; 695 } 696 697 void clear() 698 { 699 errorCodes = ErrorCode.none; 700 output.clear(); 701 } 702 } 703 704 IgnoreSetErrorHandler createIgnoreSetErrorHandler(ErrorCode ignored) 705 { 706 return typeof(return)(ignored, ErrorCode.none); 707 } 708 709 ErrorCodeHandler createErrorCodeHandler() 710 { 711 return typeof(return)(ErrorCode.none); 712 } 713 714 InMemoryErrorHandler createInMemoryErrorHandler() 715 { 716 return typeof(return)(ErrorCode.none, appender!(char[])); 717 } 718 719 mixin template ParseBoilerplate(Struct) 720 { 721 ErrorCodeHandler output = createErrorCodeHandler(); 722 auto parse(scope string[] args) 723 { 724 output.clear(); 725 return parseCommand!Struct(args, output); 726 } 727 } 728 } 729 730 731 unittest 732 { 733 static struct S 734 { 735 @ArgPositional 736 string a; 737 } 738 739 mixin ParseBoilerplate!S; 740 741 { 742 // Ok 743 const result = parse(["b"]); 744 assert(result.isOk); 745 assert(result.value.a == "b"); 746 assert(output.errorCodes == 0); 747 } 748 { 749 const result = parse(["-a", "b"]); 750 assert(result.isError); 751 assert(output.hasError(ErrorCode.unknownNamedArgumentError)); 752 } 753 { 754 const result = parse(["a", "b"]); 755 assert(result.isError); 756 assert(result.value.a == "a"); 757 assert(output.hasError(ErrorCode.tooManyPositionalArgumentsError)); 758 } 759 { 760 const result = parse([]); 761 assert(result.isError); 762 assert(output.hasError(ErrorCode.tooFewPositionalArgumentsError)); 763 } 764 } 765 766 767 unittest 768 { 769 static struct S 770 { 771 @ArgNamed 772 string a; 773 } 774 775 mixin ParseBoilerplate!S; 776 777 { 778 const result = parse(["-a", "b"]); 779 assert(result.isOk); 780 assert(result.value.a == "b"); 781 } 782 { 783 const result = parse([]); 784 assert(result.isError); 785 assert(output.hasError(ErrorCode.missingNamedArgumentsError)); 786 } 787 { 788 const result = parse(["-a"]); 789 assert(result.isError); 790 assert(output.hasError(ErrorCode.noValueForNamedArgumentError)); 791 } 792 { 793 const result = parse(["a"]); 794 assert(result.isError); 795 assert(output.hasError(ErrorCode.missingNamedArgumentsError)); 796 } 797 { 798 const result = parse(["-a", "b", "-a", "c"]); 799 assert(result.isError); 800 assert(output.hasError(ErrorCode.duplicateNamedArgumentError)); 801 } 802 { 803 const result = parse(["-b"]); 804 assert(result.isError); 805 assert(output.hasError(ErrorCode.missingNamedArgumentsError)); 806 assert(output.hasError(ErrorCode.unknownNamedArgumentError)); 807 } 808 } 809 810 unittest 811 { 812 static struct S 813 { 814 @ArgNamed 815 @(ArgConfig.accumulate) 816 string a; 817 } 818 // TODO: static assert the parse function does not compile. 819 } 820 821 unittest 822 { 823 static struct S 824 { 825 @ArgNamed 826 @(ArgConfig.optional) 827 string a; 828 } 829 830 mixin ParseBoilerplate!S; 831 832 { 833 const result = parse(["-a", "b"]); 834 assert(result.isOk); 835 assert(result.value.a == "b"); 836 } 837 { 838 const result = parse([]); 839 assert(result.isOk); 840 assert(result.value.a == typeof(result.value.a).init); 841 } 842 { 843 const result = parse(["-a"]); 844 assert(result.isError); 845 assert(output.hasError(ErrorCode.noValueForNamedArgumentError)); 846 } 847 { 848 const result = parse(["-a", "b", "-a", "c"]); 849 assert(result.isError); 850 assert(output.hasError(ErrorCode.duplicateNamedArgumentError)); 851 } 852 } 853 854 unittest 855 { 856 static struct S 857 { 858 @ArgNamed 859 @(ArgConfig.aggregate) 860 string[] a; 861 } 862 863 mixin ParseBoilerplate!S; 864 865 { 866 const result = parse(["-a", "b"]); 867 assert(result.isOk); 868 assert(result.value.a == ["b"]); 869 } 870 { 871 const result = parse([]); 872 assert(result.isError); 873 assert(output.hasError(ErrorCode.missingNamedArgumentsError)); 874 } 875 { 876 const result = parse(["-a"]); 877 assert(result.isError); 878 assert(output.hasError(ErrorCode.noValueForNamedArgumentError)); 879 } 880 { 881 const result = parse(["-a", "b", "-a", "c"]); 882 assert(result.isOk); 883 assert(result.value.a == ["b", "c"]); 884 } 885 } 886 887 unittest 888 { 889 static struct S 890 { 891 @ArgNamed 892 @(ArgConfig.aggregate | ArgConfig.optional) 893 string[] a; 894 } 895 896 mixin ParseBoilerplate!S; 897 898 { 899 const result = parse(["-a", "b"]); 900 assert(result.isOk); 901 assert(result.value.a == ["b"]); 902 } 903 { 904 const result = parse([]); 905 assert(result.isOk); 906 assert(result.value.a == []); 907 } 908 { 909 const result = parse(["-a", "b", "-a", "c"]); 910 assert(result.isOk); 911 assert(result.value.a == ["b", "c"]); 912 } 913 } 914 915 unittest 916 { 917 static struct S 918 { 919 @ArgNamed 920 @(ArgConfig.accumulate) 921 int a; 922 } 923 924 mixin ParseBoilerplate!S; 925 926 { 927 const result = parse(["-a"]); 928 assert(result.isOk); 929 assert(result.value.a == 1); 930 } 931 { 932 const result = parse([]); 933 assert(result.isError); 934 assert(output.hasError(ErrorCode.missingNamedArgumentsError)); 935 } 936 { 937 const result = parse(["-a", "-a"]); 938 assert(result.isOk); 939 assert(result.value.a == 2); 940 } 941 { 942 // Here, "b" can be either a value to "-a" or a positional argument. 943 // Since "-a" does not expect a value, it should be treated as a positional argument. 944 const result = parse(["-a", "b"]); 945 assert(result.isError); 946 assert(output.hasError(ErrorCode.tooManyPositionalArgumentsError)); 947 } 948 { 949 // Here, "b" is without a doubt a named argument value, so it produces a different error. 950 const result = parse(["-a=b"]); 951 assert(result.isError); 952 assert(output.hasError(ErrorCode.countArgumentGivenValueError)); 953 } 954 { 955 // Does not imply repeatableName. 956 const result = parse(["-aaa"]); 957 assert(result.isError); 958 assert(output.hasError(ErrorCode.unknownNamedArgumentError)); 959 } 960 { 961 // Still not allowed even with a number. 962 const result = parse(["-a=3"]); 963 assert(result.isError); 964 assert(output.hasError(ErrorCode.countArgumentGivenValueError)); 965 } 966 } 967 968 unittest 969 { 970 static struct S 971 { 972 @ArgNamed 973 @(ArgConfig.accumulate | ArgConfig.optional) 974 int a; 975 } 976 977 mixin ParseBoilerplate!S; 978 979 { 980 const result = parse([]); 981 assert(result.isOk); 982 assert(result.value.a == 0); 983 } 984 { 985 const result = parse(["-a", "-a"]); 986 assert(result.isOk); 987 assert(result.value.a == 2); 988 } 989 } 990 991 unittest 992 { 993 static struct S 994 { 995 @ArgNamed 996 @(ArgConfig.parseAsFlag) 997 bool a; 998 } 999 1000 mixin ParseBoilerplate!S; 1001 1002 { 1003 const result = parse([]); 1004 assert(result.isOk); 1005 assert(result.value.a == false); 1006 } 1007 { 1008 const result = parse(["-a", "true"]); 1009 assert(result.isOk); 1010 assert(result.value.a == true); 1011 } 1012 { 1013 const result = parse(["-a", "false"]); 1014 assert(result.isOk); 1015 assert(result.value.a == false); 1016 } 1017 { 1018 const result = parse(["-a", "stuff"]); 1019 assert(result.isError); 1020 assert(output.hasError(ErrorCode.tooManyPositionalArgumentsError)); 1021 } 1022 { 1023 const result = parse(["-a=stuff"]); 1024 assert(result.isError); 1025 assert(output.hasError(ErrorCode.bindError)); 1026 } 1027 { 1028 const result = parse(["-a", "-a"]); 1029 assert(result.isError); 1030 assert(output.hasError(ErrorCode.duplicateNamedArgumentError)); 1031 } 1032 } 1033 1034 unittest 1035 { 1036 static struct S 1037 { 1038 @ArgPositional 1039 string _; 1040 1041 @ArgNamed("implicit") 1042 @(ArgConfig.parseAsFlag) 1043 bool a; 1044 } 1045 1046 mixin ParseBoilerplate!S; 1047 1048 { 1049 const result = parse(["-implicit", "positional"]); 1050 assert(result.isOk); 1051 } 1052 } 1053 1054 unittest 1055 { 1056 static struct S 1057 { 1058 @ArgPositional 1059 string a; 1060 1061 // should be implied 1062 // @(ArgFlags.optional) 1063 1064 @ArgPositional 1065 string b = "Hello"; 1066 } 1067 1068 mixin ParseBoilerplate!S; 1069 1070 { 1071 const result = parse(["a"]); 1072 assert(result.isOk); 1073 assert(result.value.a == "a"); 1074 assert(result.value.b == "Hello"); 1075 } 1076 { 1077 const result = parse(["c", "d"]); 1078 assert(result.isOk); 1079 assert(result.value.a == "c"); 1080 assert(result.value.b == "d"); 1081 } 1082 { 1083 const result = parse([]); 1084 assert(result.isError); 1085 assert(output.hasError(ErrorCode.tooFewPositionalArgumentsError)); 1086 } 1087 { 1088 const result = parse(["a", "b", "c"]); 1089 assert(result.isError); 1090 assert(output.hasError(ErrorCode.tooManyPositionalArgumentsError)); 1091 } 1092 } 1093 1094 unittest 1095 { 1096 static struct S 1097 { 1098 @ArgPositional 1099 string a; 1100 1101 @ArgOverflow 1102 string[] overflow; 1103 } 1104 1105 mixin ParseBoilerplate!S; 1106 1107 { 1108 const result = parse(["a"]); 1109 assert(result.isOk); 1110 assert(result.value.a == "a"); 1111 assert(result.value.overflow == []); 1112 } 1113 { 1114 const result = parse(["c", "d"]); 1115 assert(result.isOk); 1116 assert(result.value.a == "c"); 1117 assert(result.value.overflow == ["d"]); 1118 } 1119 { 1120 const result = parse([]); 1121 assert(result.isError); 1122 assert(output.hasError(ErrorCode.tooFewPositionalArgumentsError)); 1123 } 1124 { 1125 const result = parse(["a", "b", "c"]); 1126 assert(result.isOk); 1127 assert(result.value.a == "a"); 1128 assert(result.value.overflow == ["b", "c"]); 1129 } 1130 { 1131 const result = parse(["a", "b", "-c"]); 1132 assert(result.isError); 1133 assert(output.hasError(ErrorCode.unknownNamedArgumentError)); 1134 } 1135 } 1136 1137 unittest 1138 { 1139 static struct S 1140 { 1141 @ArgPositional 1142 string a; 1143 1144 @ArgRaw 1145 string[] raw; 1146 } 1147 1148 mixin ParseBoilerplate!S; 1149 1150 { 1151 const result = parse(["a"]); 1152 assert(result.isOk); 1153 assert(result.value.a == "a"); 1154 assert(result.value.raw == []); 1155 } 1156 { 1157 const result = parse(["c", "--", "d"]); 1158 assert(result.isOk); 1159 assert(result.value.raw == ["d"]); 1160 } 1161 { 1162 const result = parse(["c", "--", "- Stuff -"]); 1163 assert(result.isOk); 1164 assert(result.value.a == "c"); 1165 assert(result.value.raw == ["- Stuff -"]); 1166 } 1167 { 1168 // Normally, non utf8 argument names are not supported, but here they are not parsed at all. 1169 const result = parse(["c", "--", "--Штука", "-物事"]); 1170 assert(result.isOk); 1171 assert(result.value.a == "c"); 1172 assert(result.value.raw == ["--Штука", "-物事"]); 1173 } 1174 } 1175 1176 unittest 1177 { 1178 static struct S 1179 { 1180 @ArgNamed 1181 @(ArgConfig.repeatableName | ArgConfig.optional) 1182 int a; 1183 } 1184 1185 mixin ParseBoilerplate!S; 1186 1187 { 1188 const result = parse([]); 1189 assert(result.isOk); 1190 assert(result.value.a == 0); 1191 } 1192 { 1193 const result = parse(["-a"]); 1194 assert(result.isOk); 1195 assert(result.value.a == 1); 1196 } 1197 { 1198 const result = parse(["-aaa"]); 1199 assert(result.isOk); 1200 assert(result.value.a == 3); 1201 } 1202 { 1203 const result = parse(["-aaa", "-a"]); 1204 assert(result.isError); 1205 assert(output.hasError(ErrorCode.duplicateNamedArgumentError)); 1206 } 1207 } 1208 1209 unittest 1210 { 1211 static struct S 1212 { 1213 @ArgNamed 1214 int a; 1215 } 1216 1217 static auto parse(scope string[] args, ref IgnoreSetErrorHandler handler) 1218 { 1219 handler.clear(); 1220 return parseCommand!S(args, handler); 1221 } 1222 1223 { 1224 auto handler = createIgnoreSetErrorHandler(ErrorCode.duplicateNamedArgumentError); 1225 const result = parse(["-a=2", "-a=3"], handler); 1226 assert(result.isOk); 1227 assert(result.value.a == 3); 1228 } 1229 }