1 module jcli.commandparser.parser;
2 
3 import jcli.argbinder, jcli.argparser, jcli.core, jcli.introspect, std;
4 
5 struct CommandParser(alias CommandT_, alias ArgBinderInstance_ = ArgBinder!())
6 {
7     alias CommandT          = CommandT_;
8     alias CommandInfo       = commandInfoFor!CommandT;
9     alias ArgBinderInstance = ArgBinderInstance_;
10 
11     static ResultOf!CommandT parse()(string[] args)
12     {
13         return parse(ArgParser(args));
14     }
15 
16     static ResultOf!CommandT parse()(ArgParser parser)
17     {
18         CommandT command;
19 
20         enum MaxPositionals = CommandInfo.positionalArgs.length;
21         size_t positionCount;
22         Pattern[string] requiredNamed; // key is identifier
23         bool[string] namedFound; // key is identifier, value is dummy.
24 
25         static foreach(arg; CommandInfo.namedArgs)
26         {
27             static if(!(arg.existence & ArgExistence.optional))
28                 requiredNamed[arg.identifier] = arg.uda.pattern;
29         }
30 
31         while(!parser.empty)
32         {
33             auto arg = parser.front;
34             scope(exit) parser.popFront();
35 
36             if(arg.fullSlice == "--")
37             {
38                 static if(CommandInfo.rawArg != typeof(CommandInfo.rawArg).init)
39                 {
40                     parser.popFront();
41                     getArg!(CommandInfo.rawArg)(command) = parser;
42                 }
43                 break;
44             }
45 
46             OuterSwitch: final switch(arg.kind) with(ArgParser.Result.Kind)
47             {
48                 case rawText:
49                     {
50                         Switch: switch(positionCount)
51                         {
52                             static foreach(i, positional; CommandInfo.positionalArgs)
53                             {
54                                 case i:
55                                     auto result = ArgBinderInstance.bind!positional(arg.fullSlice, command);
56                                     if(!result.isOk)
57                                         return fail!CommandT(result.error);
58                                     break Switch;
59                             }
60 
61                             // I might be hitting a compiler bug, because without this, the "positionCount++" is sometimes,
62                             // not all the time, but sometimes unreachable
63                             case 200302104:
64                                 break;
65 
66                             default:
67                                 static if(CommandInfo.overflowArg == typeof(CommandInfo.overflowArg).init)
68                                     return fail!CommandT("Too many positional arguments near '%s'. Expected %s positional arguments.".format(arg.fullSlice, MaxPositionals));
69                                 else
70                                 {
71                                     getArg!(CommandInfo.overflowArg)(command) ~= arg.fullSlice;
72                                     break;
73                                 }
74                         }
75                     }
76                     positionCount++;
77                     break;
78 
79                 case argument:
80                     static foreach(named; CommandInfo.namedArgs)
81                     {
82                         if(named.uda.pattern.match(arg.nameSlice, (named.config & ArgConfig.caseInsensitive) > 0).matched
83                         || (named.scheme == ArgParseScheme.repeatableName && named.uda.pattern.patterns.any!(p => p.length == 1 && arg.nameSlice.all!(c => c == p[0]))))
84                         {
85                             static if(!(named.existence & ArgExistence.optional))
86                                 requiredNamed[named.identifier] = named.uda.pattern;
87                             if((named.existence & ArgExistence.multiple) == 0)
88                             {
89                                 enforce((named.identifier in namedFound) is null,
90                                     "Named argument %s cannot be specified multiple times.".format(named.identifier)
91                                 );
92                             }
93                             namedFound[named.identifier] = true;
94 
95                             static if(named.scheme == ArgParseScheme.bool_)
96                             {
97                                 static assert(named.action == ArgAction.normal, "ArgParseScheme.bool_ conflicts with anything that isn't ArgAction.normal.");
98 
99                                 auto value = true;
100                                 auto copy = parser;
101                                 copy.popFront();
102                                 if(!copy.empty && copy.front.kind == rawText)
103                                 {
104                                     if(copy.front.fullSlice == "true" || copy.front.fullSlice == "false")
105                                     {
106                                         parser.popFront(); // Keep main parser in sync
107                                         value = copy.front.fullSlice.to!bool;
108                                     }
109                                 }
110                                 getArg!named(command) = value;
111                             }
112                             else static if(named.scheme == ArgParseScheme.normal)
113                             {
114                                 static if(named.action == ArgAction.normal)
115                                 {
116                                     parser.popFront();
117                                     if(parser.empty)
118                                         return fail!CommandT("Expected value after argument "~arg.fullSlice~" but hit end of args.");
119                                     if(parser.front.kind == argument)
120                                         return fail!CommandT("Expected value after argument "~arg.fullSlice~" but instead got argument "~parser.front.fullSlice);
121 
122                                     auto result = ArgBinderInstance.bind!named(parser.front.fullSlice, command);
123                                     if(!result.isOk)
124                                         return fail!CommandT(result.error);
125                                 }
126                                 else static if(named.action == ArgAction.count)
127                                     getArg!named(command)++;
128                                 else static assert(false, "Update me please.");
129                             }
130                             else static if(named.scheme == ArgParseScheme.repeatableName)
131                             {
132                                 static assert(named.action == ArgAction.count, "ArgParseScheme.bool_ conflicts with anything that isn't ArgAction.count.");
133                                 getArg!named(command) += arg.nameSlice.length;
134                             }
135                             break OuterSwitch;
136                         }
137                     }
138                     return fail!CommandT("Unknown argument: "~arg.fullSlice);
139             }
140         }
141 
142         enforce(
143             positionCount >= CommandInfo.positionalArgs.length,
144             "Expected %s positional arguments but got %s instead. Missing the following required positional arguments:%s".format(
145                 CommandInfo.positionalArgs.length, positionCount,
146                 CommandInfo.positionalArgs[positionCount..$]
147                            .map!(arg => arg.uda.name.length ? arg.uda.name : "NO_NAME")
148                            .fold!((a,b) => a~" "~b)("")
149             )
150         );
151 
152         Pattern[] notFound;
153         foreach(k, v; requiredNamed)
154         {
155             if(!namedFound.byKey.any!(key => key == k))
156                 notFound ~= v;
157         }
158 
159         if(notFound.length)
160         {
161             return fail!CommandT(
162                 "The following required named arguments were not found: "
163                 ~notFound.fold!((a,b) => a.length ? a~", "~b.pattern : b.pattern)("")
164             );
165         }
166         
167         return ok(command);
168     }
169 }
170 
171 unittest
172 {
173     @Command("ab")
174     static struct S
175     {
176         @ArgPositional
177         string s;
178 
179 
180         @ArgNamed("abc")
181         string a;
182 
183         @ArgNamed("b")
184         @(ArgExistence.multiple)
185         string b;
186 
187         @ArgNamed("c")
188         bool c;
189 
190         @ArgNamed("d")
191         bool d;
192 
193         @ArgNamed("e")
194         bool e;
195 
196         @ArgPositional
197         string f;
198 
199         @ArgNamed("v")
200         @(ArgAction.count)
201         int v;
202 
203         @ArgOverflow
204         string[] overflow;
205 
206         @ArgRaw
207         ArgParser raw;
208     }
209 
210     alias parser = CommandParser!S;
211     auto result = parser.parse([
212         "abc", 
213         "--abc=1", 
214         "-b", "2", 
215         "-b=3",
216         "-c",
217         "-d false",
218         "-e arg2",
219         "-vv",
220         "-vvvv",
221         "overflow1",
222         "overflow2",
223         "--",
224         "raw 1",
225         "raw 2",
226     ]);
227     assert(result.isOk, result.error);
228     auto value = result.value;
229     auto withoutRaw = value;
230     withoutRaw.raw = ArgParser.init;
231     assert(withoutRaw == S(
232         "abc",
233         "1",
234         "3",
235         true,
236         false,
237         true,
238         "arg2",
239         6,
240         ["overflow1", "overflow2"],
241     ), value.to!string);
242     assert(value.raw.equal(ArgParser(["raw 1", "raw 2"])), value.raw.to!string);
243 }