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.misc; 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_) 33 { 34 enum type = type_; 35 alias T = T_; 36 enum description = description_; 37 enum shorthand = shorthand_; 38 enum placeholder = placeholder_; 39 40 T value; 41 alias value this; 42 43 this(T value_) 44 { 45 value = value_; 46 } 47 } 48 49 /// An on/off switch (e.g. --verbose). Does not have a value, other than its presence. 50 template Switch(string description=null, char shorthand=0) 51 { 52 alias Switch = OptionImpl!(OptionType.switch_, bool, description, shorthand, null); 53 } 54 55 /// An option with a value (e.g. --tries N). The default placeholder depends on the type 56 /// (N for numbers, STR for strings). 57 template Option(T, string description=null, string placeholder=null, char shorthand=0) 58 { 59 alias Option = OptionImpl!(OptionType.option, T, description, shorthand, placeholder); 60 } 61 62 /// An ordered parameter. 63 template Parameter(T, string description=null) 64 { 65 alias Parameter = OptionImpl!(OptionType.parameter, T, description, 0, null); 66 } 67 68 private template OptionValueType(T) 69 { 70 static if (is(T == OptionImpl!Args, Args...)) 71 alias OptionValueType = T.T; 72 else 73 alias OptionValueType = T; 74 } 75 76 private OptionValueType!T* optionValue(T)(ref T option) 77 { 78 static if (is(T == OptionImpl!Args, Args...)) 79 return &option.value; 80 else 81 return &option; 82 } 83 84 private template isParameter(T) 85 { 86 static if (is(T == OptionImpl!Args, Args...)) 87 enum isParameter = T.type == OptionType.parameter; 88 else 89 static if (is(T == bool)) 90 enum isParameter = false; 91 else 92 enum isParameter = true; 93 } 94 95 private template isOptionArray(Param) 96 { 97 alias T = OptionValueType!Param; 98 static if (is(T == string)) 99 enum isOptionArray = false; 100 else 101 static if (is(T U : U[])) 102 enum isOptionArray = true; 103 else 104 enum isOptionArray = false; 105 } 106 107 private template optionShorthand(T) 108 { 109 static if (is(T == OptionImpl!Args, Args...)) 110 enum optionShorthand = T.shorthand; 111 else 112 enum char optionShorthand = 0; 113 } 114 115 private template optionDescription(T) 116 { 117 static if (is(T == OptionImpl!Args, Args...)) 118 enum optionDescription = T.description; 119 else 120 enum string optionDescription = null; 121 } 122 123 private enum bool optionHasDescription(T) = optionDescription!T !is null; 124 125 private template optionPlaceholder(T) 126 { 127 static if (is(T == OptionImpl!Args, Args...)) 128 { 129 static if (T.placeholder.length) 130 enum optionPlaceholder = T.placeholder; 131 else 132 enum optionPlaceholder = optionPlaceholder!(OptionValueType!T); 133 } 134 else 135 static if (isOptionArray!T) 136 enum optionPlaceholder = optionPlaceholder!(typeof(T.init[0])); 137 else 138 static if (is(T : real)) 139 enum optionPlaceholder = "N"; 140 else 141 static if (is(T == string)) 142 enum optionPlaceholder = "STR"; 143 else 144 enum optionPlaceholder = "X"; 145 } 146 147 struct FunOptConfig 148 { 149 std.getopt.config[] getoptConfig; 150 string usageHeader, usageFooter; 151 } 152 153 /// Parse the given arguments according to FUN's parameters, and call FUN. 154 /// Throws GetOptException on errors. 155 auto funopt(alias FUN, FunOptConfig config = FunOptConfig.init)(string[] args) 156 { 157 alias ParameterTypeTuple!FUN Params; 158 Params values; 159 enum names = [ParameterIdentifierTuple!FUN]; 160 alias defaults = ParameterDefaultValueTuple!FUN; 161 162 foreach (i, defaultValue; defaults) 163 { 164 static if (!is(defaultValue == void)) 165 { 166 //values[i] = defaultValue; 167 // https://issues.dlang.org/show_bug.cgi?id=13252 168 values[i] = cast(OptionValueType!(Params[i])) defaultValue; 169 } 170 } 171 172 enum structFields = 173 config.getoptConfig.length.iota.map!(n => "std.getopt.config config%d = std.getopt.config.%s;\n".format(n, config.getoptConfig[n])).join() ~ 174 Params.length.iota.map!(n => "string selector%d; OptionValueType!(Params[%d])* value%d;\n".format(n, n, n)).join(); 175 176 static struct GetOptArgs { mixin(structFields); } 177 GetOptArgs getOptArgs; 178 179 static string optionSelector(int i)() 180 { 181 string[] variants; 182 auto shorthand = optionShorthand!(Params[i]); 183 if (shorthand) 184 variants ~= [shorthand]; 185 enum words = names[i].splitByCamelCase(); 186 variants ~= words.join().toLower(); 187 if (words.length > 1) 188 variants ~= words.join("-").toLower(); 189 return variants.join("|"); 190 } 191 192 foreach (i, ref value; values) 193 { 194 enum selector = optionSelector!i(); 195 mixin("getOptArgs.selector%d = selector;".format(i)); 196 mixin("getOptArgs.value%d = optionValue(values[%d]);".format(i, i)); 197 } 198 199 auto origArgs = args; 200 bool help; 201 202 getopt(args, 203 std.getopt.config.bundling, 204 getOptArgs.tupleof, 205 "h|help", &help, 206 ); 207 208 void printUsage() 209 { 210 import std.stdio; 211 stderr.writeln(config.usageHeader, getUsage!FUN(origArgs[0]), config.usageFooter); 212 } 213 214 if (help) 215 { 216 printUsage(); 217 return cast(ReturnType!FUN)0; 218 } 219 220 args = args[1..$]; 221 222 foreach (i, ref value; values) 223 { 224 alias T = Params[i]; 225 static if (isParameter!T) 226 { 227 static if (is(T == string[])) 228 { 229 values[i] = args; 230 args = null; 231 } 232 else 233 { 234 if (args.length) 235 { 236 values[i] = to!T(args[0]); 237 args = args[1..$]; 238 } 239 else 240 { 241 static if (is(defaults[i] == void)) 242 { 243 // If the first argument is mandatory, 244 // and no arguments were given, print usage. 245 if (origArgs.length == 1) 246 printUsage(); 247 248 throw new GetOptException("No " ~ names[i] ~ " specified."); 249 } 250 } 251 } 252 } 253 } 254 255 if (args.length) 256 throw new GetOptException("Extra parameters specified: %(%s %)".format(args)); 257 258 return FUN(values); 259 } 260 261 unittest 262 { 263 void f1(bool verbose, Option!int tries, string filename) 264 { 265 assert(verbose); 266 assert(tries == 5); 267 assert(filename == "filename.ext"); 268 } 269 funopt!f1(["program", "--verbose", "--tries", "5", "filename.ext"]); 270 271 void f2(string a, Parameter!string b, string[] rest) 272 { 273 assert(a == "a"); 274 assert(b == "b"); 275 assert(rest == ["c", "d"]); 276 } 277 funopt!f2(["program", "a", "b", "c", "d"]); 278 279 void f3(Option!(string[], null, "DIR", 'x') excludeDir) 280 { 281 assert(excludeDir == ["a", "b", "c"]); 282 } 283 funopt!f3(["program", "--excludedir", "a", "--exclude-dir", "b", "-x", "c"]); 284 285 void f4(Option!string outputFile = "output.txt", string inputFile = "input.txt", string[] dataFiles = null) 286 { 287 assert(inputFile == "input.txt"); 288 assert(outputFile == "output.txt"); 289 assert(dataFiles == []); 290 } 291 funopt!f4(["program"]); 292 293 void f5(string input = null) 294 { 295 assert(input is null); 296 } 297 funopt!f5(["program"]); 298 } 299 300 // *************************************************************************** 301 302 private string getProgramName(string program) 303 { 304 auto programName = program.baseName(); 305 version(Windows) 306 { 307 programName = programName.toLower(); 308 if (programName.extension == ".exe") 309 programName = programName.stripExtension(); 310 } 311 312 return programName; 313 } 314 315 private string getUsage(alias FUN)(string program) 316 { 317 auto programName = getProgramName(program); 318 enum formatString = getUsageFormatString!FUN(); 319 return formatString.format(programName); 320 } 321 322 private string getUsageFormatString(alias FUN)() 323 { 324 alias ParameterTypeTuple!FUN Params; 325 enum names = [ParameterIdentifierTuple!FUN]; 326 alias defaults = ParameterDefaultValueTuple!FUN; 327 328 string result = "Usage: %s"; 329 enum haveNonParameters = !allSatisfy!(isParameter, Params); 330 enum haveDescriptions = anySatisfy!(optionHasDescription, Params); 331 static if (haveNonParameters && haveDescriptions) 332 result ~= " [OPTION]..."; 333 334 string getSwitchText(int i)() 335 { 336 alias Param = Params[i]; 337 string switchText = "--" ~ names[i].splitByCamelCase().join("-").toLower(); 338 static if (is(Param == OptionImpl!Args, Args...)) 339 static if (Param.type == OptionType.option) 340 switchText ~= "=" ~ optionPlaceholder!Param; 341 return switchText; 342 } 343 344 foreach (i, Param; Params) 345 { 346 static if (isParameter!Param) 347 { 348 result ~= " "; 349 static if (!is(defaults[i] == void)) 350 result ~= "["; 351 result ~= toUpper(names[i].splitByCamelCase().join("-")); 352 static if (!is(defaults[i] == void)) 353 result ~= "]"; 354 } 355 else 356 { 357 static if (optionHasDescription!Param) 358 continue; 359 else 360 result ~= " [" ~ getSwitchText!i() ~ "]"; 361 } 362 static if (isOptionArray!Param) 363 result ~= "..."; 364 } 365 366 result ~= "\n"; 367 368 static if (haveDescriptions) 369 { 370 enum haveShorthands = anySatisfy!(optionShorthand, Params); 371 string[Params.length] selectors; 372 size_t longestSelector; 373 374 foreach (i, Param; Params) 375 static if (optionHasDescription!Param) 376 { 377 string switchText = getSwitchText!i(); 378 if (haveShorthands) 379 { 380 auto c = optionShorthand!Param; 381 if (c) 382 selectors[i] = "-%s, %s".format(c, switchText); 383 else 384 selectors[i] = " %s".format(switchText); 385 } 386 else 387 selectors[i] = switchText; 388 longestSelector = max(longestSelector, selectors[i].length); 389 } 390 391 result ~= "\nOptions:\n"; 392 foreach (i, Param; Params) 393 static if (optionHasDescription!Param) 394 { 395 result ~= wrap( 396 optionDescription!Param, 397 79, 398 " %-*s ".format(longestSelector, selectors[i]), 399 " ".replicate(2 + longestSelector + 2) 400 ); 401 } 402 } 403 404 return result; 405 } 406 407 unittest 408 { 409 void f1( 410 Switch!("Enable verbose logging", 'v') verbose, 411 Option!(int, "Number of tries") tries, 412 Option!(int, "Seconds to wait each try", "SECS") timeout, 413 string filename, 414 string output = "default", 415 string[] extraFiles = null 416 ) 417 {} 418 419 auto usage = getUsage!f1("program"); 420 assert(usage == 421 "Usage: program [OPTION]... FILENAME [OUTPUT] [EXTRA-FILES]... 422 423 Options: 424 -v, --verbose Enable verbose logging 425 --tries=N Number of tries 426 --timeout=SECS Seconds to wait each try 427 ", usage); 428 429 void f2( 430 bool verbose, 431 Option!(string[]) extraFile, 432 string filename, 433 string output = "default" 434 ) 435 {} 436 437 usage = getUsage!f2("program"); 438 assert(usage == 439 "Usage: program [--verbose] [--extra-file=STR]... FILENAME [OUTPUT] 440 ", usage); 441 } 442 443 // *************************************************************************** 444 445 /// Dispatch the command line to a type's static methods, according to the 446 /// first parameter on the given command line (the "action"). 447 /// String UDAs are used as usage documentation for generating --help output 448 /// (or when no action is specified). 449 auto funoptDispatch(alias Actions, FunOptConfig config = FunOptConfig.init)(string[] args) 450 { 451 string program = args[0]; 452 453 auto fun(string action, string[] actionArguments = []) 454 { 455 foreach (m; __traits(allMembers, Actions)) 456 { 457 enum name = m.toLower(); 458 if (name == action) 459 { 460 auto args = [getProgramName(program) ~ " " ~ action] ~ actionArguments; 461 return funopt!(__traits(getMember, Actions, m), config)(args); 462 } 463 } 464 465 throw new GetOptException("Unknown action: " ~ action); 466 } 467 468 enum actionList = genActionList!Actions(); 469 470 const FunOptConfig myConfig = (){ 471 auto c = config; 472 c.getoptConfig ~= std.getopt.config.stopOnFirstNonOption; 473 c.usageFooter = actionList ~ c.usageFooter; 474 return c; 475 }(); 476 return funopt!(fun, myConfig)(args); 477 } 478 479 private string genActionList(alias Actions)() 480 { 481 string result = "\nActions:\n"; 482 483 size_t longestAction = 0; 484 foreach (m; __traits(allMembers, Actions)) 485 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 486 longestAction = max(longestAction, m.length); 487 488 foreach (m; __traits(allMembers, Actions)) 489 static if (hasAttribute!(string, __traits(getMember, Actions, m))) 490 { 491 enum name = m.toLower(); 492 result ~= wrap( 493 //__traits(comment, __traits(getMember, Actions, m)), // https://github.com/D-Programming-Language/dmd/pull/3531 494 getAttribute!(string, __traits(getMember, Actions, m)), 495 79, 496 " %-*s ".format(longestAction, name), 497 " ".replicate(2 + longestAction + 2) 498 ); 499 } 500 501 return result; 502 } 503 504 unittest 505 { 506 struct Actions 507 { 508 @(`Perform action f1`) 509 static void f1(bool verbose) {} 510 } 511 512 funoptDispatch!Actions(["program", "f1", "--verbose"]); 513 514 assert(genActionList!Actions() == " 515 Actions: 516 f1 Perform action f1 517 "); 518 }