1 /** 2 Handles program options which are arguments passed with a leading `-` or `--` and followed by a value 3 4 Features: 5 6 <li> Input validation 7 <li> Customize seperators for associative array args 8 <li> Supports environment variables 9 <li> Supports default values 10 <li> Supports custom types that have a constructor that is called with a string 11 <li> You can supply custom types and they will be called with a string that you can parse 12 13 Enhancements_over_std.getopt: 14 15 <li> `getopt(args)` is destructive on args. 16 <li> You cannot create your getopts and the parse later, which in combination with try/catch leads to awkward code 17 <li> `getopt` doesn't accept `$ ./program -p 3`. For short opts, you have to do `$ ./program -p3`. 18 <li> `getopt` doesn't allow case-sensitive short name and a case-insensitive long name 19 <li> `getopt` will initialize an array type with the default values AND what the program arg was. 20 <li> You cannot assign values to bundled short args, they are only incrementable 21 <li> There is no way to handle what happens with duplicate arguments 22 */ 23 module dcli.program_options; 24 25 /// 26 unittest { 27 import std.file: thisExePath; 28 import std.process: environment; 29 30 environment["OPT_4"] = "22"; 31 32 auto args = [ 33 thisExePath, // Program name should be ignored 34 "program_name", // Unknown argument, there's a handler for stray arguments 35 "--opt1", "value1", // "--arg value" format 36 "-b", "1", // "-a 1" format, case sensitive short name by default 37 "--Opt3=2", // "--arg=value" format, case insesitive long name by default 38 // "--opt4", // Set by an envaronment variable 39 "--OPT5", "4", // first value of an int array 40 "--opt5", "5", // second value of an int array 41 // "--opt6", // Not set, will be give default value of int array 42 "--opt7", "9", // Also an array 43 "--unknown", "ha", // Unknown option and value 44 "--opt8=two", // An enum vaue 45 "-i", "-j", "--incremental", "--opt9", // Option aliasing 46 "--opt10", "11", // A validated, must be greater than 10 47 "--opt11", "3,4,5", // Array format "--arg=v0,v1,v2" 48 "--opt12", "1=2::3=4::5=6", // Associative array with custom seperator (Default is ",") 49 "--opt13", "verbose", // A custom parsed value - opt13 is an int 50 "-xyz=-7", // Setting multiple, bundleable options at once 51 "--opt1", "value2", // Uses duplication policy to be ignored 52 "--opt14", "1,2", // Parses to a Custom type 53 "--opt15", // Boolean, no value 54 "--", // Args after this are ignored 55 "extra", 56 ]; 57 58 enum Enum { one, two, } 59 static struct Custom { 60 int x; 61 int y; 62 this(int a, int b) { 63 x = a; 64 y = b; 65 } 66 this(string str) { 67 import std..string: split; 68 import std.conv: to; 69 auto parts = str.split(","); 70 x = parts[0].to!int; 71 y = parts[1].to!int; 72 } 73 } 74 75 auto options = ProgramOptions!( 76 Option!("opt1", string) 77 .shortName!"a" 78 .description!"This is the description for option 1" 79 .duplicatePolicy!(OptionDuplicatePolicy.firstOneWins), 80 Option!("opt2", int) 81 .shortName!"b" 82 .description!"This is the description for option 2", 83 Option!("opt3", int) 84 .shortName!"B" 85 .description!( 86 `There are three kinds of comments: 87 1. Something rather sinister 88 2. And something else that's not so sinister` 89 ), 90 Option!("opt4", int) 91 .defaultValue!3 92 .environmentVar!"OPT_4" 93 .description!"THis is one that takes an env var", 94 Option!("opt5", int[]) 95 .environmentVar!"OPT_5" 96 .description!"THis is one that takes an env var as well", 97 Option!("opt6", int[]) 98 .defaultValue!([6, 7, 8]), 99 Option!("opt7", float[]) 100 .defaultValue!([1, 2]), 101 Option!("opt8", Enum), 102 Option!("opt9", int) 103 .shortName!"i|j" 104 .longName!"incremental|opt9" 105 .incremental!true 106 .description!"sets some level incremental thingy", 107 Option!("opt10", int) 108 .validator!(a => a > 10), 109 Option!("opt11", int[]), 110 Option!("opt12", int[int]) 111 .separator!"::", 112 Option!("opt13", int) 113 .parser!((value) { 114 if (value == "verbose") return 7; 115 return -1; 116 }), 117 Option!("b0", int) 118 .shortName!"x", 119 Option!("b1", int) 120 .shortName!"y", 121 Option!("b2", int) 122 .shortName!"z", 123 Option!("opt14", Custom), 124 Option!("opt15", bool), 125 Option!("opt16", bool) 126 .longName!"" 127 .environmentVar!"OPT_16" 128 .description!"THis one only takes and envornment variable and cant be set with any flags", 129 )(); 130 131 string[] unknownArgs; 132 options.unknownArgHandler = (string name) { 133 unknownArgs ~= name; 134 return false; 135 }; 136 137 assert(options.parse(args) == ["extra"]); 138 assert(unknownArgs == ["program_name", "--unknown", "ha"]); 139 140 assert(options.opt1 == "value1"); 141 assert(options.opt2 == 1); 142 assert(options.opt3 == 2); 143 assert(options.opt4 == 22); 144 assert(options.opt5 == [4, 5]); 145 assert(options.opt6 == [6, 7, 8]); 146 assert(options.opt7 == [9]); 147 assert(options.opt8 == Enum.two); 148 assert(options.opt9 == 4); 149 assert(options.opt10 > 10); 150 assert(options.opt11 == [3, 4, 5]); 151 assert(options.opt12 == [1: 2, 3: 4, 5: 6]); 152 assert(options.opt13 == 7); 153 assert(options.b0 == -7); 154 assert(options.b1 == -7); 155 assert(options.b2 == -7); 156 assert(options.opt14 == Custom(1, 2)); 157 assert(options.opt15 == true); 158 159 assert(options.helpText == 160 `Options: 161 -a --opt1 This is the description for option 1 162 -b --opt2 This is the description for option 2 163 -B --opt3 There are three kinds of comments: 164 1. Something rather sinister 165 2. And something else that's not so sinister 166 --opt4 THis is one that takes an env var 167 --opt5 THis is one that takes an env var as well 168 --opt6 169 --opt7 170 --opt8 171 -i --incremental sets some level incremental thingy 172 --opt10 173 --opt11 174 --opt12 175 --opt13 176 -x --b0 177 -y --b1 178 -z --b2 179 --opt14 180 --opt15 181 182 Environment Vars: 183 OPT_4 See: --opt4 184 OPT_5 See: --opt5 185 OPT_16 THis one only takes and envornment variable and cant be set with any flags` 186 ); 187 } 188 189 // version = dcli_programoptions_debug; 190 191 private void debug_print(Args...)(Args args, int line = __LINE__, string file = __FILE__) @trusted @nogc { 192 version(dcli_programoptions_debug) { 193 debug { 194 import std.stdio: writeln; 195 import std.path: baseName, stripExtension, dirName, pathSplitter; 196 import std.range: array; 197 auto stripped = baseName(stripExtension(file)); 198 if (stripped == "package") { 199 stripped = pathSplitter(dirName(file)).array[$ - 1] ~ "/" ~ stripped; 200 } 201 writeln("[", stripped, ":", line, "] : ", args); 202 } 203 } 204 } 205 206 /** 207 The duplication policy can be passed to a `ProgramOptions` `Option.duplicatePolicy` 208 */ 209 enum OptionDuplicatePolicy { 210 /// Throws a `DuplicateProgramArgument` 211 reject, 212 /// The last argument determines the value of the option 213 lastOneWins, 214 /// The first argument determines the value of the option 215 firstOneWins 216 } 217 218 /** 219 Represents one program options. One of more of these can be given to 220 a `ProgramOptions` object as template arguments. 221 222 Params: 223 varName = The name of the variable that will hold the value of the option. 224 This value represents the default long name of the program option. 225 T = The type of this variable 226 227 Named_optional_arguments: 228 229 A number of named optional arguments can be given to an `Option` for e.g.: 230 --- 231 ProgramOptions!( 232 Option!("optionName", bool).shortName!"o"; 233 ); 234 --- 235 236 This will create an options object that can parse `--optionName` and `-o` on the command line. 237 238 The following named optional arguments are available: 239 240 <li>`shortName`: `string` - the short name for the option 241 <li>`longName`: `string` - the long name of the option if different from the variable name 242 <li>`defaultValue`: `T` - the default value if not supplied 243 <li>`description`: `string` - description for help message 244 <li>`environmentVar`: `string` - the name of the envrionemtn var that can set this option if present 245 <li>`caseSensitiveLongName`: `bool` - true if long name is case sensitive 246 <li>`caseSensitiveShortName`: `bool` - true if short name is case sensitive 247 <li>`incremental`: `bool` - true if this option represents an incremental value 248 <li>`validator`: `bool function(T)` - a function that will be given the value and must return true if the value is valid 249 <li>`seperator`: `string` - the seperator used to parse associative array values 250 <li>`parser`: `T function(string)` - a function that must return a T after parsing the string 251 <li>`bundleable`: `bool` - true if this short option can be bundled with other short options 252 <li>`duplicatePolicy`: `OptionDuplicatePolicy` - what to do when the option is encoutered for a second time 253 */ 254 template Option(string varName, T) { 255 alias Option = OptionImpl!(varName, T); 256 } 257 258 private template OptionImpl( 259 string _varName, 260 T, 261 string _longName = _varName, 262 string _shortName = null, 263 T _defaultValue = T.init, 264 string _description = null, 265 string _environmentVar = null, 266 bool _caseSensitiveLongName = false, 267 bool _caseSensitiveShortName = true, 268 bool _incremental = false, 269 alias _validator = null, 270 string _separator = ",", 271 alias _parser = null, 272 bool _bundleable = true, 273 OptionDuplicatePolicy _duplicatePolicy = OptionDuplicatePolicy.reject, 274 ) if (_varName.length > 0) { 275 276 import std.typecons: Flag; 277 import bolts: isUnaryOver; 278 import std.traits: ReturnType; 279 import ddash.range: frontOr; 280 281 alias VarName = _varName; 282 alias Type = T; 283 alias LongName = _longName; 284 alias ShortName = _shortName; 285 alias DefaultValue = _defaultValue; 286 alias Description = _description; 287 alias EnvironmentVar = _environmentVar; 288 alias CaseSensitiveLongName = _caseSensitiveLongName; 289 alias CaseSensitiveShortName = _caseSensitiveShortName; 290 alias Incremental = _incremental; 291 alias Validator = _validator; 292 alias Separator = _separator; 293 alias Parser = _parser; 294 alias Bundleable = _bundleable; 295 alias DuplicatePolicy = _duplicatePolicy; 296 297 static if (_incremental) { 298 import std.traits: isIntegral; 299 static assert( 300 isIntegral!T, 301 "Cannot create incremental option '" ~ VarName ~ "' of type " ~ T.stringof ~ ". Incrementals must by integral type." 302 ); 303 } 304 305 import std.array: split; 306 private immutable NormalizedName = (LongName ~ "|" ~ ShortName ~ "|" ~ VarName).split("|").frontOr(""); 307 private immutable PrimaryLongName = LongName.split("|").frontOr(""); 308 private immutable PrimaryShortName = ShortName.split("|").frontOr(""); 309 310 public alias longName(string value) = OptionImpl!( 311 VarName, Type, value, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 312 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 313 ); 314 315 public alias shortName(string value) = OptionImpl!( 316 VarName, Type, LongName, value, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 317 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 318 ); 319 320 public alias defaultValue(T value) = OptionImpl!( 321 VarName, Type, LongName, ShortName, value, Description, EnvironmentVar, CaseSensitiveLongName, 322 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 323 ); 324 325 public template description(string value) if (value.length > 0) { 326 alias description = OptionImpl!( 327 VarName, Type, LongName, ShortName, DefaultValue, value, EnvironmentVar, CaseSensitiveLongName, 328 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 329 ); 330 } 331 332 public template environmentVar(string value) if (value.length > 0) { 333 alias environmentVar = OptionImpl!( 334 VarName, Type, LongName, ShortName, DefaultValue, Description, value, CaseSensitiveLongName, 335 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 336 ); 337 } 338 339 public alias caseSensitiveLongName(bool value) = OptionImpl!( 340 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, value, 341 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 342 ); 343 344 public alias caseSensitiveShortName(bool value) = OptionImpl!( 345 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 346 value, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 347 ); 348 349 public template incremental(bool value) { 350 alias incremental = OptionImpl!( 351 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 352 CaseSensitiveShortName, value, Validator, Separator, Parser, Bundleable, DuplicatePolicy, 353 ); 354 } 355 356 public static template validator(alias value) if (isUnaryOver!(value, T)) { 357 import std.functional: unaryFun; 358 alias validator = OptionImpl!( 359 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 360 CaseSensitiveShortName, Incremental, unaryFun!value, Separator, Parser, Bundleable, DuplicatePolicy, 361 ); 362 } 363 364 public template separator(string value) if (value.length > 0) { 365 alias separator = OptionImpl!( 366 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 367 CaseSensitiveShortName, Incremental, Validator, value, Parser, Bundleable, DuplicatePolicy, 368 ); 369 } 370 371 public static template parser(alias value) { 372 alias parser = OptionImpl!( 373 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 374 CaseSensitiveShortName, Incremental, Validator, Separator, value, Bundleable, DuplicatePolicy, 375 ); 376 } 377 378 public template bundleable(bool value) { 379 alias bundleable = OptionImpl!( 380 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 381 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, value, DuplicatePolicy, 382 ); 383 } 384 385 public template duplicatePolicy(OptionDuplicatePolicy value) { 386 alias duplicatePolicy = OptionImpl!( 387 VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName, 388 CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, value, 389 ); 390 } 391 392 private bool isMatch(string name, Flag!"shortName" isShortName) { 393 import std.uni: toLower; 394 import std.array: split; 395 import std.algorithm: any; 396 if (isShortName) { 397 static if (CaseSensitiveShortName) { 398 auto lhs = ShortName; 399 auto rhs = name; 400 } else { 401 auto lhs = ShortName.toLower; 402 auto rhs = name.toLower; 403 } 404 return lhs.split("|").any!(a => a == rhs); 405 } else { 406 static if (CaseSensitiveLongName) { 407 auto lhs = LongName; 408 auto rhs = name; 409 } else { 410 auto lhs = LongName.toLower; 411 auto rhs = name.toLower; 412 } 413 return lhs.split("|").any!(a => a == rhs); 414 } 415 } 416 } 417 418 /// Thrown when an error occurs in parsing 419 class ProgramOptionException: Exception { 420 this(string msg, string file = __FILE__, size_t line = __LINE__) { 421 super(msg, file, line); 422 } 423 } 424 425 /// Occurs when an argument is incorrectly formatted 426 class MalformedProgramArgument: ProgramOptionException { 427 this(string msg, string file = __FILE__, size_t line = __LINE__) { 428 super(msg, file, line); 429 } 430 } 431 432 /// Occurs when an argument does not validate 433 class InvalidProgramArgument: ProgramOptionException { 434 this(string msg, string file = __FILE__, size_t line = __LINE__) { 435 super(msg, file, line); 436 } 437 } 438 439 /// Occurs if there's a duplicate argument 440 class DuplicateProgramArgument: ProgramOptionException { 441 this(string msg, string file = __FILE__, size_t line = __LINE__) { 442 super(msg, file, line); 443 } 444 } 445 446 /// Occurs when a program argument is missing 447 class MissingProgramArgument: ProgramOptionException { 448 this(string msg, string file = __FILE__, size_t line = __LINE__) { 449 super(msg, file, line); 450 } 451 } 452 453 package(dcli) template isProgramOptions(T) { 454 import std.traits: isInstanceOf; 455 enum isProgramOptions = isInstanceOf!(ProgramOptions, T); 456 } 457 458 /** 459 You can configure a `ProgramOptions` object with a number of `Option`s and then use it to parse 460 and array of command line arguments. 461 462 The object will generate its member variables from the `Option`s you pass in, for e.g. 463 464 --- 465 auto opts = ProgramOptions!(Option!("one", int), Option!("two", string[])); 466 static assert(is(typeof(opts.one) == int)); 467 static assert(is(typeof(opts.two) == string[])); 468 --- 469 470 All the member varibles will have their `init` values unless you specify a `defaultValue` for 471 an `Option`. 472 473 Params: 474 Options = 1 or more `Option` objects, each representing one command line argument 475 */ 476 struct ProgramOptions(Options...) if (Options.length > 0) { 477 478 import std.typecons: Flag, Tuple, tuple, Yes, No; 479 import optional; 480 481 static foreach (I, opt; Options) { 482 mixin("opt.Type " ~ opt.VarName ~ "= opt.DefaultValue;"); 483 } 484 485 private bool[string] encounteredOptions; 486 487 Optional!(bool delegate(string)) unknownArgHandler; 488 489 private enum AssignResult { 490 setValueUsed, 491 setValueUnused, 492 noMatch 493 } 494 495 /* 496 Why is this static you may ask... because of D's 'delegates can only have one context pointer' thing: 497 498 https://www.bountysource.com/issues/1375082-cannot-use-delegates-as-parameters-to-non-global-template 499 500 The error would be: 501 Error: template instance `tryAssign!()` cannot use local Option!(..., __lambda2) 502 as parameter to non-global template tryAssign(alias option)(string assumedValue) 503 504 So making this a static removes the need for an extra context pointer 505 */ 506 private static AssignResult tryAssign(alias option, T)( 507 ref T self, 508 string assumedValue, 509 Flag!"ignoreDuplicate" ignoreDuplicate = No.ignoreDuplicate, 510 ) { 511 import std.traits: isNarrowString, isArray, isAssociativeArray; 512 import std.conv: to; 513 import std.array: split; 514 import std.algorithm: filter; 515 516 // If this is the first encounter, we also set the value to T.init 517 bool isDuplicate = false; 518 if (!ignoreDuplicate) { 519 auto p = option.VarName in self.encounteredOptions; 520 if (p !is null && !*p) { 521 *p = true; 522 mixin("self."~option.VarName ~ " = option.Type.init;"); 523 } else { 524 isDuplicate = true; 525 } 526 } 527 528 static if (!(isArray!(option.Type) && !isNarrowString!(option.Type)) && !isAssociativeArray!(option.Type) && !option.Incremental) { 529 static if (option.DuplicatePolicy == OptionDuplicatePolicy.firstOneWins) { 530 if (isDuplicate) { 531 // Flags are special. They may or may not consume the value depending on if consuming 532 // the value succeeds or not 533 static if (is(option.Type == bool)) { 534 try { 535 assumedValue.to!bool; 536 } catch (Exception) { 537 return AssignResult.setValueUnused; 538 } 539 } 540 return AssignResult.setValueUsed; 541 } 542 } 543 544 static if (option.DuplicatePolicy == OptionDuplicatePolicy.reject) { 545 if (isDuplicate) { 546 throw new DuplicateProgramArgument( 547 "Duplicate program option argument '" ~ assumedValue ~ "' not allowed for option '" ~ option.NormalizedName ~ "'" 548 ); 549 } 550 } 551 } 552 553 auto tryParseValidate(T = option.Type)(string assumedValue) { 554 import bolts: isNullType; 555 T value; 556 static if (!isNullType!(option.Parser)) { 557 value = option.Parser(assumedValue); 558 } else { 559 static assert( 560 __traits(compiles, { assumedValue.to!(T); }), 561 "Cannot convert program arg to type '" ~ T.stringof ~ "'. Maybe type is missing a this(string) contructor?" 562 ); 563 value = assumedValue.to!(T); 564 } 565 static if (!isNullType!(option.Validator)) { 566 if (!option.Validator(value)) { 567 throw new InvalidProgramArgument("Option '" ~ option.NormalizedName ~ "' got invalid value: " ~ assumedValue); 568 } 569 } 570 return value; 571 } 572 573 AssignResult result = AssignResult.setValueUsed; 574 static if (option.Incremental) { 575 mixin("self."~option.VarName ~ " += 1;"); 576 result = AssignResult.setValueUnused; 577 } else static if (is(option.Type == bool)) { 578 mixin("self."~option.VarName ~ " = true;"); 579 try { 580 mixin("self."~option.VarName ~ " = " ~ "assumedValue.to!bool;"); 581 } catch (Exception) { 582 result = AssignResult.setValueUnused; 583 } 584 } else static if (isArray!(option.Type) && !isNarrowString!(option.Type)) { 585 import std.range: ElementType; 586 auto values = assumedValue.split(option.Separator).filter!"a.length"; 587 foreach (str; values) { 588 auto value = tryParseValidate!(ElementType!(option.Type))(str); 589 mixin("self."~option.VarName ~ " ~= value;"); 590 } 591 } else static if (isAssociativeArray!(option.Type)) { 592 auto values = assumedValue.split(option.Separator).filter!"a.length"; 593 foreach (str; values) { 594 auto parts = str.split("="); 595 if (!parts.length == 2) { 596 return AssignResult.noMatch; 597 } 598 import std.traits: KeyType, ValueType; 599 auto key = parts[0].to!(KeyType!(option.Type)); 600 auto value = parts[1].to!(ValueType!(option.Type)); 601 mixin("self."~option.VarName ~ "[key] = value;"); 602 } 603 } else { 604 import bolts: isNullType; 605 auto value = tryParseValidate(assumedValue); 606 mixin("self."~option.VarName ~ " = value;"); 607 } 608 return result; 609 } 610 611 private AssignResult trySet(string name, string value, Flag!"shortName" shortName) { 612 import std.conv: ConvException; 613 try { 614 static foreach (opt; Options) { 615 if (opt.isMatch(name, shortName)) { 616 return tryAssign!(opt)(this, value); 617 } 618 } 619 } catch (ConvException ex) { 620 throw new MalformedProgramArgument( 621 "Could not set '" ~ name ~ "' to '" ~ value ~ "' - " ~ ex.msg 622 ); 623 } 624 return AssignResult.noMatch; 625 } 626 627 private bool isBundleableShortName(string str) { 628 import std.typecons: Yes; 629 static foreach (opt; Options) { 630 if (opt.isMatch(str, Yes.shortName)) { 631 return opt.Bundleable; 632 } 633 } 634 return false; 635 } 636 637 private bool isShortName(string str) { 638 import std.typecons: Yes; 639 static foreach (opt; Options) { 640 if (opt.isMatch(str, Yes.shortName)) { 641 return true; 642 } 643 } 644 return false; 645 } 646 647 /** 648 Parses the command line arguments according to the set of `Option`s that are passed in. 649 Environment variables are parsed first and then program args second 650 */ 651 public string[] parse(const string[] args) { 652 this.parseEnv; 653 return this.parseArgs(args); 654 } 655 656 private void parseEnv() { 657 import std.process: environment; 658 import std.conv: ConvException; 659 660 // Parse possible environment vars 661 static foreach (opt; Options) { 662 static if (opt.EnvironmentVar.length) {{ 663 string value; 664 try { 665 value = environment[opt.EnvironmentVar]; 666 } catch (Exception ex) { 667 // Env var doesn't exist 668 } 669 if (value.length) { 670 try { 671 tryAssign!(opt)(this, value, Yes.ignoreDuplicate); 672 } catch (ConvException ex) { 673 throw new MalformedProgramArgument( 674 "Could not set '" ~ opt.EnvironmentVar ~ "' to '" ~ value ~ "' - " ~ ex.msg 675 ); 676 } 677 } 678 }} 679 } 680 } 681 682 private string[] parseArgs(const string[] args) { 683 import std.algorithm: startsWith, findSplit, filter, map; 684 import std.range: drop, array, tee, take; 685 import std.conv: to; 686 import std.typecons: Yes, No; 687 import std..string: split; 688 689 // Initialize the encounters array 690 static foreach (opt; Options) { 691 encounteredOptions[opt.VarName] = false; 692 } 693 694 // Parse the first arg and executable path and see if we should skip first arg 695 import ddash.range: first, last, frontOr, withFront; 696 import ddash.algorithm: flatMap; 697 import std.file: thisExePath; 698 699 int index = 0; 700 701 args.first.map!(a => a.split("/").last).withFront!((a) { 702 import std.file: thisExePath; 703 thisExePath.split("/").last.withFront!((b) { 704 if (a == b) { 705 index = 1; 706 } 707 }); 708 }); 709 710 while (index < args.length) { 711 auto arg = args[index]; 712 debug_print("parsing arg ", arg); 713 bool shortOption = false; 714 string rest = ""; 715 if (arg.startsWith("--")) { 716 rest = arg.drop(2); 717 shortOption = false; 718 } else if (arg.startsWith("-")) { 719 rest = arg.drop(1); 720 shortOption = true; 721 } else { 722 unknownArgHandler(arg).filter!"a".tee!((a) { 723 throw new MalformedProgramArgument( 724 "Unknown argument '" ~ arg ~ "'" 725 ); 726 }).array; 727 index++; 728 continue; 729 } 730 731 // Make sure we have something in rest before we continue 732 if (rest.length == 0) { 733 debug_print("no option found") ; 734 if (shortOption) { 735 index += 1; 736 continue; 737 } 738 // Else it was a -- so we slice those out and we're done 739 debug_print("done. storing rest arguments: ", args[index + 1 .. $]) ; 740 return args[index + 1 .. $].dup; 741 } 742 743 auto nextValue() { 744 import ddash.range: nth; 745 auto next = args.nth(index + 1); 746 if (next.empty) { 747 throw new MissingProgramArgument("Did not find value for arg " ~ args[index]); 748 } 749 return next.front; 750 } 751 752 assert(rest.length > 0); 753 754 string[] options; 755 string value; 756 int steps = () { 757 if (shortOption) { 758 debug_print("parsing short name") ; 759 if (rest.length == 1) { 760 debug_print("one letter option only") ; 761 options = [rest.to!string]; 762 value = nextValue(); 763 return 2; 764 } 765 766 // If we have more than one char then it can be either of the forms 767 // 768 // a) ooo - bundleable options no value 769 // b) o=V - one option and assigned value 770 // c) oV - one option and stuck value 771 // d) ooo=V - bundleable options and assigned value 772 // e) oooV - bundleable options and stuck value 773 774 assert(rest.length > 1); 775 776 import std.algorithm: until; 777 auto bundleableArgs = rest.until!(a => !isBundleableShortName(a.to!string)).array; 778 779 debug_print("bundleableArgs=", bundleableArgs) ; 780 781 // Greater than one, and all of them are bundleable, and no value - case a 782 if (bundleableArgs.length == rest.length) { 783 debug_print("case a") ; 784 options = bundleableArgs.map!(to!string).array; 785 value = nextValue(); 786 return 2; 787 } 788 789 auto shortNames = rest.until!(a => !isShortName(a.to!string)).array; 790 791 debug_print("shortNames=", shortNames) ; 792 793 // Greater than one, but only one valid short name - case b and c 794 if (shortNames.length == 1) { 795 options = shortNames.map!(to!string).array; 796 auto parts = rest.findSplit("="); 797 debug_print("got parts=", parts) ; 798 if (parts[0].length == 1 && parts[1] == "=") { // case b 799 debug_print("case b") ; 800 value = parts[2]; 801 } else { 802 debug_print("case c") ; 803 value = parts[0].drop(1); 804 } 805 return 1; 806 } 807 808 // We have more than one short name, so now we have bundleables 809 assert(shortNames.length > 1); 810 811 if (shortNames.length != bundleableArgs.length) { 812 throw new MalformedProgramArgument( 813 "Bundled args '" ~ shortNames.to!string ~ "' are not all bundleable" 814 ); 815 } 816 817 assert(shortNames.length == bundleableArgs.length); 818 819 // Hanle case d and e 820 options = shortNames.map!(to!string).array; 821 auto parts = rest.findSplit("="); 822 debug_print("got parts=", parts) ; 823 if (parts[0].length == shortNames.length && parts[1] == "=") { // case d 824 debug_print("case d") ; 825 value = parts[2]; 826 } else { // case e 827 debug_print("case e") ; 828 value = parts[0].drop(shortNames.length); 829 } 830 return 1; 831 } else { 832 debug_print("parsing long name") ; 833 834 // We have a long name, and two cases: 835 // 836 // a) name 837 // b) name=V 838 839 auto parts = rest.findSplit("="); 840 debug_print("got parts=", parts) ; 841 if (parts[1] != "=") { // case a 842 debug_print("case a") ; 843 options = [parts[0]]; 844 value = nextValue(); 845 return 2; 846 } 847 848 // case b 849 options = [parts[0]]; 850 value = parts[2]; 851 debug_print("case b") ; 852 return 1; 853 } 854 }(); 855 856 debug_print("parsed => ", options, ", value: ", value, ", steps: ", steps) ; 857 858 loop: foreach (option; options) { 859 860 auto result = trySet(option, value, shortOption ? Yes.shortName : No.shortName); 861 862 with (AssignResult) final switch (result) { 863 case setValueUsed: 864 break; 865 case setValueUnused: 866 steps = 1; 867 break; 868 case noMatch: 869 unknownArgHandler(arg).filter!"a".tee!((a) { 870 throw new MalformedProgramArgument( 871 "Unknown argument '" ~ arg ~ "'" 872 ); 873 }).array; 874 steps = 1; 875 break loop; 876 } 877 } 878 879 index += steps; 880 } 881 882 return []; 883 } 884 885 /** 886 Returns an string that is a map the variable names and their values 887 */ 888 public string toString() const { 889 import std.conv: to; 890 string ret = "{ "; 891 static foreach (I, Opt; Options) { 892 ret ~= Opt.VarName ~ ": " ~ mixin(Opt.VarName ~ ".to!string"); 893 static if (I < Options.length - 1) { 894 ret ~= ", "; 895 } 896 } 897 ret ~= " }"; 898 return ret; 899 } 900 901 /** 902 Returns a string that represents a block of text that can be output to stdout 903 to display a help message 904 */ 905 public string helpText() const { 906 import std..string: leftJustify, stripRight; 907 import std.typecons: Tuple; 908 909 string ret; 910 911 // The max lengths will be used to indent the description text 912 size_t maxLongNameLength = 0; 913 size_t maxEnvVarLength = 0; 914 static foreach (Opt; Options) { 915 static if (Opt.Description) { 916 if (Opt.PrimaryLongName.length > maxLongNameLength) { 917 maxLongNameLength = Opt.PrimaryLongName.length; 918 } 919 static if (Opt.EnvironmentVar.length) { 920 if (Opt.EnvironmentVar.length > maxEnvVarLength) { 921 maxEnvVarLength = Opt.EnvironmentVar.length; 922 } 923 } 924 } 925 } 926 927 maxLongNameLength += 2; // for the -- 928 immutable maxShortNameLength = 2; 929 930 immutable startIndent = 2; 931 immutable shortLongIndent = 2; 932 immutable longDescIndent = 3; 933 934 alias EnvVarData = Tuple!(string, "name", string, "linkedOption", string, "description"); 935 EnvVarData[] envVarData; 936 937 bool hasOptionsSection = false; 938 static foreach (I, Opt; Options) {{ 939 940 // This will be used later to write out the Environment Var section 941 EnvVarData envVarDatum; 942 envVarDatum.description = Opt.Description; 943 944 // If we have a long name or short name we can display an "Options" section 945 static if (Opt.PrimaryShortName.length || Opt.PrimaryLongName.length) { 946 947 // Set this to null because we will output the description as part of the opttion names 948 envVarDatum.description = null; 949 950 // Write the Option section header once. 951 if (!hasOptionsSection) { 952 ret ~= "Options:"; 953 hasOptionsSection = true; 954 } 955 956 ret ~= "\n"; 957 958 string desc = startIndent.spaces; 959 static if (Opt.PrimaryShortName.length) {{ 960 auto str = "-" ~ Opt.PrimaryShortName; 961 envVarDatum.linkedOption = str; 962 desc ~= str; 963 }} else { 964 desc ~= maxShortNameLength.spaces; 965 } 966 desc ~= shortLongIndent.spaces; 967 static if (Opt.PrimaryLongName.length) {{ 968 auto str = "--" ~ Opt.PrimaryLongName; 969 envVarDatum.linkedOption = str; 970 desc ~= str.leftJustify(maxLongNameLength, ' '); 971 }} else { 972 desc ~= maxLongNameLength.spaces; 973 } 974 desc ~= longDescIndent.spaces; 975 auto indent = startIndent + maxShortNameLength + shortLongIndent + maxLongNameLength + longDescIndent; 976 if (!Opt.Description.length) { 977 desc = desc.stripRight; 978 } 979 ret ~= desc ~ printWithIndent(indent, Opt.Description); 980 } 981 982 static if (Opt.EnvironmentVar.length) { 983 envVarDatum.name = Opt.EnvironmentVar; 984 envVarData ~= envVarDatum; 985 } 986 }} 987 988 foreach (i, data; envVarData) { 989 // If first iteration, write out section header and add new lines if we had some text before (means we had an Options section) 990 if (i == 0) { 991 if (ret.length) { 992 ret ~= "\n\n"; 993 } 994 ret ~= "Environment Vars:"; 995 } 996 997 ret ~= "\n"; 998 ret ~= startIndent.spaces ~ data.name.leftJustify(maxEnvVarLength) ~ longDescIndent.spaces; 999 if (data.description.length) { 1000 ret ~= printWithIndent(startIndent + maxEnvVarLength + longDescIndent, data.description); 1001 } else { 1002 ret ~= "See: " ~ data.linkedOption; 1003 } 1004 } 1005 1006 return ret; 1007 } 1008 } 1009 1010 private string spaces(size_t i) pure nothrow { 1011 import std.range: repeat, take, array; 1012 return ' '.repeat.take(i).array; 1013 } 1014 1015 private string printWithIndent(size_t indent, string description, int maxColumnCount = 80) { 1016 import std..string: splitLines; 1017 import std.algorithm: splitter; 1018 import std.uni: isWhite; 1019 import std.range: array; 1020 string ret; 1021 auto currentWidth = indent; 1022 auto lines = description.splitLines; 1023 foreach(li, line; lines) { 1024 auto words = line.splitter!isWhite.array; 1025 foreach (wi, word; words) { 1026 ret ~= word ~ (wi < words.length - 1 ? 1 : 0).spaces; 1027 currentWidth += word.length; 1028 if (currentWidth > maxColumnCount) { 1029 ret ~= "\n" ~ indent.spaces; 1030 currentWidth = indent; 1031 } 1032 } 1033 if (li < lines.length - 1) { 1034 ret ~= "\n"; 1035 ret ~= indent.spaces; 1036 currentWidth = indent; 1037 } 1038 } 1039 return ret; 1040 } 1041 1042 unittest { 1043 import std.exception; 1044 auto opts = ProgramOptions!( 1045 Option!("opt", string).shortName!"o" 1046 )(); 1047 assertThrown!MissingProgramArgument(opts.parse(["-o"])); 1048 }