1 module argparse.parser;
2 
3 import argparse;
4 import argparse.internal;
5 
6 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7 
8 package struct Parser
9 {
10     import std.sumtype: SumType;
11 
12     struct Unknown {}
13     struct EndOfArgs {}
14     struct Positional {}
15     struct NamedShort {
16         string name;
17         string nameWithDash;
18         string value = null;  // null when there is no value
19     }
20     struct NamedLong {
21         string name;
22         string nameWithDash;
23         string value = null;  // null when there is no value
24     }
25 
26     alias Argument = SumType!(Unknown, EndOfArgs, Positional, NamedShort, NamedLong);
27 
28     immutable Config config;
29 
30     string[] args;
31     string[] unrecognizedArgs;
32 
33     bool[size_t] idxParsedArgs;
34     size_t idxNextPositional = 0;
35 
36     struct CmdParser
37     {
38         Result delegate(const ref Argument) parse;
39         Result delegate(const ref Argument) complete;
40         const(string)[] completeSuggestion;
41         bool isDefault;
42     }
43     CmdParser[] cmdStack;
44 
45     Argument splitArgumentNameValue(string arg)
46     {
47         import std.string : indexOf;
48 
49         if(arg.length == 0)
50             return Argument.init;
51 
52         if(arg == config.endOfArgs)
53             return Argument(EndOfArgs.init);
54 
55         if(arg[0] != config.namedArgChar)
56             return Argument(Positional.init);
57 
58         if(arg.length == 1 || arg.length == 2 && arg[1] == config.namedArgChar)
59             return Argument.init;
60 
61         auto idxAssignChar = config.assignChar == char.init ? -1 : arg.indexOf(config.assignChar);
62 
63         immutable string nameWithDash = idxAssignChar < 0 ? arg  : arg[0 .. idxAssignChar];
64         immutable string value        = idxAssignChar < 0 ? null : arg[idxAssignChar + 1 .. $];
65 
66         return arg[1] == config.namedArgChar
67         ? Argument(NamedLong (nameWithDash[2..$], nameWithDash, value))
68         : Argument(NamedShort(nameWithDash[1..$], nameWithDash, value));
69     }
70 
71     auto parseArgument(T, PARSE)(PARSE parse, ref T receiver, string value, string nameWithDash, size_t argIndex)
72     {
73         auto res = parse(config, nameWithDash, receiver, value, args);
74         if(!res)
75             return res;
76 
77         idxParsedArgs[argIndex] = true;
78 
79         return Result.Success;
80     }
81 
82     auto parseSubCommand(T)(const ref CommandArguments!T cmd, ref T receiver)
83     {
84         import std.range: front, popFront;
85 
86         auto found = cmd.findSubCommand(args.front);
87         if(found.parse is null)
88             return Result.UnknownArgument;
89 
90         if(found.level < cmdStack.length)
91             cmdStack.length = found.level;
92 
93         cmdStack ~= CmdParser((const ref arg) => found.parse(config, this, arg, false, receiver), (const ref arg) => found.complete(config, this, arg, false, receiver));
94 
95         found.initialize(receiver);
96         args.popFront();
97 
98         return Result.Success;
99     }
100 
101     auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Unknown)
102     {
103         static if(completionMode)
104         {
105             import std.range: front, popFront;
106             import std.algorithm: filter;
107             import std.string:startsWith;
108             import std.array:array;
109             if(args.length == 1)
110             {
111                 // last arg means we need to provide args and subcommands
112                 auto A = args[0] == "" ? cmd.completeSuggestion : cmd.completeSuggestion.filter!(_ => _.startsWith(args[0])).array;
113                 return Result(0, Result.Status.success, "", A);
114             }
115         }
116 
117         return Result.UnknownArgument;
118     }
119 
120     auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, EndOfArgs)
121     {
122         static if(!completionMode)
123         {
124             import std.range: popFront;
125 
126             args.popFront();
127 
128             cmd.setTrailingArgs(receiver, args);
129             unrecognizedArgs ~= args;
130         }
131 
132         args = [];
133 
134         return Result.Success;
135     }
136 
137     auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Positional)
138     {
139         auto foundArg = cmd.findPositionalArgument(idxNextPositional);
140         if(foundArg.arg is null)
141             return parseSubCommand(cmd, receiver);
142 
143         auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, null, foundArg.arg.names[0], foundArg.index);
144         if(!res)
145             return res;
146 
147         idxNextPositional++;
148 
149         return Result.Success;
150     }
151 
152     auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, NamedLong arg)
153     {
154         import std.algorithm : startsWith;
155         import std.range: popFront;
156 
157         auto foundArg = cmd.findNamedArgument(arg.name);
158 
159         if(foundArg.arg is null && arg.name.startsWith("no-"))
160         {
161             foundArg = cmd.findNamedArgument(arg.name[3..$]);
162             if(foundArg.arg is null || !foundArg.arg.allowBooleanNegation)
163                 return Result.UnknownArgument;
164 
165             arg.value = "false";
166         }
167 
168         if(foundArg.arg is null)
169             return Result.UnknownArgument;
170 
171         if(isDefaultCmd && foundArg.arg.ignoreInDefaultCommand)
172             return Result.UnknownArgument;
173 
174         args.popFront();
175         return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index);
176     }
177 
178     auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, NamedShort arg)
179     {
180         import std.range: popFront;
181 
182         auto foundArg = cmd.findNamedArgument(arg.name);
183         if(foundArg.arg !is null)
184         {
185             if(isDefaultCmd && foundArg.arg.ignoreInDefaultCommand)
186                 return Result.UnknownArgument;
187 
188             args.popFront();
189             return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index);
190         }
191 
192         // Try to parse "-ABC..." where "A","B","C" are different single-letter arguments
193         do
194         {
195             auto name = [arg.name[0]];
196             foundArg = cmd.findNamedArgument(name);
197             if(foundArg.arg is null)
198                 return Result.UnknownArgument;
199 
200             // In case of bundling there can be no or one argument value
201             if(config.bundling && foundArg.arg.minValuesCount.get > 1)
202                 return Result.UnknownArgument;
203 
204             // In case of NO bundling there MUST be one argument value
205             if(!config.bundling && foundArg.arg.minValuesCount.get != 1)
206                 return Result.UnknownArgument;
207 
208             string value;
209             if(foundArg.arg.minValuesCount == 0)
210                 arg.name = arg.name[1..$];
211             else
212             {
213                 // Bundling case: try to parse "-ABvalue" where "A","B" are different single-letter arguments and "value" is a value for "B"
214                 // No bundling case: try to parse "-Avalue" where "A" is a single-letter argument and "value" is its value
215                 value = arg.name[1..$];
216                 arg.name = "";
217             }
218 
219             auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, value, "-"~name, foundArg.index);
220             if(!res)
221                 return res;
222         }
223         while(arg.name.length > 0);
224 
225         args.popFront();
226         return Result.Success;
227     }
228 
229     auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Argument arg)
230     {
231         import std.sumtype: match;
232 
233         return arg.match!(_ => parse!completionMode(cmd, isDefaultCmd, receiver, _));
234     }
235 
236     auto parse(bool completionMode)(Argument arg)
237     {
238         import std.range: front, popFront;
239 
240         auto result = Result.Success;
241 
242         const argsCount = args.length;
243 
244         foreach_reverse(cmdParser; cmdStack)
245         {
246             static if(completionMode)
247             {
248                 auto res = cmdParser.complete(arg);
249                 if(res)
250                     result.suggestions ~= res.suggestions;
251             }
252             else
253             {
254                 auto res = cmdParser.parse(arg);
255 
256                 if(res.status != Result.Status.unknownArgument)
257                     return res;
258             }
259         }
260 
261         if(args.length > 0 && argsCount == args.length)
262         {
263             unrecognizedArgs ~= args.front;
264             args.popFront();
265         }
266 
267         return result;
268     }
269 
270     auto parseAll(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver)
271     {
272         import std.range: empty, front;
273 
274         cmdStack ~= CmdParser(
275         (const ref arg)
276         {
277             return parse!completionMode(cmd, false, receiver, arg);
278         },
279         (const ref arg)
280         {
281             return parse!completionMode(cmd, false, receiver, arg);
282         },
283         cmd.completeSuggestion);
284 
285         auto found = cmd.findSubCommand(DEFAULT_COMMAND);
286         if(found.parse !is null)
287         {
288             auto p = CmdParser((const ref arg) => found.parse(config, this, arg, true, receiver));
289             p.isDefault = true;
290             cmdStack ~= p;
291             found.initialize(receiver);
292         }
293 
294         while(!args.empty)
295         {
296             static if(completionMode)
297                 auto res = parse!completionMode(args.length > 1 ? splitArgumentNameValue(args.front) : Argument.init);
298             else
299                 auto res = parse!completionMode(splitArgumentNameValue(args.front));
300             if(!res)
301                 return res;
302 
303             static if(completionMode)
304                 if(args.empty)
305                     return res;
306         }
307 
308         return cmd.checkRestrictions(idxParsedArgs, config);
309     }
310 }
311 
312 unittest
313 {
314     assert(Parser.init.splitArgumentNameValue("") == Parser.Argument(Parser.Unknown.init));
315     assert(Parser.init.splitArgumentNameValue("-") == Parser.Argument(Parser.Unknown.init));
316     assert(Parser.init.splitArgumentNameValue("--") == Parser.Argument(Parser.EndOfArgs.init));
317     assert(Parser.init.splitArgumentNameValue("abc=4") == Parser.Argument(Parser.Positional.init));
318     assert(Parser.init.splitArgumentNameValue("-abc") == Parser.Argument(Parser.NamedShort("abc", "-abc", null)));
319     assert(Parser.init.splitArgumentNameValue("--abc") == Parser.Argument(Parser.NamedLong("abc", "--abc", null)));
320     assert(Parser.init.splitArgumentNameValue("-abc=fd") == Parser.Argument(Parser.NamedShort("abc", "-abc", "fd")));
321     assert(Parser.init.splitArgumentNameValue("--abc=fd") == Parser.Argument(Parser.NamedLong("abc", "--abc", "fd")));
322     assert(Parser.init.splitArgumentNameValue("-abc=") == Parser.Argument(Parser.NamedShort("abc", "-abc", "")));
323     assert(Parser.init.splitArgumentNameValue("--abc=") == Parser.Argument(Parser.NamedLong("abc", "--abc", "")));
324     assert(Parser.init.splitArgumentNameValue("-=abc") == Parser.Argument(Parser.NamedShort("", "-", "abc")));
325     assert(Parser.init.splitArgumentNameValue("--=abc") == Parser.Argument(Parser.NamedLong("", "--", "abc")));
326 }
327 
328 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
329 
330 package static Result callParser(Config config, bool completionMode, COMMAND)(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs)
331 {
332     auto parser = Parser(config, args);
333 
334     auto command = CommandArguments!COMMAND(config);
335     auto res = parser.parseAll!completionMode(command, receiver);
336 
337     static if(!completionMode)
338     {
339         if(res)
340             unrecognizedArgs = parser.unrecognizedArgs;
341         else if(res.errorMsg.length > 0)
342             config.onError(res.errorMsg);
343     }
344 
345     return res;
346 }
347 
348 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////