1 /**
2  * Translate command-line parameters to a function signature,
3  * generating --help text automatically.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <ae@cy.md>
13  */
14 
15 module ae.utils.funopt;
16 
17 import std.algorithm;
18 import std.array;
19 import std.conv;
20 import std.getopt;
21 import std.path;
22 import std.range;
23 import std.string;
24 import std.traits;
25 import std.typetuple;
26 
27 import ae.utils.meta : structFields, hasAttribute, getAttribute, rangeTuple, I, ParameterNames;
28 import ae.utils.array : split1;
29 import ae.utils.text;
30 
31 public static import std.getopt; // See ae.utils.main
32 
33 private enum OptionType { switch_, option, parameter }
34 
35 struct _OptionImpl(OptionType type_, T_, string description_, char shorthand_, string placeholder_, string name_)
36 {
37 	enum type = type_;
38 	alias T = T_;
39 	enum description = description_;
40 	enum shorthand = shorthand_;
41 	enum placeholder = placeholder_;
42 	enum name = name_;
43 
44 	T value;
45 	alias value this;
46 
47 	this(T value_)
48 	{
49 		value = value_;
50 	}
51 }
52 
53 /// An on/off switch (e.g. --verbose). Does not have a value, other than its presence.
54 template Switch(string description=null, char shorthand=0, string name=null)
55 {
56 	alias Switch = _OptionImpl!(OptionType.switch_, bool, description, shorthand, null, name);
57 }
58 
59 /// An option with a value (e.g. --tries N). The default placeholder depends on the type
60 /// (N for numbers, STR for strings).
61 template Option(T, string description=null, string placeholder=null, char shorthand=0, string name=null)
62 {
63 	alias Option = _OptionImpl!(OptionType.option, T, description, shorthand, placeholder, name);
64 }
65 
66 /// An ordered parameter.
67 template Parameter(T, string description=null, string name=null)
68 {
69 	alias Parameter = _OptionImpl!(OptionType.parameter, T, description, 0, null, name);
70 }
71 
72 /// Specify this as the description to hide the option from --help output.
73 enum hiddenOption = "hiddenOption";
74 
75 private template OptionValueType(T)
76 {
77 	static if (is(T == _OptionImpl!Args, Args...))
78 		alias OptionValueType = T.T;
79 	else
80 		alias OptionValueType = T;
81 }
82 
83 private OptionValueType!T* optionValue(T)(ref T option)
84 {
85 	static if (is(T == _OptionImpl!Args, Args...))
86 		return &option.value;
87 	else
88 		return &option;
89 }
90 
91 private template isParameter(T)
92 {
93 	static if (is(T == _OptionImpl!Args, Args...))
94 		enum isParameter = T.type == OptionType.parameter;
95 	else
96 	static if (is(T == bool))
97 		enum isParameter = false;
98 	else
99 		enum isParameter = true;
100 }
101 
102 private template isOptionArray(Param)
103 {
104 	alias T = OptionValueType!Param;
105 	static if (is(Unqual!T == string))
106 		enum isOptionArray = false;
107 	else
108 	static if (is(T U : U[]))
109 		enum isOptionArray = true;
110 	else
111 		enum isOptionArray = false;
112 }
113 
114 private template optionShorthand(T)
115 {
116 	static if (is(T == _OptionImpl!Args, Args...))
117 		enum optionShorthand = T.shorthand;
118 	else
119 		enum char optionShorthand = 0;
120 }
121 
122 private template optionDescription(T)
123 {
124 	static if (is(T == _OptionImpl!Args, Args...))
125 		enum optionDescription = T.description;
126 	else
127 		enum string optionDescription = null;
128 }
129 
130 private enum bool optionHasDescription(T) = !isHiddenOption!T && optionDescription!T !is null;
131 
132 private template optionPlaceholder(T)
133 {
134 	static if (is(T == _OptionImpl!Args, Args...))
135 	{
136 		static if (T.placeholder.length)
137 			enum optionPlaceholder = T.placeholder;
138 		else
139 			enum optionPlaceholder = optionPlaceholder!(OptionValueType!T);
140 	}
141 	else
142 	static if (isOptionArray!T)
143 		enum optionPlaceholder = optionPlaceholder!(typeof(T.init[0]));
144 	else
145 	static if (is(T : real))
146 		enum optionPlaceholder = "N";
147 	else
148 	static if (is(T == string))
149 		enum optionPlaceholder = "STR";
150 	else
151 		enum optionPlaceholder = "X";
152 }
153 
154 private template optionName(T, string paramName)
155 {
156 	static if (is(T == _OptionImpl!Args, Args...))
157 		static if (T.name)
158 			enum optionName = T.name;
159 		else
160 			enum optionName = paramName;
161 	else
162 		enum optionName = paramName;
163 }
164 
165 private template isHiddenOption(T)
166 {
167 	static if (is(T == _OptionImpl!Args, Args...))
168 		static if (T.description is hiddenOption)
169 			enum isHiddenOption = true;
170 		else
171 			enum isHiddenOption = false;
172 	else
173 		enum isHiddenOption = false;
174 }
175 
176 /// `funopt` configuration.
177 struct FunOptConfig
178 {
179 	/// `getopt` configuration.
180 	std.getopt.config[] getoptConfig;
181 }
182 
183 private template optionNames(alias FUN)
184 {
185 	alias Params = ParameterTypeTuple!FUN;
186 	alias parameterNames = ParameterNames!FUN;
187 	enum optionNameAt(int i) = optionName!(Params[i], parameterNames[i]);
188 	alias optionNames = staticMap!(optionNameAt, rangeTuple!(parameterNames.length));
189 }
190 
191 /// Default help text print function.
192 /// Sends the text to stderr.writeln.
193 void defaultUsageFun(string usage)
194 {
195 	import std.stdio;
196 	stderr.writeln(usage);
197 }
198 
199 /// Parse the given arguments according to FUN's parameters, and call FUN.
200 /// Throws GetOptException on errors.
201 auto funopt(alias FUN, FunOptConfig config = FunOptConfig.init, alias usageFun = defaultUsageFun)(string[] args)
202 if (isCallable!FUN)
203 {
204 	alias Params = staticMap!(Unqual, ParameterTypeTuple!FUN);
205 	Params values;
206 	enum names = optionNames!FUN;
207 	alias defaults = ParameterDefaultValueTuple!FUN;
208 
209 	foreach (i, defaultValue; defaults)
210 	{
211 		static if (!is(defaultValue == void))
212 		{
213 			//values[i] = defaultValue;
214 			// https://issues.dlang.org/show_bug.cgi?id=13252
215 			values[i] = cast(OptionValueType!(Params[i])) defaultValue;
216 		}
217 	}
218 
219 	// Can't pass options with empty names to getopt, filter them out.
220 	static immutable string[] namesArr = [names];
221 	static immutable bool[] optionUseGetOpt = Params.length.iota.map!(n => namesArr[n].length > 0).array;
222 
223 	enum structFields =
224 		config.getoptConfig.length.iota.map!(n => "std.getopt.config config%d = std.getopt.config.%s;\n".format(n, config.getoptConfig[n])).join() ~
225 		Params.length.iota
226 			.filter!(n => optionUseGetOpt[n])
227 			.map!(n => "string selector%d; OptionValueType!(Params[%d])* value%d;\n".format(n, n, n)).join();
228 
229 	static struct GetOptArgs { mixin(structFields); }
230 	GetOptArgs getOptArgs;
231 
232 	static string optionSelector(int i)()
233 	{
234 		string[] variants;
235 		auto shorthand = optionShorthand!(Params[i]);
236 		if (shorthand)
237 			variants ~= [shorthand];
238 		enum keywords = names[i].identifierToCommandLineKeywords();
239 		variants ~= keywords;
240 		return variants.join("|");
241 	}
242 
243 	foreach (i, ref value; values)
244 		static if (optionUseGetOpt[i])
245 		{
246 			enum selector = optionSelector!i();
247 			mixin("getOptArgs.selector%d = selector;".format(i));
248 			mixin("getOptArgs.value%d = optionValue(values[%d]);".format(i, i));
249 		}
250 
251 	auto origArgs = args;
252 	bool help;
253 
254 	getopt(args,
255 		std.getopt.config.bundling,
256 		getOptArgs.tupleof,
257 		"h|help", &help,
258 	);
259 
260 	void printUsage()
261 	{
262 		usageFun(getUsage!FUN(origArgs[0]));
263 	}
264 
265 	if (help)
266 	{
267 		printUsage();
268 		static if (is(ReturnType!FUN == void))
269 			return;
270 		else
271 			return ReturnType!FUN.init;
272 	}
273 
274 	args = args[1..$];
275 
276 	// Slurp remaining, unparsed arguments into parameter fields
277 
278 	foreach (i, ref value; values)
279 	{
280 		alias T = Params[i];
281 		static if (isParameter!T)
282 		{
283 			static if (isOptionArray!(OptionValueType!T))
284 			{
285 				values[i] = args.map!(arg => arg.to!(ElementType!(OptionValueType!T))).array;
286 				args = null;
287 			}
288 			else
289 			{
290 				if (args.length)
291 				{
292 					values[i] = to!(OptionValueType!T)(args[0]);
293 					args = args[1..$];
294 				}
295 				else
296 				{
297 					static if (is(defaults[i] == void))
298 					{
299 						// If the first argument is mandatory,
300 						// and no arguments were given, print usage.
301 						if (origArgs.length == 1)
302 							printUsage();
303 
304 						enum plainName = names[i].identifierToPlainText;
305 						throw new GetOptException("No " ~ plainName ~ " specified.");
306 					}
307 				}
308 			}
309 		}
310 	}
311 
312 	if (args.length)
313 		throw new GetOptException("Extra parameters specified: %(%s %)".format(args));
314 
315 	return FUN(values);
316 }
317 
318 unittest
319 {
320 	void f1(bool verbose, Option!int tries, string filename)
321 	{
322 		assert(verbose);
323 		assert(tries == 5);
324 		assert(filename == "filename.ext");
325 	}
326 	funopt!f1(["program", "--verbose", "--tries", "5", "filename.ext"]);
327 
328 	void f2(string a, Parameter!string b, string[] rest)
329 	{
330 		assert(a == "a");
331 		assert(b == "b");
332 		assert(rest == ["c", "d"]);
333 	}
334 	funopt!f2(["program", "a", "b", "c", "d"]);
335 
336 	void f3(Option!(string[], null, "DIR", 'x') excludeDir)
337 	{
338 		assert(excludeDir == ["a", "b", "c"]);
339 	}
340 	funopt!f3(["program", "--excludedir", "a", "--exclude-dir", "b", "-x", "c"]);
341 
342 	void f4(Option!string outputFile = "output.txt", string inputFile = "input.txt", string[] dataFiles = null)
343 	{
344 		assert(inputFile == "input.txt");
345 		assert(outputFile == "output.txt");
346 		assert(dataFiles == []);
347 	}
348 	funopt!f4(["program"]);
349 
350 	void f5(string input = null)
351 	{
352 		assert(input is null);
353 	}
354 	funopt!f5(["program"]);
355 
356 	funopt!({})(["program"]);
357 
358 	funopt!((int) {})(["program", "5"]);
359 
360 	funopt!((int n) { assert(n); })(["program", "5"]);
361 
362 	funopt!((int[] n) { assert(n == [12, 34]); })(["program", "12", "34"]);
363 }
364 
365 // ***************************************************************************
366 
367 private string canonicalizeCommandLineArgument(string s) { return s.replace("-", ""); }
368 private string canonicalizeIdentifier(string s) { return s.chomp("_").toLower(); }
369 private string identifierToCommandLineKeyword(string s) { return s.length ? s.chomp("_").splitByCamelCase.join("-").toLower() : "parameter"; }
370 private string identifierToCommandLineParam  (string s) { return s.length ? s.chomp("_").splitByCamelCase.join("-").toUpper() : "PARAMETER"; }
371 private string identifierToPlainText         (string s) { return s.length ? s.chomp("_").splitByCamelCase.join(" ").toLower() : "parameter"; }
372 private string[] identifierToCommandLineKeywords(string s) { auto words = s.chomp("_").splitByCamelCase(); return [words.join().toLower()] ~ (words.length > 1 ? [words.join("-").toLower()] : []); } /// for getopt
373 
374 private string getProgramName(string program)
375 {
376 	auto programName = program.baseName();
377 	version(Windows)
378 	{
379 		programName = programName.toLower();
380 		if (programName.extension == ".exe")
381 			programName = programName.stripExtension();
382 	}
383 
384 	return programName;
385 }
386 
387 private string escapeFmt(string s) { return s.replace("%", "%%"); }
388 
389 /// Constructs the `funopt` usage string.
390 string getUsage(alias FUN)(string program)
391 {
392 	auto programName = getProgramName(program);
393 	enum formatString = getUsageFormatString!FUN();
394 	return formatString.format(programName);
395 }
396 
397 /// Constructs the `funopt` usage format string.
398 /// `"%1$s"` is used instead of the program name.
399 string getUsageFormatString(alias FUN)()
400 {
401 	alias ParameterTypeTuple!FUN Params;
402 	enum names = [optionNames!FUN];
403 	alias defaults = ParameterDefaultValueTuple!FUN;
404 
405 	string result = "Usage: %1$s";
406 	enum inSynopsis(Param) = isParameter!Param || !optionHasDescription!Param;
407 	enum haveOmittedOptions = !allSatisfy!(inSynopsis, Params);
408 	static if (haveOmittedOptions)
409 	{
410 		enum haveRequiredOptions = (){
411 			bool result = false;
412 			foreach (i, Param; Params)
413 				static if (!inSynopsis!Param && Param.type == OptionType.option && is(defaults[i] == void))
414 					result = true;
415 			return result;
416 		}();
417 		static if (haveRequiredOptions)
418 			result ~= " OPTION...";
419 		else
420 			result ~= " [OPTION]...";
421 	}
422 
423 	string getSwitchText(int i)()
424 	{
425 		alias Param = Params[i];
426 		static if (isParameter!Param)
427 			return names[i].identifierToCommandLineParam();
428 		else
429 		{
430 			string switchText = "--" ~ names[i].identifierToCommandLineKeyword();
431 			static if (is(Param == _OptionImpl!Args, Args...))
432 				static if (Param.type == OptionType.option)
433 					switchText ~= (optionPlaceholder!Param.canFind('=') ? ' ' : '=') ~ optionPlaceholder!Param;
434 			return switchText;
435 		}
436 	}
437 
438 	string optionalEnd;
439 	void flushOptional() { result ~= optionalEnd; optionalEnd = null; }
440 	foreach (i, Param; Params)
441 		static if (!isHiddenOption!Param && inSynopsis!Param)
442 		{
443 			static if (isParameter!Param)
444 			{
445 				result ~= " ";
446 				static if (!is(defaults[i] == void))
447 				{
448 					result ~= "[";
449 					optionalEnd ~= "]";
450 				}
451 				result ~= names[i].identifierToCommandLineParam();
452 			}
453 			else
454 			{
455 				flushOptional();
456 				result ~= " [" ~ getSwitchText!i().escapeFmt() ~ "]";
457 			}
458 			static if (isOptionArray!Param)
459 				result ~= "...";
460 		}
461 	flushOptional();
462 
463 	result ~= "\n";
464 	static if (hasAttribute!(string, FUN))
465 	{
466 		enum description = getAttribute!(string, FUN);
467 		result ~= "\n" ~ description ~ "\n";
468 	}
469 
470 	enum haveDescriptions = anySatisfy!(optionHasDescription, Params);
471 	static if (haveDescriptions)
472 	{
473 		enum haveShorthands = anySatisfy!(optionShorthand, Params);
474 		string[Params.length] selectors;
475 		size_t longestSelector;
476 
477 		foreach (i, Param; Params)
478 			static if (optionHasDescription!Param)
479 			{
480 				string switchText = getSwitchText!i();
481 				if (haveShorthands)
482 				{
483 					auto c = optionShorthand!Param;
484 					if (c)
485 						selectors[i] = "-%s, %s".format(c, switchText);
486 					else
487 						selectors[i] = "    %s".format(switchText);
488 				}
489 				else
490 					selectors[i] = switchText;
491 				longestSelector = max(longestSelector, selectors[i].length);
492 			}
493 
494 		result ~= "\nOptions:\n";
495 		foreach (i, Param; Params)
496 			static if (optionHasDescription!Param)
497 				result ~= optionWrap(optionDescription!Param.escapeFmt(), selectors[i], longestSelector);
498 	}
499 
500 	return result;
501 }
502 
503 /// Performs line wrapping for option descriptions.
504 string optionWrap(string text, string firstIndent, size_t indentWidth)
505 {
506 	enum width = 79;
507 	auto padding = " ".replicate(2 + indentWidth + 2);
508 	text = text.findSplit("\n\n")[0];
509 	auto paragraphs = text.split1("\n");
510 	auto result = verbatimWrap(
511 		paragraphs[0],
512 		width,
513 		"  %-*s  ".format(indentWidth, firstIndent),
514 		padding
515 	);
516 	result ~= paragraphs[1..$].map!(p => verbatimWrap(p, width, padding, padding)).join();
517 	return result;
518 }
519 
520 unittest
521 {
522 	void f1(
523 		Switch!("Enable verbose logging", 'v') verbose,
524 		Option!(int, "Number of tries") tries,
525 		Option!(int, "Seconds to\nwait each try", "SECS", 0, "timeout") t,
526 		in string filename,
527 		string output = "default",
528 		string[] extraFiles = null
529 	)
530 	{}
531 
532 	auto usage = getUsage!f1("program");
533 	assert(usage ==
534 "Usage: program OPTION... FILENAME [OUTPUT [EXTRA-FILES...]]
535 
536 Options:
537   -v, --verbose       Enable verbose logging
538       --tries=N       Number of tries
539       --timeout=SECS  Seconds to
540                       wait each try
541 ", usage);
542 
543 	void f2(
544 		bool verbose,
545 		Option!(string[]) extraFile,
546 		string filename,
547 		string output = "default",
548 	)
549 	{}
550 
551 	usage = getUsage!f2("program");
552 	assert(usage ==
553 "Usage: program [--verbose] [--extra-file=STR]... FILENAME [OUTPUT]
554 ", usage);
555 
556 	void f3(
557 		Parameter!(string[]) args = null,
558 	)
559 	{}
560 
561 	usage = getUsage!f3("program");
562 	assert(usage ==
563 "Usage: program [ARGS...]
564 ", usage);
565 
566 	void f4(
567 		Parameter!(string[], "The program arguments.") args = null,
568 	)
569 	{}
570 
571 	usage = getUsage!f4("program");
572 	assert(usage ==
573 "Usage: program [ARGS...]
574 
575 Options:
576   ARGS  The program arguments.
577 ", usage);
578 
579 	void f5(
580 		Option!(string[], "Features to disable.") without = null,
581 	)
582 	{}
583 
584 	usage = getUsage!f5("program");
585 	assert(usage ==
586 "Usage: program [OPTION]...
587 
588 Options:
589   --without=STR  Features to disable.
590 ", usage);
591 
592 	// If all options are on the command line, don't add "[OPTION]..."
593 	void f6(
594 		bool verbose,
595 		Parameter!(string[], "Files to transmogrify.") files = null,
596 	)
597 	{}
598 
599 	usage = getUsage!f6("program");
600 	assert(usage ==
601 "Usage: program [--verbose] [FILES...]
602 
603 Options:
604   FILES  Files to transmogrify.
605 ", usage);
606 
607 	// Ensure % characters work as expected.
608 	void f7(
609 		Parameter!(int, "How much power % to use.") powerPct,
610 	)
611 	{}
612 
613 	usage = getUsage!f7("program");
614 	assert(usage ==
615 "Usage: program POWER-PCT
616 
617 Options:
618   POWER-PCT  How much power % to use.
619 ", usage);
620 
621 	// Test program descriptions
622 	@(`Refrobnicates the transmogrifier.`)
623 	void f8(Switch!"Be verbose." verbose){}
624 
625 	usage = getUsage!f8("program");
626 	assert(usage ==
627 "Usage: program [OPTION]...
628 
629 Refrobnicates the transmogrifier.
630 
631 Options:
632   --verbose  Be verbose.
633 ", usage);
634 }
635 
636 // ***************************************************************************
637 
638 private string escapeRoff(string s)
639 {
640 	string result;
641 	foreach (c; s)
642 	{
643 		if (c == '\\')
644 			result ~= '\\';
645 		result ~= c;
646 	}
647 	return result;
648 }
649 
650 /// Constructs a roff man page based on the `funopt` usage.
651 string generateManPage(alias FUN)(
652 	/// Program name, as it should appear in the header, synopsis etc.
653 	/// If not specified, the identifier name of FUN is used.
654 	string programName = null,
655 	/// Long description for the DESCRIPTION section.
656 	/// If not specified, we either use the function string UDA, or omit the section entirely.
657 	string longDescription = null,
658 	/// Short description for the NAME section.
659 	/// If not specified, generated from the function string UDA.
660 	string shortDescription = null,
661 	/// Additional sections (BUGS, AUTHORS) to add at the end.
662 	string footer = null,
663 	/// Manual section
664 	int section = 1,
665 )
666 {
667 	if (!programName)
668 		programName = __traits(identifier, FUN);
669 
670 	string funDescription;
671 	static if (hasAttribute!(string, FUN))
672 		funDescription = getAttribute!(string, FUN).escapeRoff;
673 
674 	string otherDescription;
675 	if (funDescription)
676 		otherDescription = funDescription;
677 	else
678 		otherDescription = longDescription;
679 
680 	if (!shortDescription && otherDescription)
681 	{
682 		auto parts = (otherDescription ~ " ").findSplit(". ");
683 		if (!parts[2].length && otherDescription is longDescription)
684 			longDescription = null; // Move into shortDescription
685 		shortDescription = parts[0]
686 			.I!(s => toLower(s[0 .. 1]) ~ s[1 .. $]);
687 		shortDescription.skipOver(programName ~ " ");
688 	}
689 
690 	// Bold the program name at the start of the description
691 	if (longDescription.skipOver(programName ~ " "))
692 		longDescription = ".B " ~ programName ~ "\n" ~ longDescription;
693 
694 	alias ParameterTypeTuple!FUN Params;
695 	enum names = [optionNames!FUN];
696 	alias defaults = ParameterDefaultValueTuple!FUN;
697 
698 	string result;
699 	result ~= ".TH %s %d\n".format(programName.toUpper, section);
700 	result ~= ".SH NAME\n";
701 	if (shortDescription)
702 		result ~= "%s \\- %s\n".format(programName, shortDescription);
703 	else
704 		result ~= "%s\n".format(programName);
705 	result ~= ".SH SYNOPSIS\n";
706 	result ~= "\\fB%s\\fP".format(programName);
707 
708 	enum inSynopsis(Param) = isParameter!Param || !optionHasDescription!Param;
709 	enum haveOmittedOptions = !allSatisfy!(inSynopsis, Params);
710 	static if (haveOmittedOptions)
711 	{
712 		enum haveRequiredOptions = /*function bool*/(){
713 			bool result = false;
714 			foreach (i, Param; Params)
715 				static if (!inSynopsis!Param && Param.type == OptionType.option && is(defaults[i] == void))
716 					result = true;
717 			return result;
718 		}();
719 		static if (haveRequiredOptions)
720 			result ~= " \\fIOPTION\\fP...";
721 		else
722 			result ~= " [\\fIOPTION\\fP]...";
723 	}
724 
725 	string getSwitchText(int i)()
726 	{
727 		alias Param = Params[i];
728 		static if (isParameter!Param)
729 			return "\\fI" ~ names[i].identifierToCommandLineParam() ~ "\\fP";
730 		else
731 		{
732 			string switchText = "\\fB--" ~ names[i].identifierToCommandLineKeyword() ~ "\\fP";
733 			static if (is(Param == _OptionImpl!Args, Args...))
734 				static if (Param.type == OptionType.option)
735 					switchText ~= (optionPlaceholder!Param.canFind('=') ? ' ' : '=') ~ "\\fI" ~ optionPlaceholder!Param ~ "\\fP";
736 			return switchText;
737 		}
738 	}
739 
740 	string optionalEnd;
741 	void flushOptional() { result ~= optionalEnd; optionalEnd = null; }
742 	foreach (i, Param; Params)
743 		static if (!isHiddenOption!Param && inSynopsis!Param)
744 		{
745 			static if (isParameter!Param)
746 			{
747 				result ~= " ";
748 				static if (!is(defaults[i] == void))
749 				{
750 					result ~= "[";
751 					optionalEnd ~= "]";
752 				}
753 				result ~= "\\fI" ~ names[i].identifierToCommandLineParam() ~ "\\fP";
754 			}
755 			else
756 			{
757 				flushOptional();
758 				result ~= " [" ~ getSwitchText!i() ~ "]";
759 			}
760 			static if (isOptionArray!Param)
761 				result ~= "...";
762 		}
763 	flushOptional();
764 	result ~= "\n";
765 
766 	if (longDescription)
767 	{
768 		result ~= ".SH DESCRIPTION\n";
769 		result ~= longDescription;
770 		result ~= "\n";
771 	}
772 
773 	enum haveDescriptions = anySatisfy!(optionHasDescription, Params);
774 	static if (haveDescriptions)
775 	{
776 		result ~= ".SH OPTIONS\n\n";
777 
778 		foreach (i, Param; Params)
779 			static if (optionHasDescription!Param)
780 			{
781 				result ~= ".TP\n";
782 				auto c = optionShorthand!Param;
783 				if (c)
784 					result ~= "\\fB-%s\\fP, ".format(c);
785 				auto description = optionDescription!Param
786 					.replace("\n", "\n\n")
787 					.escapeRoff;
788 				result ~= getSwitchText!i() ~ "\n" ~ description ~ "\n\n";
789 			}
790 	}
791 
792 	result ~= footer;
793 
794 	return result;
795 }
796 
797 unittest
798 {
799 	@(`Frobnicates whatsits.`)
800 	void f1(
801 		Switch!("Enable verbose logging", 'v') verbose,
802 		Option!(int, "Number of tries") tries,
803 		Option!(int, "Seconds to wait each try", "SECS", 0, "timeout") t,
804 		in string filename,
805 		string output = "default",
806 		string[] extraFiles = null
807 	)
808 	{}
809 
810 	auto man = generateManPage!f1();
811 	assert(man ==
812 `.TH F1 1
813 .SH NAME
814 f1 \- frobnicates whatsits
815 .SH SYNOPSIS
816 \fBf1\fP \fIOPTION\fP... \fIFILENAME\fP [\fIOUTPUT\fP [\fIEXTRA-FILES\fP...]]
817 .SH OPTIONS
818 
819 .TP
820 \fB-v\fP, \fB--verbose\fP
821 Enable verbose logging
822 
823 .TP
824 \fB--tries\fP=\fIN\fP
825 Number of tries
826 
827 .TP
828 \fB--timeout\fP=\fISECS\fP
829 Seconds to wait each try
830 
831 `, man);
832 }
833 
834 // ***************************************************************************
835 
836 /// Dispatch the command line to a type's static methods, according to the
837 /// first parameter on the given command line (the "action").
838 /// String UDAs are used as usage documentation for generating --help output
839 /// (or when no action is specified).
840 auto funoptDispatch(alias Actions, FunOptConfig config = FunOptConfig.init, alias usageFun = defaultUsageFun)(string[] args)
841 {
842 	string program = args[0];
843 
844 	auto funImpl(string action, string[] actionArguments = [])
845 	{
846 		action = action.canonicalizeCommandLineArgument();
847 
848 		foreach (m; __traits(allMembers, Actions))
849 			static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m)))))
850 			{
851 				alias member = I!(__traits(getMember, Actions, m));
852 				enum name = m.canonicalizeIdentifier();
853 				if (name == action)
854 				{
855 					auto args = [getProgramName(program) ~ " " ~ action] ~ actionArguments;
856 					static if (is(member == struct))
857 						return funoptDispatch!(member, config, usageFun)(args);
858 					else
859 						return funopt!(member, config, usageFun)(args);
860 				}
861 			}
862 
863 		throw new GetOptException("Unknown action: " ~ action);
864 	}
865 
866 	static if (hasAttribute!(string, Actions))
867 	{
868 		@(getAttribute!(string, Actions))
869 		auto fun(string action, string[] actionArguments = []) { return funImpl(action, actionArguments); }
870 	}
871 	else
872 		auto fun(string action, string[] actionArguments = []) { return funImpl(action, actionArguments); }
873 
874 	static void myUsageFun(string usage) { usageFun(usage ~ funoptDispatchUsage!Actions()); }
875 
876 	const FunOptConfig myConfig = (){
877 		auto c = config;
878 		c.getoptConfig ~= std.getopt.config.stopOnFirstNonOption;
879 		return c;
880 	}();
881 	return funopt!(fun, myConfig, myUsageFun)(args);
882 }
883 
884 /// Constructs the `funoptDispatch` usage string.
885 string funoptDispatchUsage(alias Actions)()
886 {
887 	string result = "\nActions:\n";
888 
889 	size_t longestAction = 0;
890 	foreach (m; __traits(allMembers, Actions))
891 		static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m)))))
892 			static if (hasAttribute!(string, __traits(getMember, Actions, m)))
893 			{
894 				enum length = m.identifierToCommandLineKeyword().length;
895 				longestAction = max(longestAction, length);
896 			}
897 
898 	foreach (m; __traits(allMembers, Actions))
899 		static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m)))))
900 			static if (hasAttribute!(string, __traits(getMember, Actions, m)))
901 			{
902 				enum name = m.identifierToCommandLineKeyword();
903 				//__traits(comment, __traits(getMember, Actions, m)) // https://github.com/D-Programming-Language/dmd/pull/3531
904 				result ~= optionWrap(getAttribute!(string, __traits(getMember, Actions, m)), name, longestAction);
905 			}
906 
907 	return result;
908 }
909 
910 unittest
911 {
912 	@(`Test program.`)
913 	struct Actions
914 	{
915 		@(`Perform action f1`)
916 		static void f1(bool verbose) {}
917 
918 		@(`Perform complicated action f2
919 
920 This action is complicated because of reasons.`)
921 		static void f2() {}
922 
923 		@(`An action sub-group`)
924 		struct fooBar
925 		{
926 			@(`Create a new foobar`)
927 			static void new_() {}
928 		}
929 	}
930 
931 	funoptDispatch!Actions(["program", "f1", "--verbose"]);
932 	funoptDispatch!Actions(["program", "foo-bar", "new"]);
933 
934 	static string usage;
935 	static void usageFun(string _usage) { usage = _usage; }
936 	funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "--help"]);
937 	assert(usage == "Usage: unittest ACTION [ACTION-ARGUMENTS...]
938 
939 Test program.
940 
941 Actions:
942   f1       Perform action f1
943   f2       Perform complicated action f2
944   foo-bar  An action sub-group
945 ", usage);
946 
947 	funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "f1", "--help"]);
948 	assert(usage == "Usage: unittest f1 [--verbose]
949 
950 Perform action f1
951 ", usage);
952 
953 	funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "f2", "--help"]);
954 	assert(usage == "Usage: unittest f2
955 
956 Perform complicated action f2
957 
958 This action is complicated because of reasons.
959 ", usage);
960 
961 	funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "foo-bar", "--help"]);
962 	assert(usage == "Usage: unittest foobar ACTION [ACTION-ARGUMENTS...]
963 
964 An action sub-group
965 
966 Actions:
967   new  Create a new foobar
968 ", usage);
969 }