1 /// Utilities for creating help text.
2 module jaster.cli.helptext;
3 
4 private
5 {
6     import std.typecons : Flag;
7     import jaster.cli.udas;
8 }
9 
10 /// A flag that should be used by content classes that have the ability to auto add argument dashes to arg names.
11 /// (e.g. "-" and "--")
12 alias AutoAddArgDashes = Flag!"addArgDashes";
13 
14 alias ArgIsOptional = Flag!"isOptional";
15 
16 /++
17  + The interface for any class that can be used to generate content inside of a help
18  + text section.
19  + ++/
20 interface IHelpSectionContent
21 {
22     /++
23      + Generates a string to display inside of a help text section.
24      +
25      + Params:
26      +  options = Options regarding how the text should be formatted.
27      +            Please try to match these options as closely as possible.
28      +
29      + Returns:
30      +  The generated content string.
31      + ++/
32     string getContent(const HelpSectionOptions options);
33 
34     // Utility functions
35     protected final
36     {
37         string lineWrap(const HelpSectionOptions options, const(char)[] value)
38         {
39             import jaster.cli.text : lineWrap, LineWrapOptions;
40 
41             return value.lineWrap(LineWrapOptions(options.lineCharLimit, options.linePrefix));
42         }
43     }
44 }
45 
46 /++
47  + A help text section.
48  +
49  + A section is basically something like "Description:", or "Arguments:", etc.
50  +
51  + Notes:
52  +  Instances of this struct aren't ever really supposed to be kept around outside of `HelpTextBuilderTechnical`, so it's
53  +  non-copyable.
54  + ++/
55 struct HelpSection
56 {
57     /// The name of the section.
58     string name;
59 
60     /// The content of this section.
61     IHelpSectionContent[] content;
62 
63     /// The formatting options for this section.
64     HelpSectionOptions options;
65 
66     /++
67      + Adds a new piece of content to this section.
68      +
69      + Params:
70      +  content = The content to add.
71      +
72      + Returns:
73      +  `this`
74      + ++/
75     ref HelpSection addContent(IHelpSectionContent content) return
76     {
77         assert(content !is null);
78         this.content ~= content;
79 
80         return this;
81     }
82 
83     @disable this(this){}
84 }
85 
86 /++
87  + Options on how the text of a section is formatted.
88  +
89  + Notes:
90  +  It is up to the individual `IHelpSectionContent` implementors to follow these options.
91  + ++/
92 struct HelpSectionOptions
93 {
94     /// The prefix to apply to every new line of text inside the section.
95     string linePrefix;
96 
97     /// How many chars there are per line. This should be seen as a hard limit.
98     size_t lineCharLimit = 120;
99 }
100 
101 /++
102  + A class used to create help text, in an object oriented fashion.
103  +
104  + Technical_Versus_Simple:
105  +  The Technical version of this class is meant to give the user full control of what's generated.
106  +
107  +  The Simple version (and any other versions the user may create) are meant to have more of a scope/predefined layout,
108  +  and so are simpler to use.
109  +
110  + Isnt_This_Overcomplicated?:
111  +  Kind of...
112  +
113  +  A goal of this library is to make it's foundational parts (such as this class, and the `ArgBinder` stuff) reusable on their own,
114  +  so even if the user doesn't like how I've designed the core part of the library (Everything in `jaster.cli.core`, and the UDAs relating to it)
115  +  they are given a small foundation to work off of to create their own version.
116  +
117  +  So I kind of need this to be a bit more complicated than it should be, so it's easy for me to provide built-in functionality that can be used
118  +  or not used as wished by the user, while also allowing the user to create their own.
119  + ++/
120 final class HelpTextBuilderTechnical
121 {
122     /// The default options for a section.
123     static const DEFAULT_SECTION_OPTIONS = HelpSectionOptions("    ", 120);
124 
125     private
126     {
127         string[]           _usages;
128         HelpSection[]      _sections;
129         HelpSectionOptions _sectionOptions = DEFAULT_SECTION_OPTIONS;
130     }
131 
132     public final
133     {
134         /// Adds a new usage.
135         void addUsage(string usageText)
136         {
137             this._usages ~= usageText;
138         }
139 
140         /++
141          + Adds a new section.
142          +
143          + Params:
144          +  sectionName = The name of the section.
145          +
146          + Returns:
147          +  A reference to the section, so that the `addContent` function can be called.
148          + ++/
149         ref HelpSection addSection(string sectionName)
150         {
151             this._sections.length += 1;
152             this._sections[$-1].name = sectionName;
153             this._sections[$-1].options = this._sectionOptions;
154 
155             return this._sections[$-1];
156         }
157 
158         ///
159         ref HelpSection getOrAddSection(string sectionName)
160         {
161             // If this is too slow then we can move to an AA
162             // (p.s. std.algorithm doesn't see _sections as an input range, even if I import std.range for the array primitives)
163             foreach(ref section; this._sections)
164             {
165                 if(section.name == sectionName)
166                     return section;
167             }
168 
169             return this.addSection(sectionName);
170         }
171 
172         /++
173          + Modifies an existing section (by returning it by reference).
174          +
175          + Assertions:
176          +  `sectionName` must exist.
177          +
178          + Params:
179          +  sectionName = The name of the section to modify.
180          +
181          + Returns:
182          +  A reference to the section, so it can be modified by calling code.
183          + ++/
184         ref HelpSection modifySection(string sectionName)
185         {
186             foreach(ref section; this._sections)
187             {
188                 if(section.name == sectionName)
189                     return section;
190             }
191 
192             assert(false, "No section called '"~sectionName~"' was found.");
193         }
194 
195         /++
196          + Generates the help text based on the given usages and sections.
197          +
198          + Notes:
199          +  The result of this function aren't cached yet.
200          +
201          + Returns:
202          +  The generated text.
203          + ++/
204         override string toString()
205         {
206             import std.array     : appender;
207             import std.algorithm : map, each, joiner;
208             import std.exception : assumeUnique;
209             import std.format    : format;
210 
211             char[] output;
212             output.reserve(4096);
213 
214             // Usages
215             this._usages.map!(u => "Usage: "~u)
216                         .joiner("\n")
217                         .each!(u => output ~= u);
218 
219             // Sections
220             foreach(ref section; this._sections)
221             {
222                 if(section.content.length == 0)
223                     continue;
224 
225                 // This could all technically be 'D-ified'/'rangeified' but I couldn't make it look nice.
226                 if(output.length > 0)
227                     output ~= "\n\n";
228                     
229                 output ~= section.name~":\n";
230                 section.content.map!(c => c.getContent(section.options))
231                                .joiner("\n")
232                                .each!(c => output ~= c);
233             }
234 
235             return output.assumeUnique;
236         }
237     }
238 }
239 
240 /++
241  + A simpler version of `HelpTextBuilerTechnical`, as it has a fixed layout, and handles all of the section and content generation.
242  +
243  + Description:
244  +  This help text builder contains the following:
245  +
246  +      * A single 'Usage' line, which is generated automatically from the rest of the given data.
247  +      * A description section.
248  +      * A section for positional parameters, which are given a position, description, and an optional display name.
249  +      * A section for named parameters, which can have multiple names, and a description.
250  +
251  + Please see the unittest for an example of its usage and output.
252  + ++/
253 final class HelpTextBuilderSimple
254 {
255     private
256     {
257         alias NamedArg = HelpSectionArgInfoContent.ArgInfo;
258 
259         struct PositionalArg
260         {
261             size_t position;
262             HelpSectionArgInfoContent.ArgInfo info;
263         }
264 
265         struct ArgGroup
266         {
267             string name;
268             string description;
269             NamedArg[] named;
270             PositionalArg[] positional;
271 
272             bool isDefaultGroup()
273             {
274                 return this.name is null;
275             }
276         }
277 
278         struct ArgGroupOrder
279         {
280             string name;
281             int order;
282         }
283 
284         string           _commandName;
285         string           _description;
286         ArgGroup[string] _groups;
287         ArgGroupOrder[]  _groupOrders;
288 
289         ref ArgGroup groupByName(string name)
290         {
291             return this._groups.require(name, () 
292             { 
293                 this._groupOrders ~= ArgGroupOrder(name, cast(int)this._groupOrders.length);
294                 return ArgGroup(name); 
295             }());
296         }
297     }
298 
299     public final
300     {
301         ///
302         HelpTextBuilderSimple addNamedArg(string group, string[] names, string description, ArgIsOptional isOptional)
303         {
304             this.groupByName(group).named ~= HelpSectionArgInfoContent.ArgInfo(names, description, isOptional);
305             return this;
306         }
307 
308         ///
309         HelpTextBuilderSimple addNamedArg(string group, string name, string description, ArgIsOptional isOptional)
310         {
311             return this.addNamedArg(group, [name], description, isOptional);
312         }
313 
314         ///
315         HelpTextBuilderSimple addNamedArg(string[] names, string description, ArgIsOptional isOptional)
316         {
317             return this.addNamedArg(null, names, description, isOptional);
318         }
319 
320         ///
321         HelpTextBuilderSimple addNamedArg(string name, string description, ArgIsOptional isOptional)
322         {
323             return this.addNamedArg(null, name, description, isOptional);
324         }
325 
326         ///
327         HelpTextBuilderSimple addPositionalArg(string group, size_t position, string description, ArgIsOptional isOptional, string displayName = null)
328         {
329             import std.conv : to;
330 
331             this.groupByName(group).positional ~= PositionalArg(
332                 position,
333                 HelpSectionArgInfoContent.ArgInfo(
334                     (displayName is null) ? [] : [displayName],
335                     description,
336                     isOptional
337                 )
338             );
339 
340             return this;
341         }
342 
343         ///
344         HelpTextBuilderSimple addPositionalArg(size_t position, string description, ArgIsOptional isOptional, string displayName = null)
345         {
346             return this.addPositionalArg(null, position, description, isOptional, displayName);
347         }
348 
349         ///
350         HelpTextBuilderSimple setGroupDescription(string group, string description)
351         {
352             this.groupByName(group).description = description;
353             return this;
354         }
355 
356         ///
357         HelpTextBuilderSimple setDescription(string desc)
358         {
359             this.description = desc;
360             return this;
361         }
362 
363         ///
364         HelpTextBuilderSimple setCommandName(string name)
365         {
366             this.commandName = name;
367             return this;
368         }
369 
370         ///
371         @property
372         ref string description()
373         {
374             return this._description;
375         }
376         
377         ///
378         @property
379         ref string commandName()
380         {
381             return this._commandName;
382         }
383 
384         override string toString()
385         {
386             import std.algorithm : map, joiner, sort, filter;
387             import std.array     : array;
388             import std.range     : tee;
389             import std.format    : format;
390             import std.exception : assumeUnique;
391 
392             auto builder = new HelpTextBuilderTechnical();
393 
394             char[] usageString;
395             usageString.reserve(512);
396             usageString ~= this._commandName;
397             usageString ~= ' ';
398 
399             if(this.description !is null)
400             {
401                 builder.addSection("Description")
402                        .addContent(new HelpSectionTextContent(this._description));
403             }
404 
405             void writePositionalArgs(ref HelpSection section, PositionalArg[] args)
406             {
407                 section.addContent(new HelpSectionArgInfoContent(
408                     args.tee!((p)
409                         {
410                             // Using git as a precedant for the angle brackets.
411                             auto name = "%s".format(p.info.names.joiner("/"));
412                             usageString ~= (p.info.isOptional)
413                                             ? "["~name~"]"
414                                             : "<"~name~">";
415                             usageString ~= ' ';
416                         })
417                         .map!(p => p.info)
418                         .array,
419                         AutoAddArgDashes.no
420                     )
421                 );
422             }
423 
424             void writeNamedArgs(Range)(ref HelpSection section, Range args)
425             {
426                 if(args.empty)
427                     return;
428 
429                 section.addContent(new HelpSectionArgInfoContent(
430                     args.tee!((a)
431                         {
432                             auto name = "%s".format(
433                                 a.names
434                                     .map!(n => (n.length == 1) ? "-"~n : "--"~n)
435                                     .joiner("|")
436                             );
437 
438                             usageString ~= (a.isOptional)
439                                             ? "["~name~"]"
440                                             : name;
441                             usageString ~= ' ';
442                         })
443                         .array,
444                         AutoAddArgDashes.yes
445                     )
446                 );
447             }
448 
449             ref HelpSection getGroupSection(ArgGroup group)
450             {
451                 scope section = &builder.getOrAddSection(group.name);
452                 if(section.content.length == 0 && group.description !is null)
453                 {
454                     // Section was just made, so add in the description.
455                     section.addContent(new HelpSectionTextContent(group.description~"\n"));
456                 }
457 
458                 return *section;
459             }
460 
461             // Not overly efficient, but keep in mind in most programs this function will only ever be called once per run (if even that).
462             // The speed is fast enough not to be annoying either.
463             // O(3n) + whatever .sort is.
464             //
465             // The reason we're doing it this way is to ensure everything is shown in this order:
466             //  Positional args
467             //  Required named args
468             //  Optional named args
469             //
470             // Otherwise you get a mess like: Usage: tool.exe complex <file> --iterations|-i [--verbose|-v] <output> --config|-c
471             this._groupOrders.sort!"a.order < b.order"();
472             auto groupsInOrder = this._groupOrders.map!(go => this._groups[go.name]);
473 
474             // Pre-make certain sections
475             builder.addSection("Positional Args");
476             builder.addSection("Named Args");
477 
478             // Pass #1: Write positional args first, since that puts the usage string in the right order.
479             foreach(group; groupsInOrder)
480             {
481                 if(group.positional.length == 0)
482                     continue;
483 
484                 scope section = (group.isDefaultGroup) ? &builder.getOrAddSection("Positional Args") : &getGroupSection(group);
485                 writePositionalArgs(*section, group.positional);
486             }
487 
488             // Pass #2: Write required named args.
489             foreach(group; groupsInOrder)
490             {
491                 if(group.named.length == 0)
492                     continue;
493 
494                 scope section = (group.isDefaultGroup) ? &builder.getOrAddSection("Named Args") : &getGroupSection(group);
495                 writeNamedArgs(*section, group.named.filter!(arg => !arg.isOptional));
496             }
497 
498             // Pass #3: Write optional named args.
499             foreach(group; groupsInOrder)
500             {
501                 if(group.named.length == 0)
502                     continue;
503 
504                 scope section = (group.isDefaultGroup) ? &builder.getOrAddSection("Named Args") : &getGroupSection(group);
505                 writeNamedArgs(*section, group.named.filter!(arg => arg.isOptional));
506             }
507 
508             builder.addUsage(usageString.assumeUnique);
509             return builder.toString();
510         }
511     }
512 }
513 ///
514 unittest
515 {
516     auto builder = new HelpTextBuilderSimple();
517 
518     builder.addPositionalArg(0, "The input file.", ArgIsOptional.no, "InputFile")
519            .addPositionalArg(1, "The output file.", ArgIsOptional.no, "OutputFile")
520            .addPositionalArg("Utility", 2, "How much to compress the file.", ArgIsOptional.no, "CompressionLevel")
521            .addNamedArg(["v","verbose"], "Verbose output", ArgIsOptional.yes)
522            .addNamedArg("Utility", "encoding", "Sets the encoding to use.", ArgIsOptional.yes)
523            .setCommandName("MyCommand")
524            .setDescription("This is a command that transforms the InputFile into an OutputFile")
525            .setGroupDescription("Utility", "Utility arguments used to modify the output.");
526 
527     assert(builder.toString() == 
528         "Usage: MyCommand <InputFile> <OutputFile> <CompressionLevel> [-v|--verbose] [--encoding] \n"
529        ~"\n"
530        ~"Description:\n"
531        ~"    This is a command that transforms the InputFile into an OutputFile\n"
532        ~"\n"
533        ~"Positional Args:\n"
534        ~"    InputFile                    - The input file.\n"
535        ~"    OutputFile                   - The output file.\n"
536        ~"\n"
537        ~"Named Args:\n"
538        ~"    -v,--verbose                 - Verbose output\n"
539        ~"\n"
540        ~"Utility:\n"
541        ~"    Utility arguments used to modify the output.\n"
542        ~"\n"
543        ~"    CompressionLevel             - How much to compress the file.\n"
544        ~"    --encoding                   - Sets the encoding to use.",
545 
546         "\n"~builder.toString()
547     );
548 }
549 
550 /+ BUILT IN SECTION CONTENT +/
551 
552 /++
553  + A simple content class the simply displays a given string.
554  +
555  + Notes:
556  +  This class is fully compliant with the `HelpSectionOptions`.
557  + ++/
558 final class HelpSectionTextContent : IHelpSectionContent
559 {
560     ///
561     string text;
562 
563     ///
564     this(string text)
565     {
566         this.text = text;
567     }
568 
569     string getContent(const HelpSectionOptions options)
570     {
571         return this.lineWrap(options, this.text);
572     }
573 }
574 ///
575 unittest
576 {
577     import std.exception : assertThrown;
578 
579     auto options = HelpSectionOptions("\t", 7 + 2); // '+ 2' is for the prefix + ending new line. 7 is the wanted char limit.
580     auto content = new HelpSectionTextContent("Hey Hip Lell Loll");
581     assert(content.getContent(options) == 
582         "\tHey Hip\n"
583        ~"\tLell Lo\n"
584        ~"\tll",
585     
586         "\n"~content.getContent(options)
587     );
588 
589     options.lineCharLimit = 200;
590     assert(content.getContent(options) == "\tHey Hip Lell Loll");
591 
592     options.lineCharLimit = 2; // Enough for the prefix and ending new line, but not enough for any piece of text.
593     assertThrown(content.getContent(options));
594 
595     options.lineCharLimit = 3; // Useable, but not readable.
596     assert(content.getContent(options) ==
597         "\tH\n"
598        ~"\te\n"
599        ~"\ty\n"
600 
601        ~"\tH\n"
602        ~"\ti\n"
603        ~"\tp\n"
604 
605        ~"\tL\n"
606        ~"\te\n"
607        ~"\tl\n"
608        ~"\tl\n"
609 
610        ~"\tL\n"
611        ~"\to\n"
612        ~"\tl\n"
613        ~"\tl"
614     );
615 }
616 
617 /++
618  + A content class for displaying information about a command line argument.
619  +
620  + Notes:
621  +  Please see this class' unittest to see an example of it's output.
622  + ++/
623 final class HelpSectionArgInfoContent : IHelpSectionContent
624 {
625     /// The divisor used to determine how many characters to use for the arg's name(s)
626     enum NAME_CHAR_LIMIT_DIVIDER = 4;
627 
628     /// The string used to split an arg's name(s) from its description.
629     const MIDDLE_AFFIX = " - ";
630 
631     /// The information about the arg.
632     struct ArgInfo
633     {
634         /// The different names that this arg can be used with (e.g 'v', 'verbose').
635         string[] names;
636 
637         /// The description of the argument.
638         string description;
639 
640         /// Whether the arg is optional or not.
641         ArgIsOptional isOptional;
642     }
643 
644     /// All registered args.
645     ArgInfo[] args;
646 
647     /// Whether to add the dash prefix to the args' name(s). e.g. "--option" vs "option"
648     AutoAddArgDashes addDashes;
649     
650     ///
651     this(ArgInfo[] args, AutoAddArgDashes addDashes)
652     {
653         this.args = args;
654         this.addDashes = addDashes;
655     }
656 
657     string getContent(const HelpSectionOptions options)
658     {
659         import std.array     : array;
660         import std.algorithm : map, reduce, filter, count, max, splitter, substitute;
661         import std.conv      : to;
662         import std.exception : assumeUnique;
663         import std.utf       : byChar;
664 
665         // Calculate some variables.
666         const USEABLE_CHARS     = (options.lineCharLimit - options.linePrefix.length) - MIDDLE_AFFIX.length; // How many chars in total we can use.
667         const NAME_CHARS        = USEABLE_CHARS / NAME_CHAR_LIMIT_DIVIDER;                                   // How many chars per line we can use for the names.
668         const DESCRIPTION_CHARS = USEABLE_CHARS - NAME_CHARS;                                                // How many chars per line we can use for the description.
669         const DESCRIPTION_START = options.linePrefix.length + NAME_CHARS + MIDDLE_AFFIX.length;              // How many chars in that the description starts.
670 
671         // Creating the options and padding for the names and description.
672         HelpSectionOptions nameOptions;
673         nameOptions.linePrefix    = options.linePrefix;
674         nameOptions.lineCharLimit = NAME_CHARS + options.linePrefix.length;
675 
676         auto padding = new char[DESCRIPTION_START];
677         padding[] = ' ';
678 
679         HelpSectionOptions descriptionOptions;
680         descriptionOptions.linePrefix    = padding.assumeUnique;
681         descriptionOptions.lineCharLimit = DESCRIPTION_CHARS + DESCRIPTION_START; // For the first line, the padding needs to be removed manually.
682 
683         char[] output;
684         output.reserve(4096);
685 
686         // Hello inefficient code, my old friend...
687         foreach(arg; this.args)
688         {
689             // Line wrap. (This line alone is like, O(3n), not even mentioning memory usage)
690             auto nameText = lineWrap(
691                 nameOptions, 
692                 arg.names.map!(n => (this.addDashes) 
693                                      ? (n.length == 1) ? "-"~n : "--"~n
694                                      : n
695                          )
696                          .filter!(n => n.length > 0)
697                          .reduce!((a, b) => a~","~b)
698                          .byChar
699                          .array
700             );
701 
702             auto descriptionText = lineWrap(
703                 descriptionOptions,
704                 arg.description
705             );
706 
707             if(descriptionText.length > descriptionOptions.linePrefix.length)
708                 descriptionText = descriptionText[descriptionOptions.linePrefix.length..$]; // Remove the padding from the first line.
709 
710             // Then create our output line-by-line
711             auto nameLines = nameText.splitter('\n');
712             auto descriptionLines = descriptionText.splitter('\n');
713 
714             bool isFirstLine = true;
715             size_t nameLength = 0;
716             while(!nameLines.empty || !descriptionLines.empty)
717             {
718                 if(!nameLines.empty)
719                 {
720                     nameLength = nameLines.front.length; // Need to keep track of this for the next two ifs.
721 
722                     output ~= nameLines.front;
723                     nameLines.popFront();
724                 }
725                 else
726                     nameLength = 0;
727 
728                 if(isFirstLine)
729                 {
730                     // Push the middle affix into the middle, where it should be.
731                     const ptrdiff_t missingChars = (NAME_CHARS - nameLength) + nameOptions.linePrefix.length;
732                     if(missingChars > 0)
733                         output ~= descriptionOptions.linePrefix[0..missingChars];
734 
735                     output ~= MIDDLE_AFFIX;
736                 }
737 
738                 if(!descriptionLines.empty)
739                 {
740                     auto description = descriptionLines.front;
741                     if(!isFirstLine)
742                         description = description[nameLength..$]; // The name might be multi-line, so we need to adjust the padding.
743 
744                     output ~= description;
745                     descriptionLines.popFront();
746                 }
747 
748                 output ~= "\n";
749                 isFirstLine = false; // IMPORTANT for it to be here, please don't move it.
750             }
751         }
752 
753         // Void the last new line, as it provides consistency with HelpSectionTextContent
754         if(output.length > 0 && output[$-1] == '\n')
755             output = output[0..$-1];
756 
757         // debug
758         // {
759         //     char[] a;
760         //     a.length = nameOptions.linePrefix.length;
761         //     a[] = '1';
762         //     output ~= a;
763 
764         //     a.length = NAME_CHARS;
765         //     a[] = '2';
766         //     output ~= a;
767 
768         //     a.length = MIDDLE_AFFIX.length;
769         //     a[] = '3';
770         //     output ~= a;
771 
772         //     a.length = DESCRIPTION_CHARS;
773         //     a[] = '4';
774         //     output ~= a;
775         //     output ~= '\n';
776 
777         //     a.length = DESCRIPTION_START;
778         //     a[] = '>';
779         //     output ~= a;
780         //     output ~= '\n';
781 
782         //     a.length = descriptionOptions.lineCharLimit;
783         //     a[] = '*';
784         //     output ~= a;
785         //     output ~= '\n';
786 
787         //     a.length = options.lineCharLimit;
788         //     a[] = '#';
789         //     output ~= a;
790         // }
791 
792         return output.assumeUnique;
793     }
794 }
795 ///
796 unittest
797 {
798     auto content = new HelpSectionArgInfoContent(
799         [
800             HelpSectionArgInfoContent.ArgInfo(["v", "verbose"],           "Display detailed information about what the program is doing."),
801             HelpSectionArgInfoContent.ArgInfo(["f", "file"],              "The input file."),
802             HelpSectionArgInfoContent.ArgInfo(["super","longer","names"], "Some unusuable command with long names and a long description.")
803         ],
804 
805         AutoAddArgDashes.yes
806     );
807     auto options = HelpSectionOptions(
808         "    ",
809         80
810     );
811 
812     assert(content.getContent(options) ==
813         "    -v,--verbose       - Display detailed information about what the program is\n"
814        ~"                         doing.\n"
815        ~"    -f,--file          - The input file.\n"
816        ~"    --super,--longer,  - Some unusuable command with long names and a long desc\n"
817        ~"    --names              ription.",
818 
819         "\n"~content.getContent(options)
820     );
821 }