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 <vladimir@thecybershadow.net> 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 private enum OptionType { switch_, option, parameter } 32 33 struct OptionImpl(OptionType type_, T_, string description_, char shorthand_, string placeholder_, string name_) 34 { 35 enum type = type_; 36 alias T = T_; 37 enum description = description_; 38 enum shorthand = shorthand_; 39 enum placeholder = placeholder_; 40 enum name = name_; 41 42 T value; 43 alias value this; 44 45 this(T value_) 46 { 47 value = value_; 48 } 49 } 50 51 /// An on/off switch (e.g. --verbose). Does not have a value, other than its presence. 52 template Switch(string description=null, char shorthand=0, string name=null) 53 { 54 alias Switch = OptionImpl!(OptionType.switch_, bool, description, shorthand, null, name); 55 } 56 57 /// An option with a value (e.g. --tries N). The default placeholder depends on the type 58 /// (N for numbers, STR for strings). 59 template Option(T, string description=null, string placeholder=null, char shorthand=0, string name=null) 60 { 61 alias Option = OptionImpl!(OptionType.option, T, description, shorthand, placeholder, name); 62 } 63 64 /// An ordered parameter. 65 template Parameter(T, string description=null, string name=null) 66 { 67 alias Parameter = OptionImpl!(OptionType.parameter, T, description, 0, null, name); 68 } 69 70 /// Specify this as the description to hide the option from --help output. 71 enum hiddenOption = "hiddenOption"; 72 73 private template OptionValueType(T) 74 { 75 static if (is(T == OptionImpl!Args, Args...)) 76 alias OptionValueType = T.T; 77 else 78 alias OptionValueType = T; 79 } 80 81 private OptionValueType!T* optionValue(T)(ref T option) 82 { 83 static if (is(T == OptionImpl!Args, Args...)) 84 return &option.value; 85 else 86 return &option; 87 } 88 89 private template isParameter(T) 90 { 91 static if (is(T == OptionImpl!Args, Args...)) 92 enum isParameter = T.type == OptionType.parameter; 93 else 94 static if (is(T == bool)) 95 enum isParameter = false; 96 else 97 enum isParameter = true; 98 } 99 100 private template isOptionArray(Param) 101 { 102 alias T = OptionValueType!Param; 103 static if (is(Unqual!T == string)) 104 enum isOptionArray = false; 105 else 106 static if (is(T U : U[])) 107 enum isOptionArray = true; 108 else 109 enum isOptionArray = false; 110 } 111 112 private template optionShorthand(T) 113 { 114 static if (is(T == OptionImpl!Args, Args...)) 115 enum optionShorthand = T.shorthand; 116 else 117 enum char optionShorthand = 0; 118 } 119 120 private template optionDescription(T) 121 { 122 static if (is(T == OptionImpl!Args, Args...)) 123 enum optionDescription = T.description; 124 else 125 enum string optionDescription = null; 126 } 127 128 private enum bool optionHasDescription(T) = !isHiddenOption!T && optionDescription!T !is null; 129 130 private template optionPlaceholder(T) 131 { 132 static if (is(T == OptionImpl!Args, Args...)) 133 { 134 static if (T.placeholder.length) 135 enum optionPlaceholder = T.placeholder; 136 else 137 enum optionPlaceholder = optionPlaceholder!(OptionValueType!T); 138 } 139 else 140 static if (isOptionArray!T) 141 enum optionPlaceholder = optionPlaceholder!(typeof(T.init[0])); 142 else 143 static if (is(T : real)) 144 enum optionPlaceholder = "N"; 145 else 146 static if (is(T == string)) 147 enum optionPlaceholder = "STR"; 148 else 149 enum optionPlaceholder = "X"; 150 } 151 152 private template optionName(T, string paramName) 153 { 154 static if (is(T == OptionImpl!Args, Args...)) 155 static if (T.name) 156 enum optionName = T.name; 157 else 158 enum optionName = paramName; 159 else 160 enum optionName = paramName; 161 } 162 163 private template isHiddenOption(T) 164 { 165 static if (is(T == OptionImpl!Args, Args...)) 166 static if (T.description is hiddenOption) 167 enum isHiddenOption = true; 168 else 169 enum isHiddenOption = false; 170 else 171 enum isHiddenOption = false; 172 } 173 174 struct FunOptConfig 175 { 176 std.getopt.config[] getoptConfig; 177 } 178 179 private template optionNames(alias FUN) 180 { 181 alias Params = ParameterTypeTuple!FUN; 182 alias parameterNames = ParameterIdentifierTuple!FUN; 183 enum optionNameAt(int i) = optionName!(Params[i], parameterNames[i]); 184 alias optionNames = staticMap!(optionNameAt, RangeTuple!(parameterNames.length)); 185 } 186 187 /// Default help text print function. 188 /// Sends the text to stderr.writeln. 189 void defaultUsageFun(string usage) 190 { 191 import std.stdio; 192 stderr.writeln(usage); 193 } 194 195 /// Parse the given arguments according to FUN's parameters, and call FUN. 196 /// Throws GetOptException on errors. 197 auto funopt(alias FUN, FunOptConfig config = FunOptConfig.init, alias usageFun = defaultUsageFun)(string[] args) 198 if (isFunction!FUN) 199 { 200 alias Params = staticMap!(Unqual, ParameterTypeTuple!FUN); 201 Params values; 202 enum names = optionNames!FUN; 203 alias defaults = ParameterDefaultValueTuple!FUN; 204 205 foreach (i, defaultValue; defaults) 206 { 207 static if (!is(defaultValue == void)) 208 { 209 //values[i] = defaultValue; 210 // https://issues.dlang.org/show_bug.cgi?id=13252 211 values[i] = cast(OptionValueType!(Params[i])) defaultValue; 212 } 213 } 214 215 enum structFields = 216 config.getoptConfig.length.iota.map!(n => "std.getopt.config config%d = std.getopt.config.%s;\n".format(n, config.getoptConfig[n])).join() ~ 217 Params.length.iota.map!(n => "string selector%d; OptionValueType!(Params[%d])* value%d;\n".format(n, n, n)).join(); 218 219 static struct GetOptArgs { mixin(structFields); } 220 GetOptArgs getOptArgs; 221 222 static string optionSelector(int i)() 223 { 224 string[] variants; 225 auto shorthand = optionShorthand!(Params[i]); 226 if (shorthand) 227 variants ~= [shorthand]; 228 enum keywords = names[i].identifierToCommandLineKeywords(); 229 variants ~= keywords; 230 return variants.join("|"); 231 } 232 233 foreach (i, ref value; values) 234 { 235 enum selector = optionSelector!i(); 236 mixin("getOptArgs.selector%d = selector;".format(i)); 237 mixin("getOptArgs.value%d = optionValue(values[%d]);".format(i, i)); 238 } 239 240 auto origArgs = args; 241 bool help; 242 243 getopt(args, 244 std.getopt.config.bundling, 245 getOptArgs.tupleof, 246 "h|help", &help, 247 ); 248 249 void printUsage() 250 { 251 usageFun(getUsage!FUN(origArgs[0])); 252 } 253 254 if (help) 255 { 256 printUsage(); 257 static if (is(ReturnType!FUN == void)) 258 return; 259 else 260 return ReturnType!FUN.init; 261 } 262 263 args = args[1..$]; 264 265 // Slurp remaining, unparsed arguments into parameter fields 266 267 foreach (i, ref value; values) 268 { 269 alias T = Params[i]; 270 static if (isParameter!T) 271 { 272 static if (is(OptionValueType!T : const(string)[])) 273 { 274 values[i] = cast(OptionValueType!T)args; 275 args = null; 276 } 277 else 278 { 279 if (args.length) 280 { 281 values[i] = to!(OptionValueType!T)(args[0]); 282 args = args[1..$]; 283 } 284 else 285 { 286 static if (is(defaults[i] == void)) 287 { 288 // If the first argument is mandatory, 289 // and no arguments were given, print usage. 290 if (origArgs.length == 1) 291 printUsage(); 292 293 enum plainName = names[i].identifierToPlainText; 294 throw new GetOptException("No " ~ plainName ~ " specified."); 295 } 296 } 297 } 298 } 299 } 300 301 if (args.length) 302 throw new GetOptException("Extra parameters specified: %(%s %)".format(args)); 303 304 return FUN(values); 305 } 306 307 unittest 308 { 309 void f1(bool verbose, Option!int tries, string filename) 310 { 311 assert(verbose); 312 assert(tries == 5); 313 assert(filename == "filename.ext"); 314 } 315 funopt!f1(["program", "--verbose", "--tries", "5", "filename.ext"]); 316 317 void f2(string a, Parameter!string b, string[] rest) 318 { 319 assert(a == "a"); 320 assert(b == "b"); 321 assert(rest == ["c", "d"]); 322 } 323 funopt!f2(["program", "a", "b", "c", "d"]); 324 325 void f3(Option!(string[], null, "DIR", 'x') excludeDir) 326 { 327 assert(excludeDir == ["a", "b", "c"]); 328 } 329 funopt!f3(["program", "--excludedir", "a", "--exclude-dir", "b", "-x", "c"]); 330 331 void f4(Option!string outputFile = "output.txt", string inputFile = "input.txt", string[] dataFiles = null) 332 { 333 assert(inputFile == "input.txt"); 334 assert(outputFile == "output.txt"); 335 assert(dataFiles == []); 336 } 337 funopt!f4(["program"]); 338 339 void f5(string input = null) 340 { 341 assert(input is null); 342 } 343 funopt!f5(["program"]); 344 } 345 346 // *************************************************************************** 347 348 private string canonicalizeCommandLineArgument(string s) { return s.replace("-", ""); } 349 private string canonicalizeIdentifier(string s) { return s.chomp("_").toLower(); } 350 private string identifierToCommandLineKeyword(string s) { return s.chomp("_").splitByCamelCase.join("-").toLower(); } 351 private string identifierToCommandLineParam (string s) { return s.chomp("_").splitByCamelCase.join("-").toUpper(); } 352 private string identifierToPlainText (string s) { return s.chomp("_").splitByCamelCase.join(" ").toLower(); } 353 private string[] identifierToCommandLineKeywords(string s) { auto words = s.chomp("_").splitByCamelCase(); return [words.join().toLower()] ~ (words.length > 1 ? [words.join("-").toLower()] : []); } /// for getopt 354 355 private string getProgramName(string program) 356 { 357 auto programName = program.baseName(); 358 version(Windows) 359 { 360 programName = programName.toLower(); 361 if (programName.extension == ".exe") 362 programName = programName.stripExtension(); 363 } 364 365 return programName; 366 } 367 368 string getUsage(alias FUN)(string program) 369 { 370 auto programName = getProgramName(program); 371 enum formatString = getUsageFormatString!FUN(); 372 return formatString.format(programName); 373 } 374 375 string getUsageFormatString(alias FUN)() 376 { 377 alias ParameterTypeTuple!FUN Params; 378 enum names = [optionNames!FUN]; 379 alias defaults = ParameterDefaultValueTuple!FUN; 380 381 string result = "Usage: %s"; 382 enum inSynopsis(Param) = isParameter!Param || !optionHasDescription!Param; 383 enum haveOmittedOptions = !allSatisfy!(inSynopsis, Params); 384 static if (haveOmittedOptions) 385 result ~= " [OPTION]..."; 386 387 string getSwitchText(int i)() 388 { 389 alias Param = Params[i]; 390 static if (isParameter!Param) 391 return names[i].identifierToCommandLineParam(); 392 else 393 { 394 string switchText = "--" ~ names[i].identifierToCommandLineKeyword(); 395 static if (is(Param == OptionImpl!Args, Args...)) 396 static if (Param.type == OptionType.option) 397 switchText ~= (optionPlaceholder!Param.canFind('=') ? ' ' : '=') ~ optionPlaceholder!Param; 398 return switchText; 399 } 400 } 401 402 foreach (i, Param; Params) 403 static if (!isHiddenOption!Param && inSynopsis!Param) 404 { 405 static if (isParameter!Param) 406 { 407 result ~= " "; 408 static if (!is(defaults[i] == void)) 409 result ~= "["; 410 result ~= names[i].identifierToCommandLineParam(); 411 static if (!is(defaults[i] == void)) 412 result ~= "]"; 413 } 414 else 415 result ~= " [" ~ getSwitchText!i() ~ "]"; 416 static if (isOptionArray!Param) 417 result ~= "..."; 418 } 419 420 result ~= "\n"; 421 422 enum haveDescriptions = anySatisfy!(optionHasDescription, Params); 423 static if (haveDescriptions) 424 { 425 enum haveShorthands = anySatisfy!(optionShorthand, Params); 426 string[Params.length] selectors; 427 size_t longestSelector; 428 429 foreach (i, Param; Params) 430 static if (optionHasDescription!Param) 431 { 432 string switchText = getSwitchText!i(); 433 if (haveShorthands) 434 { 435 auto c = optionShorthand!Param; 436 if (c) 437 selectors[i] = "-%s, %s".format(c, switchText); 438 else 439 selectors[i] = " %s".format(switchText); 440 } 441 else 442 selectors[i] = switchText; 443 longestSelector = max(longestSelector, selectors[i].length); 444 } 445 446 result ~= "\nOptions:\n"; 447 foreach (i, Param; Params) 448 static if (optionHasDescription!Param) 449 result ~= optionWrap(optionDescription!Param, selectors[i], longestSelector); 450 } 451 452 return result; 453 } 454 455 string optionWrap(string text, string firstIndent, size_t indentWidth) 456 { 457 enum width = 79; 458 auto padding = " ".replicate(2 + indentWidth + 2); 459 text = text.findSplit("\n\n")[0]; 460 auto paragraphs = text.split1("\n"); 461 auto result = wrap( 462 paragraphs[0], 463 width, 464 " %-*s ".format(indentWidth, firstIndent), 465 padding 466 ); 467 result ~= paragraphs[1..$].map!(p => wrap(p, width, padding, padding)).join(); 468 return result; 469 } 470 471 unittest 472 { 473 void f1( 474 Switch!("Enable verbose logging", 'v') verbose, 475 Option!(int, "Number of tries") tries, 476 Option!(int, "Seconds to\nwait each try", "SECS", 0, "timeout") t, 477 in string filename, 478 string output = "default", 479 string[] extraFiles = null 480 ) 481 {} 482 483 auto usage = getUsage!f1("program"); 484 assert(usage == 485 "Usage: program [OPTION]... FILENAME [OUTPUT] [EXTRA-FILES]... 486 487 Options: 488 -v, --verbose Enable verbose logging 489 --tries=N Number of tries 490 --timeout=SECS Seconds to 491 wait each try 492 ", usage); 493 494 void f2( 495 bool verbose, 496 Option!(string[]) extraFile, 497 string filename, 498 string output = "default", 499 ) 500 {} 501 502 usage = getUsage!f2("program"); 503 assert(usage == 504 "Usage: program [--verbose] [--extra-file=STR]... FILENAME [OUTPUT] 505 ", usage); 506 507 void f3( 508 Parameter!(string[]) args = null, 509 ) 510 {} 511 512 usage = getUsage!f3("program"); 513 assert(usage == 514 "Usage: program [ARGS]... 515 ", usage); 516 517 void f4( 518 Parameter!(string[], "The program arguments.") args = null, 519 ) 520 {} 521 522 usage = getUsage!f4("program"); 523 assert(usage == 524 "Usage: program [ARGS]... 525 526 Options: 527 ARGS The program arguments. 528 ", usage); 529 530 void f5( 531 Option!(string[], "Features to disable.") without = null, 532 ) 533 {} 534 535 usage = getUsage!f5("program"); 536 assert(usage == 537 "Usage: program [OPTION]... 538 539 Options: 540 --without=STR Features to disable. 541 ", usage); 542 543 // If all options are on the command line, don't add "[OPTION]..." 544 void f6( 545 bool verbose, 546 Parameter!(string[], "Files to transmogrify.") files = null, 547 ) 548 {} 549 550 usage = getUsage!f6("program"); 551 assert(usage == 552 "Usage: program [--verbose] [FILES]... 553 554 Options: 555 FILES Files to transmogrify. 556 ", usage); 557 } 558 559 // *************************************************************************** 560 561 /// Dispatch the command line to a type's static methods, according to the 562 /// first parameter on the given command line (the "action"). 563 /// String UDAs are used as usage documentation for generating --help output 564 /// (or when no action is specified). 565 auto funoptDispatch(alias Actions, FunOptConfig config = FunOptConfig.init, alias usageFun = defaultUsageFun)(string[] args) 566 { 567 string program = args[0]; 568 569 auto fun(string action, string[] actionArguments = []) 570 { 571 action = action.canonicalizeCommandLineArgument(); 572 573 static void descUsageFun(string description)(string usage) 574 { 575 auto lines = usage.split("\n"); 576 usageFun((lines[0..1] ~ [null, description] ~ lines[1..$]).join("\n")); 577 } 578 579 foreach (m; __traits(allMembers, Actions)) 580 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 581 { 582 alias member = I!(__traits(getMember, Actions, m)); 583 enum name = m.canonicalizeIdentifier(); 584 if (name == action) 585 { 586 static if (hasAttribute!(string, member)) 587 { 588 enum description = getAttribute!(string, member); 589 alias myUsageFun = descUsageFun!description; 590 } 591 else 592 alias myUsageFun = usageFun; 593 594 auto args = [getProgramName(program) ~ " " ~ action] ~ actionArguments; 595 static if (is(member == struct)) 596 return funoptDispatch!(member, config, usageFun)(args); 597 else 598 return funopt!(member, config, myUsageFun)(args); 599 } 600 } 601 602 throw new GetOptException("Unknown action: " ~ action); 603 } 604 605 static void myUsageFun(string usage) { usageFun(usage ~ funoptDispatchUsage!Actions()); } 606 607 const FunOptConfig myConfig = (){ 608 auto c = config; 609 c.getoptConfig ~= std.getopt.config.stopOnFirstNonOption; 610 return c; 611 }(); 612 return funopt!(fun, myConfig, myUsageFun)(args); 613 } 614 615 string funoptDispatchUsage(alias Actions)() 616 { 617 string result = "\nActions:\n"; 618 619 size_t longestAction = 0; 620 foreach (m; __traits(allMembers, Actions)) 621 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 622 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 623 { 624 enum length = m.identifierToCommandLineKeyword().length; 625 longestAction = max(longestAction, length); 626 } 627 628 foreach (m; __traits(allMembers, Actions)) 629 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 630 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 631 { 632 enum name = m.identifierToCommandLineKeyword(); 633 //__traits(comment, __traits(getMember, Actions, m)) // https://github.com/D-Programming-Language/dmd/pull/3531 634 result ~= optionWrap(getAttribute!(string, __traits(getMember, Actions, m)), name, longestAction); 635 } 636 637 return result; 638 } 639 640 unittest 641 { 642 struct Actions 643 { 644 @(`Perform action f1`) 645 static void f1(bool verbose) {} 646 647 @(`An action sub-group`) 648 struct fooBar 649 { 650 @(`Create a new foobar`) 651 static void new_() {} 652 } 653 } 654 655 funoptDispatch!Actions(["program", "f1", "--verbose"]); 656 657 assert(funoptDispatchUsage!Actions() == " 658 Actions: 659 f1 Perform action f1 660 foo-bar An action sub-group 661 "); 662 663 funoptDispatch!Actions(["program", "foo-bar", "new"]); 664 665 assert(funoptDispatchUsage!(Actions.fooBar)() == " 666 Actions: 667 new Create a new foobar 668 "); 669 670 static string usage; 671 static void usageFun(string _usage) { usage = _usage; } 672 funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "f1", "--help"]); 673 assert(usage == "Usage: unittest f1 [--verbose] 674 675 Perform action f1 676 ", usage); 677 }