1 module argparse.help;
2 
3 import argparse;
4 import argparse.internal;
5 
6 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7 /// Help printing functions
8 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9 
10 package enum helpArgument = {
11     ArgumentInfo arg;
12     arg.names = ["h","help"];
13     arg.description = "Show this help message and exit";
14     arg.minValuesCount = 0;
15     arg.maxValuesCount = 0;
16     arg.allowBooleanNegation = false;
17     arg.ignoreInDefaultCommand = true;
18     return arg;
19 }();
20 
21 private bool isHelpArgument(string name)
22 {
23     static foreach(n; helpArgument.names)
24         if(n == name)
25             return true;
26 
27     return false;
28 }
29 
30 unittest
31 {
32     assert(isHelpArgument("h"));
33     assert(isHelpArgument("help"));
34     assert(!isHelpArgument("a"));
35     assert(!isHelpArgument("help1"));
36 }
37 
38 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
39 
40 package string getProgramName()
41 {
42     import core.runtime: Runtime;
43     import std.path: baseName;
44     return Runtime.args[0].baseName;
45 }
46 
47 unittest
48 {
49     assert(getProgramName().length > 0);
50 }
51 
52 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
53 
54 package void substituteProg(Output)(auto ref Output output, string text, string prog)
55 {
56     import std.array: replaceInto;
57     output.replaceInto(text, "%(PROG)", prog);
58 }
59 
60 unittest
61 {
62     import std.array: appender;
63     auto a = appender!string;
64     a.substituteProg("this is some text where %(PROG) is substituted but PROG and prog are not", "-myprog-");
65     assert(a[] == "this is some text where -myprog- is substituted but PROG and prog are not");
66 }
67 
68 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
69 
70 package string spaces(ulong num)
71 {
72     import std.range: repeat;
73     import std.array: array;
74     return ' '.repeat(num).array;
75 }
76 
77 unittest
78 {
79     assert(spaces(0) == "");
80     assert(spaces(1) == " ");
81     assert(spaces(5) == "     ");
82 }
83 
84 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
85 
86 package void wrapMutiLine(Output, S)(auto ref Output output,
87 S s,
88 in size_t columns = 80,
89 S firstindent = null,
90 S indent = null,
91 in size_t tabsize = 8)
92 {
93     import std.string: wrap, lineSplitter, join;
94     import std.algorithm: map, copy;
95 
96     auto lines = s.lineSplitter;
97     if(lines.empty)
98     {
99         output.put(firstindent);
100         output.put("\n");
101         return;
102     }
103 
104     output.put(lines.front.wrap(columns, firstindent, indent, tabsize));
105     lines.popFront;
106 
107     lines.map!(s => s.wrap(columns, indent, indent, tabsize)).copy(output);
108 }
109 
110 unittest
111 {
112     string test(string s, size_t columns, string firstindent = null, string indent = null)
113     {
114         import std.array: appender;
115         auto a = appender!string;
116         a.wrapMutiLine(s, columns, firstindent, indent);
117         return a[];
118     }
119     assert(test("a short string", 7) == "a short\nstring\n");
120     assert(test("a\nshort string", 7) == "a\nshort\nstring\n");
121 
122     // wrap will not break inside of a word, but at the next space
123     assert(test("a short string", 4) == "a\nshort\nstring\n");
124 
125     assert(test("a short string", 7, "\t") == "\ta\nshort\nstring\n");
126     assert(test("a short string", 7, "\t", "    ") == "\ta\n    short\n    string\n");
127 }
128 
129 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
130 
131 private void printValue(Output)(auto ref Output output, in ArgumentInfo info)
132 {
133     if(info.maxValuesCount.get == 0)
134         return;
135 
136     if(info.minValuesCount.get == 0)
137         output.put('[');
138 
139     output.put(info.placeholder);
140     if(info.maxValuesCount.get > 1)
141         output.put(" ...");
142 
143     if(info.minValuesCount.get == 0)
144         output.put(']');
145 }
146 
147 unittest
148 {
149     auto test(int min, int max)
150     {
151         ArgumentInfo info;
152         info.placeholder = "v";
153         info.minValuesCount = min;
154         info.maxValuesCount = max;
155 
156         import std.array: appender;
157         auto a = appender!string;
158         a.printValue(info);
159         return a[];
160     }
161 
162     assert(test(0,0) == "");
163     assert(test(0,1) == "[v]");
164     assert(test(0,5) == "[v ...]");
165     assert(test(1,1) == "v");
166     assert(test(1,5) == "v ...");
167     assert(test(3,3) == "v ...");
168     assert(test(3,5) == "v ...");
169 }
170 
171 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
172 
173 private void printInvocation(Output)(auto ref Output output, in ArgumentInfo info, in string[] names, in Config config)
174 {
175     if(info.positional)
176         output.printValue(info);
177     else
178     {
179         import std.algorithm: each;
180 
181         names.each!((i, name)
182         {
183             if(i > 0)
184                 output.put(", ");
185 
186             output.put(getArgumentName(name, config));
187 
188             if(info.maxValuesCount.get > 0)
189             {
190                 output.put(' ');
191                 output.printValue(info);
192             }
193         });
194     }
195 }
196 
197 unittest
198 {
199     auto test(bool positional)()
200     {
201         enum info = {
202             ArgumentInfo info;
203             info.placeholder = "v";
204             static if (positional)
205                 info.position = 0;
206             return info;
207         }();
208 
209         import std.array: appender;
210         auto a = appender!string;
211         a.printInvocation(info.setDefaults!(int, "foo"), ["f","foo"], Config.init);
212         return a[];
213     }
214 
215     assert(test!false == "-f v, --foo v");
216     assert(test!true == "v");
217 }
218 
219 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
220 
221 private void printUsage(Output)(auto ref Output output, in ArgumentInfo info, in Config config)
222 {
223     if(!info.required)
224         output.put('[');
225 
226     output.printInvocation(info, [info.names[0]], config);
227 
228     if(!info.required)
229         output.put(']');
230 }
231 
232 unittest
233 {
234     auto test(bool required, bool positional)()
235     {
236         enum info = {
237             ArgumentInfo info;
238             info.names ~= "foo";
239             info.placeholder = "v";
240             info.required = required;
241             static if (positional)
242                 info.position = 0;
243             return info;
244         }();
245 
246         import std.array: appender;
247         auto a = appender!string;
248         a.printUsage(info.setDefaults!(int, "foo"), Config.init);
249         return a[];
250     }
251 
252     assert(test!(false, false) == "[--foo v]");
253     assert(test!(false, true) == "[v]");
254     assert(test!(true, false) == "--foo v");
255     assert(test!(true, true) == "v");
256 }
257 
258 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
259 
260 private void printUsage(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config)
261 {
262     import std.algorithm: map;
263     import std.array: join;
264 
265     string progName = (cmd.parentNames ~ cmd.info.names[0]).map!(_ => _.length > 0 ? _ : getProgramName()).join(" ");
266 
267     output.put("Usage: ");
268 
269     if(cmd.info.usage.length > 0)
270         substituteProg(output, cmd.info.usage, progName);
271     else
272     {
273         import std.algorithm: filter, each, map;
274 
275         alias print = (r) => r
276             .filter!((ref _) => !_.hideFromHelp)
277             .each!((ref _)
278             {
279                 output.put(' ');
280                 argparse.help.printUsage(output, _, config);
281             });
282 
283         output.put(progName);
284 
285         // named args
286         print(cmd.arguments.arguments.filter!((ref _) => !_.positional));
287         // positional args
288         print(cmd.arguments.positionalArguments.map!(ref (_) => cmd.arguments.arguments[_]));
289         // sub commands
290         if(cmd.subCommands.length > 0)
291             output.put(" <command> [<args>]");
292     }
293 
294     output.put('\n');
295 }
296 
297 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
298 
299 private void printUsage(T, Output)(auto ref Output output, in Config config)
300 {
301     printUsage(output, CommandArguments!T(config), config);
302 }
303 
304 unittest
305 {
306     @(Command("MYPROG").Usage("custom usage of %(PROG)"))
307     struct T
308     {
309         string s;
310     }
311 
312     auto test(string usage)
313     {
314         import std.array: appender;
315 
316         auto a = appender!string;
317         a.printUsage!T(Config.init);
318         return a[];
319     }
320 
321     enum expected = "Usage: custom usage of MYPROG\n";
322     static assert(test("custom usage of %(PROG)") == expected);
323     assert(test("custom usage of %(PROG)") == expected);
324 }
325 
326 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
327 
328 private void printHelp(Output, ARGS)(auto ref Output output, in Group group, ARGS args, int helpPosition)
329 {
330     import std.string: leftJustify;
331 
332     if(group.arguments.length == 0 || group.name.length == 0)
333         return;
334 
335     alias printDescription = {
336         output.put(group.name);
337         output.put(":\n");
338 
339         if (group.description.length > 0)
340         {
341             output.put("  ");
342             output.put(group.description);
343             output.put("\n\n");
344         }
345     };
346     bool descriptionIsPrinted = false;
347 
348     immutable ident = spaces(helpPosition + 2);
349 
350     foreach(idx; group.arguments)
351     {
352         auto arg = &args[idx];
353 
354         if(arg.invocation.length == 0)
355             continue;
356 
357         if(!descriptionIsPrinted)
358         {
359             printDescription();
360             descriptionIsPrinted = true;
361         }
362 
363         if(arg.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text
364         {
365             import std.array: appender;
366 
367             auto invocation = appender!string;
368             invocation ~= "  ";
369             invocation ~= arg.invocation.leftJustify(helpPosition);
370             output.wrapMutiLine(arg.help, 80-2, invocation[], ident);
371         }
372         else
373         {
374             // long action name; start on the next line
375             output.put("  ");
376             output.put(arg.invocation);
377             output.put("\n");
378             output.wrapMutiLine(arg.help, 80-2, ident, ident);
379         }
380     }
381 
382     output.put('\n');
383 }
384 
385 
386 private void printHelp(Output)(auto ref Output output, in Arguments arguments, in Config config, bool helpArgIsPrinted = false)
387 {
388     import std.algorithm: map, maxElement, min;
389     import std.array: appender, array;
390 
391     // pre-compute the output
392     auto args =
393         arguments.arguments
394         .map!((ref _)
395         {
396             struct Result
397             {
398                 string invocation, help;
399             }
400 
401             if(_.hideFromHelp)
402                 return Result.init;
403 
404             if(isHelpArgument(_.names[0]))
405             {
406                 if(helpArgIsPrinted)
407                     return Result.init;
408 
409                 helpArgIsPrinted = true;
410             }
411 
412             auto invocation = appender!string;
413             invocation.printInvocation(_, _.names, config);
414 
415             return Result(invocation[], _.description);
416         }).array;
417 
418     immutable maxInvocationWidth = args.map!(_ => _.invocation.length).maxElement;
419     immutable helpPosition = min(maxInvocationWidth + 4, 24);
420 
421     //user-defined groups
422     foreach(ref group; arguments.groups[2..$])
423         output.printHelp(group, args, helpPosition);
424 
425     //required args
426     output.printHelp(arguments.requiredGroup, args, helpPosition);
427 
428     //optionals args
429     output.printHelp(arguments.optionalGroup, args, helpPosition);
430 
431     if(arguments.parentArguments)
432         output.printHelp(*arguments.parentArguments, config, helpArgIsPrinted);
433 }
434 
435 private void printHelp(Output)(auto ref Output output, in CommandInfo[] commands, in Config config)
436 {
437     import std.algorithm: map, maxElement, min;
438     import std.array: appender, array, join;
439 
440     if(commands.length == 0)
441         return;
442 
443     output.put("Available commands:\n");
444 
445     // pre-compute the output
446     auto cmds = commands
447         .map!((ref _)
448         {
449             struct Result
450             {
451                 string invocation, help;
452             }
453 
454             //if(_.hideFromHelp)
455             //    return Result.init;
456 
457             return Result(_.names.join(","), _.shortDescription.length > 0 ? _.shortDescription : _.description);
458         }).array;
459 
460     immutable maxInvocationWidth = cmds.map!(_ => _.invocation.length).maxElement;
461     immutable helpPosition = min(maxInvocationWidth + 4, 24);
462 
463 
464     immutable ident = spaces(helpPosition + 2);
465 
466     foreach(const ref cmd; cmds)
467     {
468         if(cmd.invocation.length == 0)
469             continue;
470 
471         if(cmd.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text
472         {
473             import std.array: appender;
474             import std.string: leftJustify;
475 
476             auto invocation = appender!string;
477             invocation ~= "  ";
478             invocation ~= cmd.invocation.leftJustify(helpPosition);
479             output.wrapMutiLine(cmd.help, 80-2, invocation[], ident);
480         }
481         else
482         {
483             // long action name; start on the next line
484             output.put("  ");
485             output.put(cmd.invocation);
486             output.put("\n");
487             output.wrapMutiLine(cmd.help, 80-2, ident, ident);
488         }
489     }
490 
491     output.put('\n');
492 }
493 
494 
495 private void printHelp(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config)
496 {
497     printUsage(output, cmd, config);
498     output.put('\n');
499 
500     if(cmd.info.description.length > 0)
501     {
502         output.put(cmd.info.description);
503         output.put("\n\n");
504     }
505 
506     // sub commands
507     output.printHelp(cmd.subCommands, config);
508 
509     output.printHelp(cmd.arguments, config);
510 
511     if(cmd.info.epilog.length > 0)
512     {
513         output.put(cmd.info.epilog);
514         output.put('\n');
515     }
516 }
517 
518 void printHelp(T, Output)(auto ref Output output, in Config config)
519 {
520     printHelp(output, CommandArguments!T(config), config);
521 }
522 
523 unittest
524 {
525     @(Command("MYPROG")
526      .Description("custom description")
527      .Epilog("custom epilog")
528     )
529     struct T
530     {
531         @NamedArgument  string s;
532         @(NamedArgument.Placeholder("VALUE"))  string p;
533 
534         @(NamedArgument.HideFromHelp())  string hidden;
535 
536         enum Fruit { apple, pear };
537         @(NamedArgument(["f","fruit"]).Required().Description("This is a help text for fruit. Very very very very very very very very very very very very very very very very very very very long text")) Fruit f;
538 
539         @(NamedArgument.AllowedValues!([1,4,16,8])) int i;
540 
541         @(PositionalArgument(0).Description("This is a help text for param0. Very very very very very very very very very very very very very very very very very very very long text")) string param0;
542         @(PositionalArgument(1).AllowedValues!(["q","a"])) string param1;
543 
544         @TrailingArguments string[] args;
545     }
546 
547     auto test(alias func)()
548     {
549         import std.array: appender;
550 
551         auto a = appender!string;
552         func!T(a, Config.init);
553         return a[];
554     }
555     static assert(test!printUsage.length > 0);  // ensure that it works at compile time
556     static assert(test!printHelp .length > 0);  // ensure that it works at compile time
557 
558     assert(test!printUsage == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n");
559     assert(test!printHelp  == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n\n"~
560         "custom description\n\n"~
561         "Required arguments:\n"~
562         "  -f {apple,pear}, --fruit {apple,pear}\n"~
563         "                          This is a help text for fruit. Very very very very\n"~
564         "                          very very very very very very very very very very\n"~
565         "                          very very very very very long text\n"~
566         "  param0                  This is a help text for param0. Very very very very\n"~
567         "                          very very very very very very very very very very\n"~
568         "                          very very very very very long text\n"~
569         "  {q,a}                   \n\n"~
570         "Optional arguments:\n"~
571         "  -s S                    \n"~
572         "  -p VALUE                \n"~
573         "  -i {1,4,16,8}           \n"~
574         "  -h, --help              Show this help message and exit\n\n"~
575         "custom epilog\n");
576 }
577 
578 unittest
579 {
580     @Command("MYPROG")
581     struct T
582     {
583         @(ArgumentGroup("group1").Description("group1 description"))
584         {
585             @NamedArgument
586             {
587                 string a;
588                 string b;
589             }
590             @PositionalArgument(0) string p;
591         }
592 
593         @(ArgumentGroup("group2").Description("group2 description"))
594         @NamedArgument
595         {
596             string c;
597             string d;
598         }
599         @PositionalArgument(1) string q;
600     }
601 
602     auto test(alias func)()
603     {
604         import std.array: appender;
605 
606         auto a = appender!string;
607         func!T(a, Config.init);
608         return a[];
609     }
610 
611     assert(test!printHelp  == "Usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q\n\n"~
612         "group1:\n"~
613         "  group1 description\n\n"~
614         "  -a A          \n"~
615         "  -b B          \n"~
616         "  p             \n\n"~
617         "group2:\n"~
618         "  group2 description\n\n"~
619         "  -c C          \n"~
620         "  -d D          \n\n"~
621         "Required arguments:\n"~
622         "  q             \n\n"~
623         "Optional arguments:\n"~
624         "  -h, --help    Show this help message and exit\n\n");
625 }
626 
627 unittest
628 {
629     import std.sumtype: SumType;
630 
631     @Command("MYPROG")
632     struct T
633     {
634         @(Command("cmd1").ShortDescription("Perform cmd 1"))
635         struct CMD1
636         {
637             string a;
638         }
639         @(Command("very-long-command-name-2").ShortDescription("Perform cmd 2"))
640         struct CMD2
641         {
642             string b;
643         }
644 
645         string c;
646         string d;
647 
648         SumType!(CMD1, CMD2) cmd;
649     }
650 
651     auto test(alias func)()
652     {
653         import std.array: appender;
654 
655         auto a = appender!string;
656         func!T(a, Config.init);
657         return a[];
658     }
659 
660     assert(test!printHelp  == "Usage: MYPROG [-c C] [-d D] [-h] <command> [<args>]\n\n"~
661         "Available commands:\n"~
662         "  cmd1                    Perform cmd 1\n"~
663         "  very-long-command-name-2\n"~
664         "                          Perform cmd 2\n\n"~
665         "Optional arguments:\n"~
666         "  -c C          \n"~
667         "  -d D          \n"~
668         "  -h, --help    Show this help message and exit\n\n");
669 }