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 }