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