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