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