1 // SemiTwist Library 2 // Written in the D programming language. 3 4 module semitwist.cmdlineparser; 5 6 import std.math; 7 import std..string; 8 import std.conv; 9 import std.stdio; 10 11 public import semitwist.refbox; 12 import semitwist.util.all; 13 14 //TODO: This module's API needs a serious overhaul. 15 16 //TODO: Add "switch A implies switches B and C" 17 //TODO: Add in some good ideas from the cmd parser in tango scrapple 18 19 //TODO: Convert the following sample code into an actual sample app 20 /** 21 ----- THIS IS PROBABLY OUTDATED ----- 22 Usage: 23 24 void main(string[] args) 25 { 26 bool help; 27 bool detailhelp; 28 int myInt = 2; // Default value == 2 29 bool myBool; // Default value == bool.init (ie, false) 30 string myStr; // Default value == (string).init (ie, "") 31 32 auto cmd = new CmdLineParser(); 33 mixin(defineArg!(cmd, help, "help", "Displays a help summary and exits" )); 34 mixin(defineArg!(cmd, detailhelp, "detailhelp", "Displays a detailed help message and exits" )); 35 mixin(defineArg!(cmd, myInt, "num", "An integer")); 36 mixin(defineArg!(cmd, myBool, "flag", "A flag")); 37 mixin(defineArg!(cmd, myStr, "str", "A string")); 38 39 if(!cmd.parse(args) || help) 40 { 41 Stdout.format("{}", cmd.getUsage()); 42 return; 43 } 44 if(detailhelp) 45 { 46 Stdout.format("{}", cmd.getDetailedUsage()); 47 return; 48 } 49 50 Stdout.formatln("num: {}", myInt); 51 Stdout.formatln("flag: {}", myBool); 52 Stdout.formatln("str: {}", myStr); 53 } 54 55 Sample Command Lines (All these are equivalent): 56 > myApp.exe /num:5 /flag /str:blah 57 > myApp.exe -flag:true -str:blah -num=5 58 > myApp.exe --num=5 /flag+ "-str:blah" 59 num: 5 60 flag: true 61 str: blah 62 63 > myApp.exe "/str:Hello World" 64 num: 2 65 flag: false 66 str: Hello World 67 68 > myApp.exe /foo 69 Unknown switch: "/foo" 70 Switches: (prefixes can be '/', '-' or '--') 71 -help Displays a help summary and exits 72 -detailhelp Displays a detailed help message and exits 73 -num:<int> An integer (default: 2) 74 -flag A flag 75 -str:<string> A string 76 77 */ 78 79 enum ArgFlag 80 { 81 Optional = 0b0000_0000, 82 Required = 0b0000_0001, 83 //Unique = 0b0000_0100, 84 ToLower = 0b0000_1000, // If arg is string, the value gets converted to all lower-case (for case-insensitivity) 85 Advanced = 0b0001_0000, 86 } 87 88 template defineArg(alias cmdLineParser, string name, alias var, int flags = cast(int)ArgFlag.Optional, string desc = "") 89 //template defineArg(alias cmdLineParser, string name, alias var, ArgFlag flags = ArgFlag.Optional, string desc = "") 90 { 91 //TODO: Is there a better way to do this? Ex. "static if(typeof(var) !contained_in listOfSupportedTypes)" 92 static if(!is(typeof(var) == int ) && !is(typeof(var) == int[] ) && 93 !is(typeof(var) == bool ) && !is(typeof(var) == bool[] ) && 94 !is(typeof(var) == string) && !is(typeof(var) == string[]) ) 95 { 96 static assert(false, `Attempted to pass variable '`~var.stringof~`' of type '`~typeof(var).stringof~`' to defineArg's 'var' param.`"\n" 97 `(The type must be one of, or an array of, one of the following: 'int' 'bool' 'string')`); 98 } 99 else 100 { 101 enum defineArg = "\n"~ 102 "auto _cmdarg_refbox_"~name~" = new "~nameof!(RefBox)~"!("~typeof(var).stringof~")(&"~var.stringof~");\n"~ 103 "auto _cmdarg_"~name~" = new Arg(_cmdarg_refbox_"~name~`, "`~name~`", `~desc.stringof~`);`~"\n"~ 104 cmdLineParser.stringof~".addArg(_cmdarg_"~name~", cast(ArgFlag)("~flags.stringof~"));\n"; 105 } 106 } 107 108 template setArgAllowableValues(string name, allowableValues...) 109 { 110 enum setArgAllowableValues = 111 typeof(allowableValues[0]).stringof~"[] _cmdarg_allowablevals_"~name~";\n" 112 ~_setArgAllowableValues!(name, allowableValues) 113 ~"_cmdarg_"~name~".setAllowableValues(_cmdarg_allowablevals_"~name~");\n"; 114 } 115 116 private template _setArgAllowableValues(string name, allowableValues...) 117 { 118 static if(allowableValues.length == 0) 119 enum _setArgAllowableValues = ""; 120 else 121 enum _setArgAllowableValues = 122 "_cmdarg_allowablevals_"~name~" ~= "~allowableValues[0].stringof~";\n" 123 ~ _setArgAllowableValues!(name, allowableValues[1..$]); 124 } 125 126 //TODO? Add float, double, byte, short, long, and unsigned of each. 127 //TODO: For numeric types, make sure provided values can fit in the type. (Using "to!()"?) 128 //TODO: Think about way to (or the need to) prevent adding 129 // the same Arg instance to multiple Parsers. 130 131 class Arg 132 { 133 string name; 134 string altName; 135 string desc; 136 137 bool isSwitchless = false; 138 bool isRequired = false; 139 bool arrayUnique = false; 140 bool toLower = false; 141 bool isAdvanced = false; 142 143 private Object value; 144 private Object defaultValue; 145 private Object[] allowableValues; 146 147 bool isSet = false; 148 149 this(Object value, string name, string desc="") 150 { 151 mixin(initMember("value", "name", "desc")); 152 ensureValid(); 153 } 154 155 private void genDefaultValue() 156 { 157 if(!isRequired) 158 { 159 mixin(dupRefBox!(value, "val", defaultValue)); 160 } 161 } 162 163 // Note: AllowableValues are ignored for bool and bool[] 164 private void setAllowableValues(T)(T[] allowableValues) 165 { 166 this.allowableValues.length = 0; 167 foreach(T val; allowableValues) 168 { 169 auto box = new RefBox!(T)(); 170 box = val; 171 this.allowableValues ~= box; 172 } 173 } 174 175 void ensureValid() 176 { 177 //TODO: arrayMultiple and arrayUnique cannot both be set 178 //TODO: ensure each of allowableValues is the same type as value 179 //TODO: enforce allowableValues on defaultValue 180 //TODO: reflect allowableValues in generated help 181 182 if(!isKnownRefBox!(value)) 183 { 184 throw new Exception("Param to Arg contructor must be "~RefBox.stringof~", where T is int, bool or string or an array of such types."); 185 } 186 187 void ensureValidName(string name) 188 { 189 if(!CmdLineParser.isValidArgName(name)) 190 throw new Exception(`Tried to define an invalid arg name: "%s". Arg names must be "[a-zA-Z0-9_?]*"`.format(name)); 191 } 192 ensureValidName(name); 193 ensureValidName(altName); 194 } 195 } 196 197 class CmdLineParser 198 { 199 private Arg[] args; 200 private Arg[string] argLookup; 201 202 private bool switchlessArgExists=false; 203 private size_t switchlessArg; 204 205 mixin(getter!(bool, "success")); 206 mixin(getter!(string, "errorMsg")); 207 208 private enum Prefix 209 { 210 Invalid, DoubleDash, SingleDash, Slash 211 } 212 213 enum ParseArgResult 214 { 215 Done, NotFound, Error 216 } 217 218 static bool isValidArgName(string name) 219 { 220 foreach(char c; name) 221 { 222 if(!inPattern(c, "a-zA-Z0-9") && c != '_' && c != '?') 223 return false; 224 } 225 return true; 226 } 227 228 private void ensureValid() 229 { 230 foreach(Arg arg; args) 231 { 232 arg.ensureValid(); 233 } 234 } 235 236 private void populateLookup() 237 { 238 foreach(Arg arg; args) 239 { 240 addToArgLookup(arg.name, arg); 241 242 if(arg.altName != "") 243 addToArgLookup(arg.altName, arg); 244 } 245 } 246 247 private void genDefaultValues() 248 { 249 foreach(Arg arg; args) 250 { 251 arg.genDefaultValue(); 252 } 253 } 254 255 public void addArg(Arg arg, ArgFlag flags = ArgFlag.Optional) 256 { 257 args ~= arg; 258 259 bool isSwitchless = arg.name == ""; 260 bool isRequired = ((flags & ArgFlag.Required) != 0); 261 bool toLower = ((flags & ArgFlag.ToLower) != 0); 262 bool isAdvanced = ((flags & ArgFlag.Advanced) != 0); 263 264 mixin(initMemberTo("arg", "isRequired", "toLower", "isAdvanced")); 265 266 if(isSwitchless) 267 { 268 if(switchlessArgExists) 269 args[switchlessArg].isSwitchless = false; 270 271 switchlessArgExists = true; 272 switchlessArg = args.length-1; 273 arg.isSwitchless = true; 274 } 275 } 276 277 private void addToArgLookup(string name, Arg argDef) 278 { 279 if(name in argLookup) 280 throw new Exception(`Argument name "%s" defined more than once.`.format(name)); 281 282 argLookup[name] = argDef; 283 } 284 285 private void splitArg(string fullArg, out Prefix prefix, out string name, out string suffix) 286 { 287 string argNoPrefix; 288 289 // Get prefix 290 if(fullArg.length > 2 && fullArg[0..2] == "--") 291 { 292 argNoPrefix = fullArg[2..$]; 293 prefix = Prefix.DoubleDash; 294 } 295 else if(fullArg.length > 1) 296 { 297 argNoPrefix = fullArg[1..$]; 298 299 if(fullArg[0] == '-') 300 prefix = Prefix.SingleDash; 301 else if(fullArg[0] == '/') 302 prefix = Prefix.Slash; 303 else 304 { 305 prefix = Prefix.Invalid; 306 argNoPrefix = fullArg; 307 } 308 } 309 310 // Get suffix and arg name 311 version(GNU) 312 { 313 auto tmp = [locate(argNoPrefix, ':'), locate(argNoPrefix, '+')]; 314 tmp ~= locate(argNoPrefix, '-'); 315 auto suffixIndex = reduce!"a<b?a:b"(tmp); 316 } 317 else 318 { 319 auto suffixIndex = reduce!"a<b?a:b"( [ 320 locate(argNoPrefix, ':'), 321 locate(argNoPrefix, '+'), 322 locate(argNoPrefix, '-') 323 ] ); 324 } 325 name = argNoPrefix[0..suffixIndex]; 326 suffix = suffixIndex < argNoPrefix.length ? 327 argNoPrefix[suffixIndex..$] : ""; 328 } 329 330 //TODO: Detect and error when numerical arg is passed an out-of-range value 331 private ParseArgResult parseArg(string cmdArg, string cmdName, string suffix) 332 { 333 ParseArgResult ret = ParseArgResult.Error; 334 335 void HandleMalformedArgument() 336 { 337 _errorMsg ~= `Invalid value: "%s"`.formatln(cmdArg); 338 ret = ParseArgResult.Error; 339 } 340 341 if(cmdName in argLookup) 342 { 343 auto argDef = argLookup[cmdName]; 344 345 // For some reason, unbox can't see Arg's private member "value" 346 auto argDefValue = argDef.value; 347 mixin(unbox!(argDefValue, "val")); 348 349 ret = ParseArgResult.Done; 350 if(argDef.isSet && (valAsBool || valAsStr || valAsInt)) 351 { 352 _errorMsg ~= `Switch given twice: "%s"`.formatln(cmdArg); 353 ret = ParseArgResult.Error; 354 } 355 else if(valAsBool || valAsBools) 356 { 357 bool val; 358 bool isMalformed=false; 359 switch(suffix) 360 { 361 case "": 362 case "+": 363 case ":+": 364 case ":true": 365 val = true; 366 break; 367 case "-": 368 case ":-": 369 case ":false": 370 val = false; 371 break; 372 default: 373 HandleMalformedArgument(); 374 isMalformed = true; 375 break; 376 } 377 378 if(!isMalformed) 379 { 380 if(valAsBool) 381 valAsBool = val; 382 else 383 valAsBools = valAsBools() ~ val; 384 } 385 } 386 else if(valAsStr || valAsStrs) 387 { 388 string val; 389 if(suffix.length > 1 && suffix[0] == ':') 390 { 391 val = strip(suffix[1..$]); 392 393 if(argDef.toLower) 394 val = val.toLower(); 395 396 //TODO: DRY this 397 if(argDef.allowableValues.length > 0) 398 { 399 bool matchFound=false; 400 foreach(Object allowedObj; argDef.allowableValues) 401 { 402 mixin(unbox!(allowedObj, "allowedVal")); 403 if(val == allowedValAsStr) 404 { 405 matchFound = true; 406 break; 407 } 408 } 409 if(!matchFound) 410 HandleMalformedArgument(); 411 } 412 413 if(valAsStr) 414 valAsStr = val; 415 else 416 valAsStrs = valAsStrs() ~ val; 417 } 418 else 419 HandleMalformedArgument(); 420 } 421 else if(valAsInt || valAsInts) 422 { 423 int val; 424 size_t parseAte; 425 if(suffix.length > 1 && suffix[0] == ':') 426 { 427 string trimmedSuffix = strip(suffix[1..$]); 428 auto copyTrimmedSuffix = trimmedSuffix; 429 val = std.conv.parse!int(copyTrimmedSuffix); 430 parseAte = trimmedSuffix.length - copyTrimmedSuffix.length; 431 //val = cast(int)convInt.parse(trimmedSuffix, 0, &parseAte); 432 if(parseAte == trimmedSuffix.length) 433 { 434 //TODO: DRY this 435 if(argDef.allowableValues.length > 0) 436 { 437 bool matchFound=false; 438 foreach(Object allowedObj; argDef.allowableValues) 439 { 440 mixin(unbox!(allowedObj, "allowedVal")); 441 if(val == allowedValAsInt) 442 { 443 matchFound = true; 444 break; 445 } 446 } 447 if(!matchFound) 448 HandleMalformedArgument(); 449 } 450 451 if(valAsInt) 452 valAsInt = val; 453 else 454 valAsInts = valAsInts() ~ val; 455 } 456 else 457 HandleMalformedArgument(); 458 } 459 else 460 HandleMalformedArgument(); 461 } 462 else 463 throw new Exception("Internal Error: Failed to process an Arg.value type that hasn't been set as unsupported."); 464 465 argDef.isSet = true; 466 } 467 else 468 { 469 _errorMsg ~= `Unknown switch: "%s"`.formatln(cmdArg); 470 ret = ParseArgResult.NotFound; 471 } 472 473 return ret; 474 } 475 476 //TODO: response file 477 478 public bool parse(string[] args) 479 { 480 bool error=false; 481 482 ensureValid(); 483 populateLookup(); 484 genDefaultValues(); 485 486 foreach(string argStr; args[1..$]) 487 { 488 string suffix; 489 string argName; 490 Prefix prefix; 491 492 splitArg(argStr, prefix, argName, suffix); 493 if(prefix == Prefix.Invalid) 494 { 495 if(switchlessArgExists) 496 { 497 argName = this.args[switchlessArg].name; 498 suffix = ":"~argStr; 499 } 500 else 501 { 502 _errorMsg ~= `Unexpected value: "%s"`.formatln(argStr); 503 error = true; 504 continue; 505 } 506 } 507 //mixin(traceVal!("argStr ", "prefix ", "argName", "suffix ")); 508 509 auto result = parseArg(argStr, argName, suffix); 510 switch(result) 511 { 512 case ParseArgResult.Done: 513 continue; 514 515 case ParseArgResult.Error: 516 case ParseArgResult.NotFound: 517 error = true; 518 break; 519 520 default: 521 throw new Exception("Unexpected ParseArgResult: (%s)".format(result)); 522 } 523 } 524 525 if(!verify()) 526 error = true; 527 528 _success = !error; 529 return _success; 530 } 531 532 private bool verify() 533 { 534 bool error=false; 535 536 foreach(Arg arg; this.args) 537 { 538 if(arg.isRequired && !arg.isSet) 539 { 540 _errorMsg ~= 541 `Missing switch: %s (%s)` 542 .formatln( 543 arg.name=="" ? "<"~getArgTypeName(arg)~">" : arg.name, 544 arg.desc 545 ); 546 error = true; 547 } 548 } 549 550 return !error; 551 } 552 553 //TODO: Make function to get the maximum length of the arg names 554 555 private string switchTypesMsg = 556 `Switch types: 557 flag (default): 558 Set s to true: -s -s+ -s:true 559 Set s to false: -s- -s:false 560 Default value: false (unless otherwise noted) 561 562 text: 563 Set s to "Hello": -s:Hello 564 Default value: "" (unless otherwise noted) 565 Case-sensitive unless otherwise noted. 566 567 num: 568 Set s to 3: -s:3 569 Default value: 0 (unless otherwise noted) 570 571 If "[]" appears at the end of the type, 572 this means multiple values are accepted. 573 Example: 574 -s:<text[]>: -s:file1 -s:file2 -s:anotherfile 575 `; 576 577 string getArgTypeName(Arg arg) 578 { 579 string typeName = getRefBoxTypeName(arg.value); 580 return 581 (typeName == "string" )? "text" : 582 (typeName == "string[]")? "text[]" : 583 (typeName == "char[]" )? "text" : 584 (typeName == "char[][]")? "text[]" : 585 (typeName == "bool" )? "flag" : 586 (typeName == "bool[]" )? "flag[]" : 587 (typeName == "int" )? "num" : 588 (typeName == "int[]" )? "num[]" : 589 typeName; 590 } 591 592 //TODO: Fix word wrapping 593 string getUsage(int nameColumnWidth=20) 594 { 595 string ret; 596 string indent = " "; 597 string basicArgStr; 598 string advancedArgStr; 599 600 ret ~= 601 "Switches:\n"~ 602 "(Prefixes can be '/', '-' or '--')\n"~ 603 "('[]' means multiple switches are accepted)\n"; //TODO: Only show this line if such a switch exists 604 605 foreach(Arg arg; args) 606 { 607 string* argStr = arg.isAdvanced? &advancedArgStr : &basicArgStr; 608 609 // For some reason, unbox can't see Arg's private member "defaultValue" 610 auto argDefaultValue = arg.defaultValue; 611 mixin(unbox!(argDefaultValue, "val")); 612 613 string defaultVal; 614 if(valAsInt) 615 defaultVal = "%s".format(valAsInt()); 616 else if(valAsBool) 617 defaultVal = valAsBool() ? "true" : ""; 618 else if(valAsStr) 619 defaultVal = valAsStr() == "" ? "" : `"%s"`.format(valAsStr()); 620 621 string defaultValStr = defaultVal == "" ? 622 "" : " (default: %s)".format(defaultVal); 623 624 string requiredStr = arg.isRequired ? 625 "(Required) " : ""; 626 627 string argType = "<"~getArgTypeName(arg)~">"; 628 string argSuffix = valAsBool ? "" : (":"~argType); 629 630 string argName; 631 if(arg.name=="") 632 argName = argType; 633 else 634 argName = "-"~arg.name~argSuffix; 635 if(arg.altName != "") 636 argName ~= ", -"~arg.altName~argSuffix; 637 638 string nameColumnWidthStr = "%s".format(nameColumnWidth); 639 *argStr ~= format("%s%-"~nameColumnWidthStr~"s%s%s\n", 640 indent, argName~" ", requiredStr~arg.desc, defaultValStr); 641 } 642 if(basicArgStr != "" && advancedArgStr != "") 643 { 644 basicArgStr = "\nBasic: \n"~basicArgStr; 645 advancedArgStr = "\nAdvanced: \n"~advancedArgStr; 646 } 647 return ret~basicArgStr~advancedArgStr; 648 } 649 650 string getDetailedUsage() 651 { 652 string ret; 653 string indent = " "; 654 string basicArgStr; 655 string advancedArgStr; 656 657 ret ~= "Switches: (prefixes can be '/', '-' or '--')\n"; 658 foreach(Arg arg; args) 659 { 660 string* argStr = arg.isAdvanced? &advancedArgStr : &basicArgStr; 661 662 string argName = arg.isSwitchless? "" : "-"~arg.name; 663 if(arg.altName != "") 664 argName ~= ", -"~arg.altName; 665 if(!arg.isSwitchless || arg.altName != "") 666 argName ~= " "; 667 668 // For some reason, unbox can't see Arg's private member "defaultValue" 669 auto argDefaultValue = arg.defaultValue; 670 mixin(unbox!(argDefaultValue, "val")); 671 672 string defaultVal; 673 string requiredStr; 674 string toLowerStr; 675 string switchlessStr; 676 string advancedStr; 677 678 if(valAsInt) 679 defaultVal = "%s".format(valAsInt()); 680 else if(valAsInts) 681 defaultVal = "%s".format(valAsInts()); 682 else if(valAsBool) 683 defaultVal = "%s".format(valAsBool()); 684 else if(valAsBools) 685 defaultVal = "%s".format(valAsBools()); 686 else if(valAsStr) 687 defaultVal = `"%s"`.format(valAsStr()); 688 else if(valAsStrs) //TODO: Change this one from [ blah ] to [ "blah" ] 689 defaultVal = "%s".format(valAsStrs()); 690 691 defaultVal = arg.isRequired ? "" : ", Default: "~defaultVal; 692 requiredStr = arg.isRequired ? "Required" : "Optional"; 693 toLowerStr = arg.toLower ? ", Case-Insensitive" : ""; 694 switchlessStr = arg.isSwitchless ? ", Nameless" : ""; 695 advancedStr = arg.isAdvanced ? ", Advanced" : ", Basic"; 696 697 *argStr ~= "\n"; 698 *argStr ~= format("%s(%s), %s%s%s%s%s\n", 699 argName, getArgTypeName(arg), 700 requiredStr, switchlessStr, toLowerStr, advancedStr, defaultVal); 701 *argStr ~= format("%s\n", arg.desc); 702 } 703 ret ~= basicArgStr; 704 ret ~= advancedArgStr; 705 ret ~= "\n"; 706 ret ~= switchTypesMsg; 707 return ret; 708 } 709 }