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 }