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