1 /**
2  * Argument parsing functionality.
3  *
4  * See_Also:
5  *  parse, parseArgs
6  */
7 module commandr.parser;
8 
9 import commandr.program;
10 import commandr.option;
11 import commandr.args;
12 import commandr.help;
13 import commandr.utils;
14 
15 import std.algorithm : canFind, count, each;
16 import std.stdio : writeln, writefln, stderr;
17 import std..string : startsWith, indexOf, format;
18 import std.range : empty;
19 import std.typecons : Tuple;
20 private import core.stdc.stdlib;
21 
22 
23 /**
24  * Parses program arguments.
25  *
26  * Returns instance of `ProgramArgs`, which allows working on parsed data.
27  *
28  * On top of parsing arguments, this function catches `InvalidArgumentsException`,
29  * handles `--version` and `--help` flags as well as `help` subcommand.
30  * Exception is handled by printing out the error message along with program usage information
31  * and exiting.
32  *
33  * Version and help is handled by prining out information and exiting.
34  *
35  * If you want to only parse argument without additional handling, see `parseArgs`.
36  *
37  * Similarly to `parseArgs`, `args` array is taken by reference, after call it points to first
38  * not-parsed argument (after `--`).
39  *
40  * See_Also:
41  *   `parseArgs`
42  */
43 public ProgramArgs parse(Program program, ref string[] args, HelpOutput helpConfig = HelpOutput.init) {
44     try {
45         auto res = parseArgs(program, args, helpConfig);
46 
47         if (res.flag("version")) {
48             writeln(program.version_);
49             exit(0);
50             assert(0);
51         }
52 
53         return res;
54     } catch(InvalidArgumentsException e) {
55         stderr.writeln("Error: ", e.msg);
56         program.printUsage(helpConfig);
57         exit(0);
58         assert(0);
59     }
60 }
61 
62 
63 /**
64  * Parses args.
65  *
66  * Returns instance of `ProgramArgs`, which allows working on parsed data.
67  *
68  * Program model by default adds version flag and help flags and subcommand which need to be
69  * handled by the caller. If you want to have the default behavior, use `parse` which handles
70  * above flags.
71  *
72  * `args` array is taken by reference, after call it points to first not-parsed argument (after `--`).
73  *
74  * Throws:
75  *   InvalidArgumentException
76  *
77  * See_Also:
78  *   `parse`
79  */
80 public ProgramArgs parseArgs(Program program, ref string[] args, HelpOutput helpConfig = HelpOutput.init) {
81     args = args[1..$];
82     return program.parseArgs(args, new ProgramArgs(), helpConfig);
83 }
84 
85 private ProgramArgs parseArgs(
86     Command program,
87     ref string[] args,
88     ProgramArgs init,
89     HelpOutput helpConfig = HelpOutput.init
90 ) {
91     // TODO: Split
92     ProgramArgs result = init;
93     result.name = program.name;
94     size_t argIndex = 0;
95 
96     while (args.length) {
97         string arg = args[0];
98         args = args[1..$];
99         immutable bool hasNext = args.length > 0;
100 
101         // end of args
102         if (arg == "--") {
103             break;
104         }
105         // option/flag
106         else if (arg.startsWith("-")) {
107             immutable bool isLong = arg.startsWith("--");
108             auto raw = parseRawOption(arg[1 + isLong..$]);
109 
110             // try matching flag, then fallback to option
111             auto flag = isLong ? program.getFlagByFull(raw.name) : program.getFlagByShort(raw.name);
112             int flagValue = 1;
113 
114             // repeating (-vvvv)
115             if (!isLong && flag.isNull && raw.name.length > 1) {
116                 char letter = raw.name[0];
117                 // all same character
118                 if (!raw.name.canFind!(l => l != letter)) {
119                     flagValue = cast(int)raw.name.length;
120                     raw.name = raw.name[0..1];
121                     flag = program.getFlagByShort(raw.name);
122                 }
123             }
124 
125             // flag exists, has value
126             if (!flag.isNull && raw.value != null) {
127                 throw new InvalidArgumentsException("-%s is a flag, and cannot accept value".format(raw.name));
128             }
129             // just exists
130             else if (!flag.isNull) {
131                 auto flagName = flag.get().name;
132                 result._flags.setOrIncrease(flagName, flagValue);
133 
134                 if (result._flags[flagName] > 1 && !flag.get().isRepeating) {
135                     throw new InvalidArgumentsException("flag -%s cannot be repeated".format(raw.name));
136                 }
137 
138                 continue;
139             }
140 
141             // trying to match option
142             auto option = isLong ? program.getOptionByFull(raw.name) : program.getOptionByShort(raw.name);
143             if (option.isNull) {
144                 string suggestion = (isLong ? program.fullNames : program.abbrevations).matchingCandidate(raw.name);
145 
146                 if (suggestion) {
147                     throw new InvalidArgumentsException(
148                         "unknown flag/option %s, did you mean %s%s?".format(arg, isLong ? "--" : "-", suggestion)
149                     );
150                 }
151                 else {
152                     throw new InvalidArgumentsException("unknown flag/option %s".format(arg));
153                 }
154             }
155 
156             // no value
157             if (raw.value is null) {
158                 if (!hasNext) {
159                     throw new InvalidArgumentsException(
160                         "option %s%s is missing value".format(isLong ? "--" : "-", raw.name)
161                     );
162                 }
163                 auto next = args[0];
164                 args = args[1..$];
165                 if (next.startsWith("-")) {
166                     throw new InvalidArgumentsException(
167                         "option %s%s is missing value (if value starts with \'-\' character, prefix it with '\\')"
168                         .format(isLong ? "--" : "-", raw.name)
169                     );
170                 }
171                 raw.value = next;
172             }
173             result._options.setOrAppend(option.get().name, raw.value);
174         }
175         // argument
176         else if (argIndex < program.arguments.length) {
177             Argument argument = program.arguments[argIndex];
178             if (!argument.isRepeating) {
179                 argIndex += 1;
180             }
181             result._args.setOrAppend(argument.name, arg);
182         }
183         // command
184         else {
185             if (program.commands.length == 0) {
186                 throw new InvalidArgumentsException("unknown (excessive) parameter %s".format(arg));
187             }
188             else if ((arg in program.commands) is null) {
189                 string suggestion = program.commands.keys.matchingCandidate(arg);
190                 throw new InvalidArgumentsException("unknown command %s, did you mean %s?".format(arg, suggestion));
191             }
192             else {
193                 result._command = program.commands[arg].parseArgs(args, result.copy());
194                 result._command._parent = result;
195                 break;
196             }
197         }
198     }
199 
200     if (result.flag("help")) {
201         program.printHelp(helpConfig);
202         exit(0);
203     }
204 
205     // fill defaults (before required)
206     foreach(option; program.options) {
207         if (result.option(option.name) is null && option.defaultValue) {
208             result._options[option.name] = option.defaultValue;
209         }
210     }
211 
212     foreach(arg; program.arguments) {
213         if (result.arg(arg.name) is null && arg.defaultValue) {
214             result._args[arg.name] = arg.defaultValue;
215         }
216     }
217 
218     // post-process options: check required opts, illegal repetitions and validate
219     foreach (option; program.options) {
220         if (option.isRequired && result.option(option.name) is null) {
221             throw new InvalidArgumentsException("missing required option %s".format(option.name));
222         }
223 
224         if (!option.isRepeating && result.options(option.name, []).length > 1) {
225             throw new InvalidArgumentsException("expected only one value for option %s".format(option.name));
226         }
227 
228         if (option.validators.empty) {
229             continue;
230         }
231 
232         auto values = result.options(option.name);
233         foreach (validator; option.validators)  {
234             validator.validate(option, values);
235         }
236     }
237 
238     // check required args & illegal repetitions
239     foreach (arg; program.arguments) {
240         if (arg.isRequired && result.arg(arg.name) is null) {
241             throw new InvalidArgumentsException("missing required argument %s".format(arg.name));
242         }
243 
244         if (arg.validators.empty) {
245             continue;
246         }
247 
248         auto values = result.args(arg.name);
249         foreach (validator; arg.validators)  {
250             validator.validate(arg, values);
251         }
252     }
253 
254     if (result.command is null && program.commands.length > 0) {
255         if (program.defaultCommand !is null) {
256             result._command = program.commands[program.defaultCommand].parseArgs(args, result.copy());
257             result._command._parent = result;
258         }
259         else {
260             throw new InvalidArgumentsException("missing required subcommand");
261         }
262     }
263 
264     return result;
265 }
266 
267 package ProgramArgs parseArgsNoRef(Program p, string[] args) {
268     return p.parseArgs(args);
269 }
270 
271 // internal type for holding name-value pair
272 private alias RawOption = Tuple!(string, "name", string, "value");
273 
274 
275 /*
276  * Splits --option=value into a pair of strings on match, otherwise
277  * returns a tuple with option name and null.
278  */
279 private RawOption parseRawOption(string argument) {
280     RawOption result;
281 
282     auto index = argument.indexOf("=");
283     if (index > 0) {
284         result.name = argument[0..index];
285         result.value = argument[index+1..$];
286     }
287     else {
288         result.name = argument;
289         result.value = null;
290     }
291 
292     return result;
293 }
294 
295 private void setOrAppend(T)(ref T[][string] array, string name, T value) {
296     if (name in array) {
297         array[name] ~= value;
298     } else {
299         array[name] = [value];
300     }
301 }
302 
303 private void setOrIncrease(ref int[string] array, string name, int value) {
304     if (name in array) {
305         array[name] += value;
306     } else {
307         array[name] = value;
308     }
309 }
310 
311 unittest {
312     import std.exception : assertThrown, assertNotThrown;
313 
314     assertNotThrown!InvalidArgumentsException(
315         new Program("test").parseArgsNoRef(["test"])
316     );
317 }
318 
319 // flags
320 unittest {
321     import std.exception : assertThrown, assertNotThrown;
322 
323     ProgramArgs a;
324 
325     a = new Program("test")
326             .add(new Flag("t", "test", ""))
327             .parseArgsNoRef(["test"]);
328     assert(!a.flag("test"));
329     assert(a.option("test") is null);
330     assert(a.occurencesOf("test") == 0);
331 
332     a = new Program("test")
333             .add(new Flag("t", "test", ""))
334             .parseArgsNoRef(["test", "-t"]);
335     assert(a.flag("test"));
336     assert(a.option("test") is null);
337     assert(a.occurencesOf("test") == 1);
338 
339     a = new Program("test")
340             .add(new Flag("t", "test", ""))
341             .parseArgsNoRef(["test", "--test"]);
342     assert(a.flag("test"));
343     assert(a.occurencesOf("test") == 1);
344 
345     assertThrown!InvalidArgumentsException(
346         new Program("test")
347             .add(new Flag("t", "test", "")) // no repeating
348             .parseArgsNoRef(["test", "--test", "-t"])
349     );
350 
351     assertThrown!InvalidArgumentsException(
352         new Program("test")
353             .add(new Flag("t", "test", "")) // no repeating
354             .parseArgsNoRef(["test", "-tt"])
355     );
356 
357     assertThrown!InvalidArgumentsException(
358         new Program("test")
359             .add(new Flag("t", "test", "")) // no repeating
360             .parseArgsNoRef(["test", "--tt"])
361     );
362 }
363 
364 // options
365 unittest {
366     import std.exception : assertThrown, assertNotThrown;
367     import std.range : empty;
368 
369     ProgramArgs a;
370 
371     a = new Program("test")
372             .add(new Option("t", "test", ""))
373             .parseArgsNoRef(["test"]);
374     assert(a.option("test") is null);
375     assert(a.occurencesOf("test") == 0);
376 
377     a = new Program("test")
378             .add(new Option("t", "test", ""))
379             .parseArgsNoRef(["test", "-t", "5"]);
380     assert(a.option("test") == "5");
381     assert(a.occurencesOf("test") == 0);
382 
383     a = new Program("test")
384             .add(new Option("t", "test", ""))
385             .parseArgsNoRef(["test", "-t=5"]);
386     assert(a.option("test") == "5");
387     assert(a.occurencesOf("test") == 0);
388 
389     a = new Program("test")
390             .add(new Option("t", "test", ""))
391             .parseArgsNoRef(["test", "--test", "bar"]);
392     assert(a.option("test") == "bar");
393     assert(a.occurencesOf("test") == 0);
394 
395     a = new Program("test")
396             .add(new Option("t", "test", ""))
397             .parseArgsNoRef(["test", "--test=bar"]);
398     assert(a.option("test") == "bar");
399     assert(a.occurencesOf("test") == 0);
400 
401     assertThrown!InvalidArgumentsException(
402         new Program("test")
403             .add(new Option("t", "test", ""))
404             .parseArgsNoRef(["test", "--test=a", "-t", "k"])
405     );
406 
407     assertThrown!InvalidArgumentsException(
408         new Program("test")
409             .add(new Option("t", "test", "")) // no repeating
410             .parseArgsNoRef(["test", "--test", "-t"])
411     );
412 
413     assertThrown!InvalidArgumentsException(
414         new Program("test")
415             .add(new Option("t", "test", "")) // no value
416             .parseArgsNoRef(["test", "--test"])
417     );
418 }
419 
420 // arguments
421 unittest {
422     import std.exception : assertThrown, assertNotThrown;
423     import std.range : empty;
424 
425     ProgramArgs a;
426 
427     a = new Program("test")
428             .add(new Argument("test", "").optional)
429             .parseArgsNoRef(["test"]);
430     assert(a.occurencesOf("test") == 0);
431 
432     a = new Program("test")
433             .add(new Argument("test", ""))
434             .parseArgsNoRef(["test", "t"]);
435     assert(a.occurencesOf("test") == 0);
436     assert(a.arg("test") == "t");
437 
438     assertThrown!InvalidArgumentsException(
439         new Program("test")
440             .parseArgsNoRef(["test", "test", "t"])
441     );
442 
443     assertThrown!InvalidArgumentsException(
444         new Program("test")
445             .add(new Argument("test", "")) // no value
446             .parseArgsNoRef(["test", "test", "test"])
447     );
448 }
449 
450 // required
451 unittest {
452     import std.exception : assertThrown, assertNotThrown;
453 
454     assertThrown!InvalidArgumentsException(
455         new Program("test")
456             .add(new Option("t", "test", "").required)
457             .parseArgsNoRef(["test"])
458     );
459 
460     assertThrown!InvalidArgumentsException(
461         new Program("test")
462             .add(new Option("t", "test", ""))
463             .add(new Argument("path", "").required)
464             .parseArgsNoRef(["test", "--test", "bar"])
465     );
466 
467     assertThrown!InvalidArgumentsException(
468         new Program("test")
469             .add(new Argument("test", "").required) // no value
470             .parseArgsNoRef(["test"])
471     );
472 }
473 
474 // repating
475 unittest {
476     ProgramArgs a;
477 
478     a = new Program("test")
479             .add(new Flag("t", "test", "").repeating)
480             .parseArgsNoRef(["test", "--test", "-t"]);
481     assert(a.flag("test"));
482     assert(a.occurencesOf("test") == 2);
483 
484     a = new Program("test")
485             .add(new Option("t", "test", "").repeating)
486             .parseArgsNoRef(["test", "--test=a", "-t", "k"]);
487     assert(a.option("test") == "k");
488     assert(a.optionAll("test") == ["a", "k"]);
489     assert(a.occurencesOf("test") == 0);
490 }
491 
492 // default value
493 unittest {
494     ProgramArgs a;
495 
496     a = new Program("test")
497             .add(new Option("t", "test", "")
498                 .defaultValue("reee"))
499             .parseArgsNoRef(["test"]);
500     assert(a.option("test") == "reee");
501 
502     a = new Program("test")
503             .add(new Option("t", "test", "")
504                 .defaultValue("reee"))
505             .parseArgsNoRef(["test", "--test", "aaa"]);
506     assert(a.option("test") == "aaa");
507 
508     a = new Program("test")
509             .add(new Argument("test", "")
510                 .optional
511                 .defaultValue("reee"))
512             .parseArgsNoRef(["test"]);
513     assert(a.arg("test") == "reee");
514 
515     a = new Program("test")
516             .add(new Argument("test", "")
517                 .optional
518                 .defaultValue("reee"))
519             .parseArgsNoRef(["test", "bar"]);
520     assert(a.args("test") == ["bar"]);
521 }
522 
523 // rest
524 unittest {
525     ProgramArgs a;
526     auto args = ["test", "--", "bar"];
527     a = new Program("test")
528             .add(new Argument("test", "")
529                 .optional
530                 .defaultValue("reee"))
531             .parseArgs(args);
532     assert(a.args("test") == ["reee"]);
533     assert(args == ["bar"]);
534 }
535 
536 // subcommands
537 unittest {
538     import std.exception : assertThrown;
539 
540     assertThrown!InvalidArgumentsException(
541         new Program("test")
542             .add(new Argument("test", ""))
543             .add(new Command("a"))
544             .add(new Command("b")
545                 .add(new Command("c")))
546             .parseArgsNoRef(["test", "cccc"])
547     );
548 
549     assertThrown!InvalidArgumentsException(
550         new Program("test")
551             .add(new Argument("test", ""))
552             .add(new Command("a"))
553             .add(new Command("b")
554                 .add(new Command("c")))
555             .parseArgsNoRef(["test", "cccc", "a", "c"])
556     );
557 
558     ProgramArgs a;
559     a = new Program("test")
560             .add(new Argument("test", ""))
561             .add(new Command("a"))
562             .add(new Command("b")
563                 .add(new Command("c")))
564             .parseArgsNoRef(["test", "cccc", "b", "c"]);
565     assert(a.args("test") == ["cccc"]);
566     assert(a.command !is null);
567     assert(a.command.name == "b");
568     assert(a.command.command !is null);
569     assert(a.command.command.name == "c");
570 
571 
572     auto args = ["test", "cccc", "a", "--", "c"];
573     a = new Program("test")
574             .add(new Argument("test", ""))
575             .add(new Command("a"))
576             .add(new Command("b")
577                 .add(new Command("c")))
578             .parseArgs(args);
579     assert(a.args("test") == ["cccc"]);
580     assert(a.command !is null);
581     assert(a.command.name == "a");
582     assert(args == ["c"]);
583 
584     assertThrown!InvalidArgumentsException(
585         new Program("test")
586             .add(new Argument("test", ""))
587             .add(new Command("a"))
588             .add(new Command("b")
589                 .add(new Command("c")))
590             .parseArgsNoRef(["test", "cccc", "b", "--", "c"])
591     );
592 }
593 
594 // default subcommand
595 unittest {
596     ProgramArgs a;
597 
598     a = new Program("test")
599             .add(new Command("a"))
600             .add(new Command("b")
601                 .add(new Command("c")))
602             .defaultCommand("a")
603             .parseArgsNoRef(["test"]);
604 
605     assert(a.command !is null);
606     assert(a.command.name == "a");
607 
608 
609     a = new Program("test")
610             .add(new Command("a"))
611             .add(new Command("b")
612                 .add(new Command("c"))
613                 .defaultCommand("c"))
614             .defaultCommand("b")
615             .parseArgsNoRef(["test"]);
616 
617     assert(a.command !is null);
618     assert(a.command.name == "b");
619     assert(a.command.command !is null);
620     assert(a.command.command.name == "c");
621 }