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 haveNonParameters = !allSatisfy!(isParameter, Params); 375 enum haveDescriptions = anySatisfy!(optionHasDescription, Params); 376 static if (haveNonParameters && haveDescriptions) 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 && (isParameter!Param || !optionHasDescription!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 static if (haveDescriptions) 415 { 416 enum haveShorthands = anySatisfy!(optionShorthand, Params); 417 string[Params.length] selectors; 418 size_t longestSelector; 419 420 foreach (i, Param; Params) 421 static if (optionHasDescription!Param) 422 { 423 string switchText = getSwitchText!i(); 424 if (haveShorthands) 425 { 426 auto c = optionShorthand!Param; 427 if (c) 428 selectors[i] = "-%s, %s".format(c, switchText); 429 else 430 selectors[i] = " %s".format(switchText); 431 } 432 else 433 selectors[i] = switchText; 434 longestSelector = max(longestSelector, selectors[i].length); 435 } 436 437 result ~= "\nOptions:\n"; 438 foreach (i, Param; Params) 439 static if (optionHasDescription!Param) 440 result ~= optionWrap(optionDescription!Param, selectors[i], longestSelector); 441 } 442 443 return result; 444 } 445 446 string optionWrap(string text, string firstIndent, size_t indentWidth) 447 { 448 enum width = 79; 449 auto padding = " ".replicate(2 + indentWidth + 2); 450 auto paragraphs = text.split("\n"); 451 auto result = wrap( 452 paragraphs[0], 453 width, 454 " %-*s ".format(indentWidth, firstIndent), 455 padding 456 ); 457 result ~= paragraphs[1..$].map!(p => wrap(p, width, padding, padding)).join(); 458 return result; 459 } 460 461 unittest 462 { 463 void f1( 464 Switch!("Enable verbose logging", 'v') verbose, 465 Option!(int, "Number of tries") tries, 466 Option!(int, "Seconds to\nwait each try", "SECS", 0, "timeout") t, 467 in string filename, 468 string output = "default", 469 string[] extraFiles = null 470 ) 471 {} 472 473 auto usage = getUsage!f1("program"); 474 assert(usage == 475 "Usage: program [OPTION]... FILENAME [OUTPUT] [EXTRA-FILES]... 476 477 Options: 478 -v, --verbose Enable verbose logging 479 --tries=N Number of tries 480 --timeout=SECS Seconds to 481 wait each try 482 ", usage); 483 484 void f2( 485 bool verbose, 486 Option!(string[]) extraFile, 487 string filename, 488 string output = "default", 489 ) 490 {} 491 492 usage = getUsage!f2("program"); 493 assert(usage == 494 "Usage: program [--verbose] [--extra-file=STR]... FILENAME [OUTPUT] 495 ", usage); 496 497 void f3( 498 Parameter!(string[]) args = null, 499 ) 500 {} 501 502 usage = getUsage!f3("program"); 503 assert(usage == 504 "Usage: program [ARGS]... 505 ", usage); 506 507 void f4( 508 Parameter!(string[], "The program arguments.") args = null, 509 ) 510 {} 511 512 usage = getUsage!f4("program"); 513 assert(usage == 514 "Usage: program [ARGS]... 515 516 Options: 517 ARGS The program arguments. 518 ", usage); 519 520 void f5( 521 Option!(string[], "Features to disable.") without = null, 522 ) 523 {} 524 525 usage = getUsage!f5("program"); 526 assert(usage == 527 "Usage: program [OPTION]... 528 529 Options: 530 --without=STR Features to disable. 531 ", usage); 532 } 533 534 // *************************************************************************** 535 536 /// Dispatch the command line to a type's static methods, according to the 537 /// first parameter on the given command line (the "action"). 538 /// String UDAs are used as usage documentation for generating --help output 539 /// (or when no action is specified). 540 auto funoptDispatch(alias Actions, FunOptConfig config = FunOptConfig.init, alias usageFun = defaultUsageFun)(string[] args) 541 { 542 string program = args[0]; 543 544 auto fun(string action, string[] actionArguments = []) 545 { 546 action = action.replace("-", ""); 547 548 static void descUsageFun(string description)(string usage) 549 { 550 auto lines = usage.split("\n"); 551 usageFun((lines[0..1] ~ [null, description] ~ lines[1..$]).join("\n")); 552 } 553 554 foreach (m; __traits(allMembers, Actions)) 555 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 556 { 557 enum name = m.toLower(); 558 if (name == action) 559 { 560 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 561 { 562 enum description = getAttribute!(string, __traits(getMember, Actions, m)); 563 alias myUsageFun = descUsageFun!description; 564 } 565 else 566 alias myUsageFun = usageFun; 567 568 auto args = [getProgramName(program) ~ " " ~ action] ~ actionArguments; 569 return funopt!(__traits(getMember, Actions, m), config, myUsageFun)(args); 570 } 571 } 572 573 throw new GetOptException("Unknown action: " ~ action); 574 } 575 576 static void myUsageFun(string usage) { usageFun(usage ~ genActionList!Actions()); } 577 578 const FunOptConfig myConfig = (){ 579 auto c = config; 580 c.getoptConfig ~= std.getopt.config.stopOnFirstNonOption; 581 return c; 582 }(); 583 return funopt!(fun, myConfig, myUsageFun)(args); 584 } 585 586 private string genActionList(alias Actions)() 587 { 588 string result = "\nActions:\n"; 589 590 size_t longestAction = 0; 591 foreach (m; __traits(allMembers, Actions)) 592 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 593 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 594 longestAction = max(longestAction, m.splitByCamelCase.join("-").length); 595 596 foreach (m; __traits(allMembers, Actions)) 597 static if (is(typeof(hasAttribute!(string, __traits(getMember, Actions, m))))) 598 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 599 { 600 enum name = m.splitByCamelCase.join("-").toLower(); 601 //__traits(comment, __traits(getMember, Actions, m)) // https://github.com/D-Programming-Language/dmd/pull/3531 602 result ~= optionWrap(getAttribute!(string, __traits(getMember, Actions, m)), name, longestAction); 603 } 604 605 return result; 606 } 607 608 unittest 609 { 610 struct Actions 611 { 612 @(`Perform action f1`) 613 static void f1(bool verbose) {} 614 } 615 616 funoptDispatch!Actions(["program", "f1", "--verbose"]); 617 618 assert(genActionList!Actions() == " 619 Actions: 620 f1 Perform action f1 621 "); 622 623 static string usage; 624 static void usageFun(string _usage) { usage = _usage; } 625 funoptDispatch!(Actions, FunOptConfig.init, usageFun)(["unittest", "f1", "--help"]); 626 assert(usage == "Usage: unittest f1 [--verbose] 627 628 Perform action f1 629 ", usage); 630 }