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; 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 { 198 alias Params = staticMap!(Unqual, ParameterTypeTuple!FUN); 199 Params values; 200 enum names = optionNames!FUN; 201 alias defaults = ParameterDefaultValueTuple!FUN; 202 203 foreach (i, defaultValue; defaults) 204 { 205 static if (!is(defaultValue == void)) 206 { 207 //values[i] = defaultValue; 208 // https://issues.dlang.org/show_bug.cgi?id=13252 209 values[i] = cast(OptionValueType!(Params[i])) defaultValue; 210 } 211 } 212 213 enum structFields = 214 config.getoptConfig.length.iota.map!(n => "std.getopt.config config%d = std.getopt.config.%s;\n".format(n, config.getoptConfig[n])).join() ~ 215 Params.length.iota.map!(n => "string selector%d; OptionValueType!(Params[%d])* value%d;\n".format(n, n, n)).join(); 216 217 static struct GetOptArgs { mixin(structFields); } 218 GetOptArgs getOptArgs; 219 220 static string optionSelector(int i)() 221 { 222 string[] variants; 223 auto shorthand = optionShorthand!(Params[i]); 224 if (shorthand) 225 variants ~= [shorthand]; 226 enum words = names[i].splitByCamelCase(); 227 variants ~= words.join().toLower(); 228 if (words.length > 1) 229 variants ~= words.join("-").toLower(); 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 throw new GetOptException("No " ~ names[i] ~ " specified."); 294 } 295 } 296 } 297 } 298 } 299 300 if (args.length) 301 throw new GetOptException("Extra parameters specified: %(%s %)".format(args)); 302 303 return FUN(values); 304 } 305 306 unittest 307 { 308 void f1(bool verbose, Option!int tries, string filename) 309 { 310 assert(verbose); 311 assert(tries == 5); 312 assert(filename == "filename.ext"); 313 } 314 funopt!f1(["program", "--verbose", "--tries", "5", "filename.ext"]); 315 316 void f2(string a, Parameter!string b, string[] rest) 317 { 318 assert(a == "a"); 319 assert(b == "b"); 320 assert(rest == ["c", "d"]); 321 } 322 funopt!f2(["program", "a", "b", "c", "d"]); 323 324 void f3(Option!(string[], null, "DIR", 'x') excludeDir) 325 { 326 assert(excludeDir == ["a", "b", "c"]); 327 } 328 funopt!f3(["program", "--excludedir", "a", "--exclude-dir", "b", "-x", "c"]); 329 330 void f4(Option!string outputFile = "output.txt", string inputFile = "input.txt", string[] dataFiles = null) 331 { 332 assert(inputFile == "input.txt"); 333 assert(outputFile == "output.txt"); 334 assert(dataFiles == []); 335 } 336 funopt!f4(["program"]); 337 338 void f5(string input = null) 339 { 340 assert(input is null); 341 } 342 funopt!f5(["program"]); 343 } 344 345 // *************************************************************************** 346 347 private string getProgramName(string program) 348 { 349 auto programName = program.baseName(); 350 version(Windows) 351 { 352 programName = programName.toLower(); 353 if (programName.extension == ".exe") 354 programName = programName.stripExtension(); 355 } 356 357 return programName; 358 } 359 360 string getUsage(alias FUN)(string program) 361 { 362 auto programName = getProgramName(program); 363 enum formatString = getUsageFormatString!FUN(); 364 return formatString.format(programName); 365 } 366 367 string getUsageFormatString(alias FUN)() 368 { 369 alias ParameterTypeTuple!FUN Params; 370 enum names = [optionNames!FUN]; 371 alias defaults = ParameterDefaultValueTuple!FUN; 372 373 string result = "Usage: %s"; 374 enum inSynopsis(Param) = isParameter!Param || !optionHasDescription!Param; 375 enum haveOmittedOptions = !allSatisfy!(inSynopsis, Params); 376 static if (haveOmittedOptions) 377 result ~= " [OPTION]..."; 378 379 string getSwitchText(int i)() 380 { 381 alias Param = Params[i]; 382 static if (isParameter!Param) 383 return names[i].splitByCamelCase().join("-").toUpper(); 384 else 385 { 386 string switchText = "--" ~ names[i].splitByCamelCase().join("-").toLower(); 387 static if (is(Param == OptionImpl!Args, Args...)) 388 static if (Param.type == OptionType.option) 389 switchText ~= (optionPlaceholder!Param.canFind('=') ? ' ' : '=') ~ optionPlaceholder!Param; 390 return switchText; 391 } 392 } 393 394 foreach (i, Param; Params) 395 static if (!isHiddenOption!Param && inSynopsis!Param) 396 { 397 static if (isParameter!Param) 398 { 399 result ~= " "; 400 static if (!is(defaults[i] == void)) 401 result ~= "["; 402 result ~= names[i].splitByCamelCase().join("-").toUpper(); 403 static if (!is(defaults[i] == void)) 404 result ~= "]"; 405 } 406 else 407 result ~= " [" ~ getSwitchText!i() ~ "]"; 408 static if (isOptionArray!Param) 409 result ~= "..."; 410 } 411 412 result ~= "\n"; 413 414 enum haveDescriptions = anySatisfy!(optionHasDescription, Params); 415 static if (haveDescriptions) 416 { 417 enum haveShorthands = anySatisfy!(optionShorthand, Params); 418 string[Params.length] selectors; 419 size_t longestSelector; 420 421 foreach (i, Param; Params) 422 static if (optionHasDescription!Param) 423 { 424 string switchText = getSwitchText!i(); 425 if (haveShorthands) 426 { 427 auto c = optionShorthand!Param; 428 if (c) 429 selectors[i] = "-%s, %s".format(c, switchText); 430 else 431 selectors[i] = " %s".format(switchText); 432 } 433 else 434 selectors[i] = switchText; 435 longestSelector = max(longestSelector, selectors[i].length); 436 } 437 438 result ~= "\nOptions:\n"; 439 foreach (i, Param; Params) 440 static if (optionHasDescription!Param) 441 result ~= optionWrap(optionDescription!Param, selectors[i], longestSelector); 442 } 443 444 return result; 445 } 446 447 string optionWrap(string text, string firstIndent, size_t indentWidth) 448 { 449 enum width = 79; 450 auto padding = " ".replicate(2 + indentWidth + 2); 451 auto paragraphs = text.split("\n"); 452 auto result = wrap( 453 paragraphs[0], 454 width, 455 " %-*s ".format(indentWidth, firstIndent), 456 padding 457 ); 458 result ~= paragraphs[1..$].map!(p => wrap(p, width, padding, padding)).join(); 459 return result; 460 } 461 462 unittest 463 { 464 void f1( 465 Switch!("Enable verbose logging", 'v') verbose, 466 Option!(int, "Number of tries") tries, 467 Option!(int, "Seconds to\nwait each try", "SECS", 0, "timeout") t, 468 in string filename, 469 string output = "default", 470 string[] extraFiles = null 471 ) 472 {} 473 474 auto usage = getUsage!f1("program"); 475 assert(usage == 476 "Usage: program [OPTION]... FILENAME [OUTPUT] [EXTRA-FILES]... 477 478 Options: 479 -v, --verbose Enable verbose logging 480 --tries=N Number of tries 481 --timeout=SECS Seconds to 482 wait each try 483 ", usage); 484 485 void f2( 486 bool verbose, 487 Option!(string[]) extraFile, 488 string filename, 489 string output = "default", 490 ) 491 {} 492 493 usage = getUsage!f2("program"); 494 assert(usage == 495 "Usage: program [--verbose] [--extra-file=STR]... FILENAME [OUTPUT] 496 ", usage); 497 498 void f3( 499 Parameter!(string[]) args = null, 500 ) 501 {} 502 503 usage = getUsage!f3("program"); 504 assert(usage == 505 "Usage: program [ARGS]... 506 ", usage); 507 508 void f4( 509 Parameter!(string[], "The program arguments.") args = null, 510 ) 511 {} 512 513 usage = getUsage!f4("program"); 514 assert(usage == 515 "Usage: program [ARGS]... 516 517 Options: 518 ARGS The program arguments. 519 ", usage); 520 521 void f5( 522 Option!(string[], "Features to disable.") without = null, 523 ) 524 {} 525 526 usage = getUsage!f5("program"); 527 assert(usage == 528 "Usage: program [OPTION]... 529 530 Options: 531 --without=STR Features to disable. 532 ", usage); 533 534 // If all options are on the command line, don't add "[OPTION]..." 535 void f6( 536 bool verbose, 537 Parameter!(string[], "Files to transmogrify.") files = null, 538 ) 539 {} 540 541 usage = getUsage!f6("program"); 542 assert(usage == 543 "Usage: program [--verbose] [FILES]... 544 545 Options: 546 FILES Files to transmogrify. 547 ", usage); 548 } 549 550 // *************************************************************************** 551 552 /// Dispatch the command line to a type's static methods, according to the 553 /// first parameter on the given command line (the "action"). 554 /// String UDAs are used as usage documentation for generating --help output 555 /// (or when no action is specified). 556 auto funoptDispatch(alias Actions, FunOptConfig config = FunOptConfig.init, alias usageFun = defaultUsageFun)(string[] args) 557 { 558 string program = args[0]; 559 560 auto fun(string action, string[] actionArguments = []) 561 { 562 action = action.replace("-", ""); 563 564 static void descUsageFun(string description)(string usage) 565 { 566 auto lines = usage.split("\n"); 567 usageFun((lines[0..1] ~ [null, description] ~ lines[1..$]).join("\n")); 568 } 569 570 foreach (m; __traits(allMembers, Actions)) 571 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 572 { 573 enum name = m.toLower(); 574 if (name == action) 575 { 576 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 577 { 578 enum description = getAttribute!(string, __traits(getMember, Actions, m)); 579 alias myUsageFun = descUsageFun!description; 580 } 581 else 582 alias myUsageFun = usageFun; 583 584 auto args = [getProgramName(program) ~ " " ~ action] ~ actionArguments; 585 return funopt!(__traits(getMember, Actions, m), config, myUsageFun)(args); 586 } 587 } 588 589 throw new GetOptException("Unknown action: " ~ action); 590 } 591 592 static void myUsageFun(string usage) { usageFun(usage ~ genActionList!Actions()); } 593 594 const FunOptConfig myConfig = (){ 595 auto c = config; 596 c.getoptConfig ~= std.getopt.config.stopOnFirstNonOption; 597 return c; 598 }(); 599 return funopt!(fun, myConfig, myUsageFun)(args); 600 } 601 602 private string genActionList(alias Actions)() 603 { 604 string result = "\nActions:\n"; 605 606 size_t longestAction = 0; 607 foreach (m; __traits(allMembers, Actions)) 608 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 609 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 610 longestAction = max(longestAction, m.splitByCamelCase.join("-").length); 611 612 foreach (m; __traits(allMembers, Actions)) 613 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 614 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 615 { 616 enum name = m.splitByCamelCase.join("-").toLower(); 617 //__traits(comment, __traits(getMember, Actions, m)) // https://github.com/D-Programming-Language/dmd/pull/3531 618 result ~= optionWrap(getAttribute!(string, __traits(getMember, Actions, m)), name, longestAction); 619 } 620 621 return result; 622 } 623 624 unittest 625 { 626 struct Actions 627 { 628 @(`Perform action f1`) 629 static void f1(bool verbose) {} 630 } 631 632 funoptDispatch!Actions(["program", "f1", "--verbose"]); 633 634 assert(genActionList!Actions() == " 635 Actions: 636 f1 Perform action f1 637 "); 638 639 static string usage; 640 static void usageFun(string _usage) { usage = _usage; } 641 funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "f1", "--help"]); 642 assert(usage == "Usage: unittest f1 [--verbose] 643 644 Perform action f1 645 ", usage); 646 }