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