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                 scope section = (group.isDefaultGroup) ? &builder.getOrAddSection("Positional Args") : &getGroupSection(group);
482                 writePositionalArgs(*section, group.positional);
483             }
484 
485             // Pass #2: Write required named args.
486             foreach(group; groupsInOrder)
487             {
488                 scope section = (group.isDefaultGroup) ? &builder.getOrAddSection("Named Args") : &getGroupSection(group);
489                 writeNamedArgs(*section, group.named.filter!(arg => !arg.isOptional));
490             }
491 
492             // Pass #3: Write optional named args.
493             foreach(group; groupsInOrder)
494             {
495                 scope section = (group.isDefaultGroup) ? &builder.getOrAddSection("Named Args") : &getGroupSection(group);
496                 writeNamedArgs(*section, group.named.filter!(arg => arg.isOptional));
497             }
498 
499             builder.addUsage(usageString.assumeUnique);
500             return builder.toString();
501         }
502     }
503 }
504 ///
505 unittest
506 {
507     auto builder = new HelpTextBuilderSimple();
508 
509     builder.addPositionalArg(0, "The input file.", ArgIsOptional.no, "InputFile")
510            .addPositionalArg(1, "The output file.", ArgIsOptional.no, "OutputFile")
511            .addPositionalArg("Utility", 2, "How much to compress the file.", ArgIsOptional.no, "CompressionLevel")
512            .addNamedArg(["v","verbose"], "Verbose output", ArgIsOptional.yes)
513            .addNamedArg("Utility", "encoding", "Sets the encoding to use.", ArgIsOptional.yes)
514            .setCommandName("MyCommand")
515            .setDescription("This is a command that transforms the InputFile into an OutputFile")
516            .setGroupDescription("Utility", "Utility arguments used to modify the output.");
517 
518     assert(builder.toString() == 
519         "Usage: MyCommand <InputFile> <OutputFile> <CompressionLevel> [-v|--verbose] [--encoding] \n"
520        ~"\n"
521        ~"Description:\n"
522        ~"    This is a command that transforms the InputFile into an OutputFile\n"
523        ~"\n"
524        ~"Positional Args:\n"
525        ~"    InputFile                    - The input file.\n"
526        ~"    OutputFile                   - The output file.\n"
527        ~"\n"
528        ~"Named Args:\n"
529        ~"    -v,--verbose                 - Verbose output\n"
530        ~"\n"
531        ~"Utility:\n"
532        ~"    Utility arguments used to modify the output.\n"
533        ~"\n"
534        ~"    CompressionLevel             - How much to compress the file.\n"
535        ~"    --encoding                   - Sets the encoding to use.",
536 
537         "\n"~builder.toString()
538     );
539 }
540 
541 /+ BUILT IN SECTION CONTENT +/
542 
543 /++
544  + A simple content class the simply displays a given string.
545  +
546  + Notes:
547  +  This class is fully compliant with the `HelpSectionOptions`.
548  + ++/
549 final class HelpSectionTextContent : IHelpSectionContent
550 {
551     ///
552     string text;
553 
554     ///
555     this(string text)
556     {
557         this.text = text;
558     }
559 
560     string getContent(const HelpSectionOptions options)
561     {
562         return this.lineWrap(options, this.text);
563     }
564 }
565 ///
566 unittest
567 {
568     import std.exception : assertThrown;
569 
570     auto options = HelpSectionOptions("\t", 7 + 2); // '+ 2' is for the prefix + ending new line. 7 is the wanted char limit.
571     auto content = new HelpSectionTextContent("Hey Hip Lell Loll");
572     assert(content.getContent(options) == 
573         "\tHey Hip\n"
574        ~"\tLell Lo\n"
575        ~"\tll",
576     
577         "\n"~content.getContent(options)
578     );
579 
580     options.lineCharLimit = 200;
581     assert(content.getContent(options) == "\tHey Hip Lell Loll");
582 
583     options.lineCharLimit = 2; // Enough for the prefix and ending new line, but not enough for any piece of text.
584     assertThrown(content.getContent(options));
585 
586     options.lineCharLimit = 3; // Useable, but not readable.
587     assert(content.getContent(options) ==
588         "\tH\n"
589        ~"\te\n"
590        ~"\ty\n"
591 
592        ~"\tH\n"
593        ~"\ti\n"
594        ~"\tp\n"
595 
596        ~"\tL\n"
597        ~"\te\n"
598        ~"\tl\n"
599        ~"\tl\n"
600 
601        ~"\tL\n"
602        ~"\to\n"
603        ~"\tl\n"
604        ~"\tl"
605     );
606 }
607 
608 /++
609  + A content class for displaying information about a command line argument.
610  +
611  + Notes:
612  +  Please see this class' unittest to see an example of it's output.
613  + ++/
614 final class HelpSectionArgInfoContent : IHelpSectionContent
615 {
616     /// The divisor used to determine how many characters to use for the arg's name(s)
617     enum NAME_CHAR_LIMIT_DIVIDER = 4;
618 
619     /// The string used to split an arg's name(s) from its description.
620     const MIDDLE_AFFIX = " - ";
621 
622     /// The information about the arg.
623     struct ArgInfo
624     {
625         /// The different names that this arg can be used with (e.g 'v', 'verbose').
626         string[] names;
627 
628         /// The description of the argument.
629         string description;
630 
631         /// Whether the arg is optional or not.
632         ArgIsOptional isOptional;
633     }
634 
635     /// All registered args.
636     ArgInfo[] args;
637 
638     /// Whether to add the dash prefix to the args' name(s). e.g. "--option" vs "option"
639     AutoAddArgDashes addDashes;
640     
641     ///
642     this(ArgInfo[] args, AutoAddArgDashes addDashes)
643     {
644         this.args = args;
645         this.addDashes = addDashes;
646     }
647 
648     string getContent(const HelpSectionOptions options)
649     {
650         import std.array     : array;
651         import std.algorithm : map, reduce, filter, count, max, splitter, substitute;
652         import std.conv      : to;
653         import std.exception : assumeUnique;
654         import std.utf       : byChar;
655 
656         // Calculate some variables.
657         const USEABLE_CHARS     = (options.lineCharLimit - options.linePrefix.length) - MIDDLE_AFFIX.length; // How many chars in total we can use.
658         const NAME_CHARS        = USEABLE_CHARS / NAME_CHAR_LIMIT_DIVIDER;                                   // How many chars per line we can use for the names.
659         const DESCRIPTION_CHARS = USEABLE_CHARS - NAME_CHARS;                                                // How many chars per line we can use for the description.
660         const DESCRIPTION_START = options.linePrefix.length + NAME_CHARS + MIDDLE_AFFIX.length;              // How many chars in that the description starts.
661 
662         // Creating the options and padding for the names and description.
663         HelpSectionOptions nameOptions;
664         nameOptions.linePrefix    = options.linePrefix;
665         nameOptions.lineCharLimit = NAME_CHARS + options.linePrefix.length;
666 
667         auto padding = new char[DESCRIPTION_START];
668         padding[] = ' ';
669 
670         HelpSectionOptions descriptionOptions;
671         descriptionOptions.linePrefix    = padding.assumeUnique;
672         descriptionOptions.lineCharLimit = DESCRIPTION_CHARS + DESCRIPTION_START; // For the first line, the padding needs to be removed manually.
673 
674         char[] output;
675         output.reserve(4096);
676 
677         // Hello inefficient code, my old friend...
678         foreach(arg; this.args)
679         {
680             // Line wrap. (This line alone is like, O(3n), not even mentioning memory usage)
681             auto nameText = lineWrap(
682                 nameOptions, 
683                 arg.names.map!(n => (this.addDashes) 
684                                      ? (n.length == 1) ? "-"~n : "--"~n
685                                      : n
686                          )
687                          .filter!(n => n.length > 0)
688                          .reduce!((a, b) => a~","~b)
689                          .byChar
690                          .array
691             );
692 
693             auto descriptionText = lineWrap(
694                 descriptionOptions,
695                 arg.description
696             );
697 
698             if(descriptionText.length > descriptionOptions.linePrefix.length)
699                 descriptionText = descriptionText[descriptionOptions.linePrefix.length..$]; // Remove the padding from the first line.
700 
701             // Then create our output line-by-line
702             auto nameLines = nameText.splitter('\n');
703             auto descriptionLines = descriptionText.splitter('\n');
704 
705             bool isFirstLine = true;
706             size_t nameLength = 0;
707             while(!nameLines.empty || !descriptionLines.empty)
708             {
709                 if(!nameLines.empty)
710                 {
711                     nameLength = nameLines.front.length; // Need to keep track of this for the next two ifs.
712 
713                     output ~= nameLines.front;
714                     nameLines.popFront();
715                 }
716                 else
717                     nameLength = 0;
718 
719                 if(isFirstLine)
720                 {
721                     // Push the middle affix into the middle, where it should be.
722                     const ptrdiff_t missingChars = (NAME_CHARS - nameLength) + nameOptions.linePrefix.length;
723                     if(missingChars > 0)
724                         output ~= descriptionOptions.linePrefix[0..missingChars];
725 
726                     output ~= MIDDLE_AFFIX;
727                 }
728 
729                 if(!descriptionLines.empty)
730                 {
731                     auto description = descriptionLines.front;
732                     if(!isFirstLine)
733                         description = description[nameLength..$]; // The name might be multi-line, so we need to adjust the padding.
734 
735                     output ~= description;
736                     descriptionLines.popFront();
737                 }
738 
739                 output ~= "\n";
740                 isFirstLine = false; // IMPORTANT for it to be here, please don't move it.
741             }
742         }
743 
744         // Void the last new line, as it provides consistency with HelpSectionTextContent
745         if(output.length > 0 && output[$-1] == '\n')
746             output = output[0..$-1];
747 
748         // debug
749         // {
750         //     char[] a;
751         //     a.length = nameOptions.linePrefix.length;
752         //     a[] = '1';
753         //     output ~= a;
754 
755         //     a.length = NAME_CHARS;
756         //     a[] = '2';
757         //     output ~= a;
758 
759         //     a.length = MIDDLE_AFFIX.length;
760         //     a[] = '3';
761         //     output ~= a;
762 
763         //     a.length = DESCRIPTION_CHARS;
764         //     a[] = '4';
765         //     output ~= a;
766         //     output ~= '\n';
767 
768         //     a.length = DESCRIPTION_START;
769         //     a[] = '>';
770         //     output ~= a;
771         //     output ~= '\n';
772 
773         //     a.length = descriptionOptions.lineCharLimit;
774         //     a[] = '*';
775         //     output ~= a;
776         //     output ~= '\n';
777 
778         //     a.length = options.lineCharLimit;
779         //     a[] = '#';
780         //     output ~= a;
781         // }
782 
783         return output.assumeUnique;
784     }
785 }
786 ///
787 unittest
788 {
789     auto content = new HelpSectionArgInfoContent(
790         [
791             HelpSectionArgInfoContent.ArgInfo(["v", "verbose"],           "Display detailed information about what the program is doing."),
792             HelpSectionArgInfoContent.ArgInfo(["f", "file"],              "The input file."),
793             HelpSectionArgInfoContent.ArgInfo(["super","longer","names"], "Some unusuable command with long names and a long description.")
794         ],
795 
796         AutoAddArgDashes.yes
797     );
798     auto options = HelpSectionOptions(
799         "    ",
800         80
801     );
802 
803     assert(content.getContent(options) ==
804         "    -v,--verbose       - Display detailed information about what the program is\n"
805        ~"                         doing.\n"
806        ~"    -f,--file          - The input file.\n"
807        ~"    --super,--longer,  - Some unusuable command with long names and a long desc\n"
808        ~"    --names              ription.",
809 
810         "\n"~content.getContent(options)
811     );
812 }