1 /++ 2 $(H2 The Command Type for Cmdline) 3 4 This modlue mainly has `Command` Type. 5 6 we can configure the command in manly way 7 and then use `Command.parse` to parse the input command line. 8 if you define its action callback, this callback would be called when parsing. 9 10 Authors: 笑愚(xiaoyu) 11 +/ 12 module cmdline.command; 13 14 import std.stdio; 15 import std.process; 16 import std.conv; 17 import std.array; 18 import std.range; 19 import std.range.primitives; 20 import std.traits; 21 import std.regex; 22 import std.string; 23 import std.typecons; 24 import std.algorithm; 25 import std.format; 26 import std.file; 27 import std.path; 28 import std.typetuple; 29 import std.json; 30 31 import cmdline.error; 32 import cmdline.argument; 33 import cmdline.option; 34 import cmdline.help; 35 import cmdline.pattern; 36 import cmdline.event; 37 38 version (Windows) import core.sys.windows.windows; 39 40 /// the enum represents the position of appendent help text 41 enum AddHelpPos : string { 42 BeforeAll = "beforeAll", 43 Before = "before", 44 After = "after", 45 AfterAll = "afterAll" 46 } 47 48 /// the callback passed as a parameter of `this.action` 49 alias ActionCallback = void delegate(); 50 51 /// the callback passed as a parameter of `this.action` 52 alias ActionCallback_0 = void delegate(in OptsWrap); 53 /// the callback passed as a parameter of `this.action` 54 alias ActionCallback_1 = void delegate(in OptsWrap, in ArgWrap); 55 /// the callback passed as a parameter of `this.action` 56 alias ActionCallback_2 = void delegate(in OptsWrap, in ArgWrap, in ArgWrap); 57 /// the callback passed as a parameter of `this.action` 58 alias ActionCallback_3 = void delegate(in OptsWrap, in ArgWrap, in ArgWrap, in ArgWrap); 59 /// the callback passed as a parameter of `this.action` 60 alias ActionCallback_4 = void delegate(in OptsWrap, in ArgWrap, in ArgWrap, in ArgWrap, in ArgWrap); 61 /// the callback passed as a parameter of `this.action` 62 alias ActionCallback_5 = void delegate(in OptsWrap, in ArgWrap, in ArgWrap, in ArgWrap, in ArgWrap, in ArgWrap); 63 64 /// the sequence of action callbacks 65 alias ActionCallBackSeq = AliasSeq!( 66 ActionCallback, 67 ActionCallback_0, 68 ActionCallback_1, 69 ActionCallback_2, 70 ActionCallback_3, 71 ActionCallback_4, 72 ActionCallback_5 73 ); 74 75 /// Command Type 76 class Command : EventManager { 77 package: 78 Command parent = null; 79 Command[] _commands = []; 80 81 Option[] _options = []; 82 NegateOption[] _negates = []; 83 Option[] _abandons = []; 84 85 Argument[] _arguments = []; 86 87 string _name; 88 string _defaultCommandName = ""; 89 string[] _aliasNames = []; 90 string _selfPath = ""; 91 92 string _called_sub = ""; 93 94 string _version = "*"; 95 string _usage = ""; 96 97 string _description = ""; 98 string[string] _argsDescription = null; 99 100 bool _execHandler = false; 101 string _execFile = ""; 102 string _execDir = ""; 103 string[string] _externalCmdHelpFlagMap = null; 104 105 string[] rawFlags = []; 106 string[] argFlags = []; 107 string[] errorFlags = []; 108 string[] unknownFlags = []; 109 110 Command subCommand = null; 111 112 bool immediately = false; 113 114 public ArgVariant[] args = []; 115 public OptionVariant[string] opts = null; 116 117 bool _allowExcessArguments = true; 118 bool _showHelpAfterError = false; 119 bool _showSuggestionAfterError = true; 120 bool _combineFlagAndOptionalValue = true; 121 bool _allowVariadicMerge = true; 122 bool _allowExposeOptionValue = false; 123 bool _passThroughOptionValue = false; 124 125 void delegate() _actionHandler = null; 126 Help _helpConfiguration = new Help; 127 OutputConfiguration _outputConfiguration = new OutputConfiguration(); 128 129 bool _hidden = false; 130 bool _addImplicitHelpCommand = true; 131 bool _addImplicitHelpOption = true; 132 Option _helpOption = null; 133 Command _helpCommand = null; 134 135 Option _versionOption = null; 136 Command _versionCommand = null; 137 138 Option _configOption = null; 139 const(JSONValue)*[] jconfig = []; 140 string[] _configPaths = []; 141 142 string[] _argToOptNames = []; 143 144 string[] _provide_arr = []; 145 string[] _inject_arr = []; 146 147 Option[string] _import_map = null; 148 NegateOption[string] _import_n_map = null; 149 150 Option[string] _export_map = null; 151 NegateOption[string] _export_n_map = null; 152 153 this(string name) { 154 this._name = name; 155 } 156 157 alias Self = typeof(this); 158 159 /// inherit the basic configuration from another command 160 public Self copyInheritedSettings(Command src) { 161 this._allowExcessArguments = src._allowExcessArguments; 162 this._showHelpAfterError = src._showHelpAfterError; 163 this._showSuggestionAfterError = src._showSuggestionAfterError; 164 this._combineFlagAndOptionalValue = src._combineFlagAndOptionalValue; 165 this._allowVariadicMerge = src._allowVariadicMerge; 166 this._allowExposeOptionValue = src._allowExposeOptionValue; 167 this._passThroughOptionValue = src._passThroughOptionValue; 168 this._outputConfiguration = src._outputConfiguration; 169 this._helpConfiguration = src._helpConfiguration; 170 return this; 171 } 172 173 inout(Command)[] _getCommandAndAncestors() inout { 174 Command[] result = []; 175 for (Command cmd = cast(Command) this; cmd; cmd = cmd.parent) { 176 result ~= cmd; 177 } 178 return cast(inout(Command)[]) result; 179 } 180 181 public: 182 /// get the description of command 183 string description() const { 184 return "description: " ~ this._description; 185 } 186 187 /// set the description of command 188 Self description(string str, string[string] argsDesc = null) { 189 this._description = str; 190 if (argsDesc !is null) 191 this._argsDescription = argsDesc; 192 return this; 193 } 194 195 /// configurre the help 196 Self configureHelp(Help helpConfig) { 197 this._helpConfiguration = helpConfig; 198 return this; 199 } 200 201 /// get the help configuration 202 inout(Help) configureHelp() inout { 203 return this._helpConfiguration; 204 } 205 206 /// register an option that can be as the argument of command line. 207 /// remember that it is positional sensitive and if you want to registered a variadic 208 /// option, this option must be registered at the end and not other variadic options to be registered 209 /// Params: 210 /// optName = the option's name or its long/short flag to be registered 211 /// rest = the rest be registered 212 /// Returns: `Self` for chain call 213 Self argToOpt(string optName, string[] rest...) { 214 auto tmp = [optName] ~ rest; 215 string variadic = ""; 216 foreach (string key; tmp) { 217 if (variadic.length) 218 this.error( 219 format!"not allowed to register option `%s` after variadic option `%s` which is also registered as argument"( 220 key, variadic 221 )); 222 auto opt = _findOption(key); 223 if (!opt) { 224 this.error( 225 format!"connot register option `%s` as argument for it doesn't exist"(key)); 226 } 227 if (opt.variadic) 228 variadic = opt.flags; 229 } 230 if (this._arguments.length && this._arguments[$ - 1].variadic) { 231 this.error( 232 format!"connot register options `%s` as arguments for the last registred argument `%s` is variadic"( 233 tmp.to!string, 234 this._arguments[$ - 1]._name 235 ) 236 ); 237 } 238 this._argToOptNames ~= tmp; 239 return this; 240 } 241 242 /// define a sub command and sub command's arguments, 243 /// this sub command inherit the basic configuration of parent command 244 /// Params: 245 /// nameAndArgs = like `sub <arg1> [arg2] [arg3...]` 246 /// cmdOpts = controll the behavior, 247 /// `isDefault` determine whether this sub command is default sub command, 248 /// `hidden` determine whether this sub command is hidden out of help 249 /// `Args`: that is type sequence of the arguments' type sequence 250 /// Returns: the sub command that you define 251 Command command(Args...)(string nameAndArgs, bool[string] cmdOpts = null) { 252 auto cmd = createCommand!Args(nameAndArgs); 253 this._registerCommand(cmd); 254 cmd.parent = this; 255 cmd.copyInheritedSettings(this); 256 if (cmdOpts) { 257 auto is_default = "isDefault" in cmdOpts; 258 auto is_hidden = "hidden" in cmdOpts; 259 if (is_default && *is_default) 260 this._defaultCommandName = cmd._name; 261 if (is_hidden) 262 cmd._hidden = *is_hidden; 263 } 264 return cmd; 265 } 266 267 /// define a sub command, which represents the external command line program 268 /// Params: 269 /// name = only the name of this sub command 270 /// desc = the description of this sub command 271 /// execOpts = controll the behavior, 272 /// `file` the file name of external command line program, 273 /// `dir` the directory that external command line program situates on 274 /// `help` the help option of this external command line program, which is useful when invoke the help of this sub command 275 /// Returns: `Self` for chain call 276 Self command(string name, string desc, string[string] execOpts = null) { 277 auto cmd = createCommand(name, desc); 278 cmd._execHandler = true; 279 version (Posix) { 280 cmd._execFile = format("%s-%s", this._name, cmd._name); 281 } 282 else version (Windows) { 283 cmd._execFile = format("%s-%s.exe", this._name, cmd._name); 284 } 285 cmd._execDir = dirName(thisExePath()); 286 if (execOpts) { 287 if (auto exec_file = "file" in execOpts) { 288 string tmp = strip(*exec_file); 289 auto cp = matchFirst(tmp, PTN_EXECUTABLE); 290 if (cp.empty) 291 error("error format of excutable file: " ~ tmp); 292 version (Windows) { 293 tmp = cp[1].empty ? tmp ~ ".exe" : tmp; 294 } 295 else version (Posix) { 296 tmp = !cp[1].empty ? tmp[0 .. $ - 4] : tmp; 297 } 298 cmd._execFile = tmp; 299 } 300 if (auto exec_dir = "dir" in execOpts) 301 cmd._execDir = *exec_dir; 302 if (auto exec_help_flag = "help" in execOpts) { 303 this._externalCmdHelpFlagMap[cmd._name] = *exec_help_flag; 304 } 305 } 306 if (!this._externalCmdHelpFlagMap || !(cmd._name in this._externalCmdHelpFlagMap)) 307 this._externalCmdHelpFlagMap[cmd._name] = "--help"; 308 cmd.usage(format!"run `%s %s --help` to see"(this._name, cmd._name)); 309 cmd.parent = this; 310 cmd._allowExcessArguments = true; 311 cmd._showHelpAfterError = false; 312 cmd._showSuggestionAfterError = false; 313 cmd._combineFlagAndOptionalValue = false; 314 cmd._outputConfiguration = this._outputConfiguration; 315 cmd._helpConfiguration = null; 316 cmd.disableHelp(); 317 this._registerCommand(cmd); 318 return this; 319 } 320 321 /// add the sub command, which is often used when you want more detail configuration of sub command 322 /// Params: 323 /// cmd = the command used as the sub command 324 /// cmdOpts = see also `this.command(Args...)(string nameAndArgs, bool[string] cmdOpts = null)` 325 /// `Args`: see alos `this.command(Args...)(string nameAndArgs, bool[string] cmdOpts = null)` 326 /// Returns: `Self` for chain call 327 Self addCommand(Command cmd, bool[string] cmdOpts = null) { 328 if (!cmd._name.length) { 329 this.error("Command passed to .addCommand() must have a name 330 - specify the name in Command constructor or using .name()"); 331 } 332 this._registerCommand(cmd); 333 cmd.parent = this; 334 cmd.copyInheritedSettings(this); 335 if (cmdOpts) { 336 auto is_default = "isDefault" in cmdOpts; 337 auto is_hidden = "hidden" in cmdOpts; 338 if (is_default && *is_default) 339 this._defaultCommandName = cmd._name; 340 if (is_hidden) 341 cmd._hidden = *is_hidden; 342 } 343 return this; 344 } 345 346 /// define the arguments' descriptions 347 /// Params: 348 /// argName = the argument's name 349 /// desc = the description of the argument 350 /// Returns: `Self` for chain call 351 Self argumentDesc(string argName, string desc) { 352 auto arg = _findArgument(argName); 353 if (arg) 354 arg._description = desc; 355 return this; 356 } 357 358 /// define the arguments' descriptions 359 /// Params: 360 /// descMap = the map of arguments' description, key is the name of argument 361 /// Returns: `Self` for chain call 362 Self argumentDesc(string[string] descMap) { 363 foreach (argName, desc; descMap) { 364 argumentDesc(argName, desc); 365 } 366 return this; 367 } 368 369 package: 370 void _registerArgument(Argument arg) { 371 auto other = _findArgument(arg._name); 372 if (other) { 373 this.error(format!"cannot add argument `%s` as this name already used "( 374 arg._name)); 375 } 376 this._arguments ~= arg; 377 } 378 379 void _registerCommand(Command command) { 380 auto knownBy = (Command cmd) => [cmd.name] ~ cmd.aliasNames; 381 auto alreadyUsed = knownBy(command).find!(name => this._findCommand(name)); 382 if (!alreadyUsed.empty) { 383 string exit_cmd = knownBy(this._findCommand(alreadyUsed[0])).join("|"); 384 string new_cmd = knownBy(command).join("|"); 385 this.error(format!"cannot add command `%s` as already have command `%s`"(new_cmd, exit_cmd)); 386 } 387 if (auto help_cmd = this._helpCommand) { 388 auto num = knownBy(command).count!(name => name == help_cmd._name || 389 cast(bool) help_cmd._aliasNames.count(name)); 390 if (num) { 391 string help_cmd_names = knownBy(help_cmd).join("|"); 392 string new_cmd_names = knownBy(command).join("|"); 393 this.error(format( 394 "cannot add command `%s` as this command name cannot be same as " ~ 395 "the name of help command `%s`", 396 new_cmd_names, help_cmd_names)); 397 } 398 } 399 else if (this._addImplicitHelpCommand) { 400 string help_cmd_names = "help"; 401 if (auto num = knownBy(command).count(help_cmd_names)) { 402 string new_cmd_names = knownBy(command).join("|"); 403 this.error(format( 404 "cannot add command `%s` as this command name cannot be same as " ~ 405 "the name of help command `%s`", 406 new_cmd_names, help_cmd_names)); 407 } 408 } 409 if (auto version_cmd = this._versionCommand) { 410 auto num = knownBy(command).count!(name => name == version_cmd._name || cast(bool) version_cmd 411 ._aliasNames.count( 412 name)); 413 if (num) { 414 string version_cmd_names = knownBy(version_cmd).join("|"); 415 string new_cmd_names = knownBy(command).join("|"); 416 this.error(format( 417 "cannot add command `%s` as this command name cannot be same as " ~ 418 "the name of version command `%s`", 419 new_cmd_names, version_cmd_names)); 420 } 421 } 422 command.parent = this; 423 this._commands ~= command; 424 } 425 426 void _registerOption(Option option) { 427 Option match_lopt = this._findOption(option.longFlag); 428 Option match_sopt = this._findOption(option.shortFlag); 429 NegateOption match_nopt = this._findNegateOption(option.shortFlag); 430 if (match_lopt) { 431 string match_flags = match_lopt is match_sopt ? match_lopt.flags : match_lopt.longFlag; 432 this.error( 433 format!"Cannot add option '%s' due to conflicting flag '%s' - already ued by option '%s'"( 434 option.flags, match_flags, match_lopt.flags 435 )); 436 } 437 if (match_sopt && match_sopt !is match_lopt) { 438 auto match_flags = option.shortFlag; 439 this.error( 440 format!"Cannot add option '%s' due to conflicting flag '%s' - already ued by option '%s'"( 441 option.flags, match_flags, match_sopt.flags 442 )); 443 } 444 if (match_nopt) { 445 string match_flags = match_nopt.shortFlag; 446 this.error( 447 format!"Cannot add option '%s' due to conflicting flag '%s' - already ued by option '%s'"( 448 option.flags, match_flags, match_nopt.flags 449 )); 450 } 451 if (auto help_option = this._helpOption) { 452 if (option.matchFlag(help_option)) { 453 this.error(format!"Cannot add option '%s' due to confliction help option `%s`"( 454 option.flags, help_option.flags)); 455 } 456 } 457 else if (this._addImplicitHelpOption && (option.shortFlag == "-h" || option.longFlag == "--help")) { 458 this.error(format!"Cannot add option '%s' due to confliction help option `%s`"(option.flags, "-h, --help")); 459 } 460 if (auto version_option = this._versionOption) { 461 if (option.matchFlag(version_option)) { 462 this.error(format!"Cannot add option '%s' due to confliction version option `%s`"( 463 option.flags, version_option.flags)); 464 } 465 } 466 if (auto config_option = this._configOption) { 467 if (option.matchFlag(config_option)) { 468 this.error(format!"Cannot add option '%s' due to confliction config option `%s`"( 469 option.flags, config_option.flags)); 470 } 471 } 472 this._options ~= option; 473 } 474 475 void _registerOption(NegateOption option) { 476 NegateOption match_lopt = this._findNegateOption(option.longFlag); 477 NegateOption match_sopt = this._findNegateOption(option.shortFlag); 478 Option match_opt = this._findOption(option.shortFlag); 479 if (match_lopt) { 480 string match_flags = match_lopt is match_sopt ? match_lopt.flags : match_lopt.longFlag; 481 this.error( 482 format!"Cannot add option '%s' due to conflicting flag '%s' - already ued by option '%s'"( 483 option.flags, match_flags, match_lopt.flags 484 )); 485 } 486 if (match_sopt && match_sopt !is match_lopt) { 487 auto match_flags = option.shortFlag; 488 this.error( 489 format!"Cannot add option '%s' due to conflicting flag '%s' - already ued by option '%s'"( 490 option.flags, match_flags, match_sopt.flags 491 )); 492 } 493 if (match_opt) { 494 auto match_flags = match_opt.shortFlag; 495 this.error( 496 format!"Cannot add option '%s' due to conflicting flag '%s' - already ued by option '%s'"( 497 option.flags, match_flags, match_opt.flags 498 )); 499 } 500 if (auto help_option = this._helpOption) { 501 if (option.matchFlag(help_option)) 502 this.error(format!"Cannot add negate-option '%s' due to confliction help option `%s`"( 503 option.flags, help_option.flags)); 504 } 505 else if (this._addImplicitHelpOption && (option.shortFlag == "-h" || option.longFlag == "--no-help")) { 506 this.error(format!"Cannot add negate-option '%s' due to confliction help option `%s`"( 507 option.flags, "-h, --help")); 508 } 509 if (auto version_option = this._versionOption) { 510 if (option.matchFlag(version_option)) 511 this.error(format!"Cannot add negate-option '%s' due to confliction version option `%s`"( 512 option.flags, version_option.flags)); 513 } 514 if (auto config_option = this._configOption) { 515 if (option.matchFlag(config_option)) { 516 this.error(format!"Cannot add negate-option '%s' due to confliction config option `%s`"( 517 option.flags, config_option.flags)); 518 } 519 } 520 this._negates ~= option; 521 } 522 523 public: 524 /// add the option to command 525 Self addOption(Option option) { 526 if (!this._allowVariadicMerge) 527 option.merge(false); 528 this._registerOption(option); 529 bool is_required = option.isRequired; 530 bool is_optional = option.isOptional; 531 bool is_variadic = option.variadic; 532 string name = option.name; 533 if (is_required) { 534 if (is_variadic) { 535 this.on("option:" ~ name, (string[] vals) { 536 if (vals.length == 0) { 537 this.parsingError( 538 format!"the value's num of variadic option `%s` cannot be zero"(name)); 539 } 540 setOptionVal!(Source.Cli)(name, vals); 541 }); 542 } 543 else { 544 this.on("option:" ~ name, (string val) { 545 setOptionVal!(Source.Cli)(name, val); 546 }); 547 } 548 } 549 else if (is_optional) { 550 if (is_variadic) { 551 this.on("option:" ~ name, (string[] vals) { 552 if (vals.length) 553 setOptionVal!(Source.Cli)(name, vals); 554 else 555 option.found = true; 556 }); 557 } 558 else { 559 this.on("option:" ~ name, (string val) { 560 setOptionVal!(Source.Cli)(name, val); 561 }); 562 this.on("option:" ~ name, () { option.found = true; }); 563 } 564 } 565 else { 566 this.on("option:" ~ name, () { option.found = true; }); 567 } 568 return this; 569 } 570 571 /// add the negate option to command 572 Self addOption(NegateOption option) { 573 auto opt = _findOption(option.name); 574 if (!opt) { 575 opt = createOption("--" ~ option.name, "see also option " ~ option.flags).defaultVal(); 576 _registerOption(opt); 577 _registerOption(option); 578 this.on("negate:" ~ option.name, () { 579 setOptionValDirectly(option.name, false, Source.Cli); 580 }); 581 } 582 else { 583 _registerOption(option); 584 if (opt.isBoolean) { 585 opt.defaultVal(); 586 this.on("negate:" ~ option.name, () { 587 setOptionValDirectly(option.name, false, Source.Cli); 588 }); 589 } 590 else 591 this.on("negate:" ~ option.name, () { 592 this._options = this._options.remove!(ele => ele is opt); 593 this._abandons ~= opt; 594 }); 595 } 596 return this; 597 } 598 599 /// add the action option to command, which will invoke the callback we injected when parsing the flag of this option, only useful in client cmd 600 Self addActionOption(Option option, void delegate(string[] vals...) call_back, bool endMode = true) { 601 if (!this._allowVariadicMerge) 602 option.merge(false); 603 this._registerOption(option); 604 string name = option.name; 605 this.on("option:" ~ name, () { 606 call_back(); 607 if (endMode) 608 this._exitSuccessfully(); 609 }); 610 this.on("option:" ~ name, (string str) { 611 call_back(str); 612 if (endMode) 613 this._exitSuccessfully(); 614 }); 615 this.on("option:" ~ name, (string[] strs) { 616 call_back(strs); 617 if (endMode) 618 this._exitSuccessfully(); 619 }); 620 return this; 621 } 622 623 package Self _optionImpl(T, bool isMandatory = false)(string flags, string desc) 624 if (isOptionValueType!T) { 625 auto option = createOption!T(flags, desc); 626 option.makeMandatory(isMandatory); 627 return this.addOption(option); 628 } 629 630 package Self _optionImpl(string flags, string desc) { 631 auto lflag = splitOptionFlags(flags).longFlag; 632 auto is_negate = !matchFirst(lflag, PTN_NEGATE).empty; 633 if (is_negate) { 634 auto nopt = createNOption(flags, desc); 635 return this.addOption(nopt); 636 } 637 else { 638 auto opt = createOption(flags, desc); 639 return this.addOption(opt); 640 } 641 } 642 643 /// define a value/variadic option to this command 644 Self option(T)(string flags, string desc) { 645 return _optionImpl!(T)(flags, desc); 646 } 647 648 /// define a bool option to this command 649 Self option(string flags, string desc) { 650 return _optionImpl(flags, desc); 651 } 652 653 /// define a mandatory value/variadic option to this command 654 Self requiredOption(T)(string flags, string desc) { 655 return _optionImpl!(T, true)(flags, desc); 656 } 657 658 /// define a mandatory bool option to this command 659 Self requireOption(string flags, string desc) { 660 return _optionImpl!(bool, true)(flags, desc); 661 } 662 663 package Self _optionImpl(T, bool isMandatory = false)(string flags, string desc, T defaultValue) 664 if (isOptionValueType!T) { 665 auto option = createOption!T(flags, desc); 666 option.makeMandatory(isMandatory); 667 option.defaultVal(defaultValue); 668 return this.addOption(option); 669 } 670 671 /// see also `Self option(T)(string flags, string desc)` and define a default value for this option 672 Self option(T)(string flags, string desc, T defaultValue) { 673 return _optionImpl!T(flags, desc, defaultValue); 674 } 675 676 /// see also `Self requiredOption(T)(string flags, string desc)` and define a default value for this option 677 Self requiredOption(T)(string flags, string desc, T defaultValue) { 678 return _optionImpl!(T, true)(flags, desc, defaultValue); 679 } 680 681 /// expose its option values by names to its sub or sub sub and etc command. 682 /// its sub or sub sub and etc command can access these option values using `Command.injects` by names. 683 /// remember that it is only useful for non-builtin options! 684 /// Params: 685 /// name = the name of option to be exposed 686 /// rest = the rest names of options to be exposed 687 /// Returns: `Self` for chain call 688 Self provides(in string name, in string[] rest...) { 689 auto tmp = [name] ~ rest; 690 foreach (string n; tmp) { 691 if (n.length == 0 || n[1] == '-') { 692 error(format!"the option name `\"%s\"` is not in legacy in `Command.provides`"(n)); 693 } 694 } 695 if (tmp.any!(str => this._findOption(str) is null)) { 696 this.error(format!"the non-builtin option names `%s` you provide doesn't all exist in `Command.provides`"( 697 tmp.to!string 698 )); 699 } 700 this._provide_arr.each!((str) { 701 if (str.canFind(':') && name.length < str.length 702 && (name == str[$ - name.length .. $] || name == str[0 .. name.length])) 703 error(format!"the option name `%s` has been registered to be exposed as `%s`, you cannot register it again in `Command.provides`"( 704 name, str 705 )); 706 }); 707 this._provide_arr = (tmp ~ this._provide_arr).uniq.array; 708 return this; 709 } 710 711 /// see also `Self provides(string name, string[] rest...)`. 712 /// this member function are used for register an option to be exposed as an new name. 713 /// this is usually used when you want to avoid name confilict. 714 /// Params: 715 /// name = the name of option to be exposed 716 /// asName = the new name of option to be exposed 717 /// Returns: `Self` for chain call 718 Self providesAs(in string name, in string asName) { 719 if (name.length == 0 || name[1] == '-') { 720 error(format!"the option name `\"%s\"` is not in legacy in `Command.providesAs`"(name)); 721 } 722 if (asName.length == 0 || asName[1] == '-') { 723 error(format!"the option as-name `\"%s\"` is not in legacy in `Command.providesAs`"( 724 asName)); 725 } 726 if (this._findOption(name) is null) { 727 this.error( 728 format!"the non-builtin option name `%s` you provide doesn't exist in `Command.providesAs`"( 729 name)); 730 } 731 if (this._findOption(asName) !is null) { 732 this.error( 733 format!"the as-name `%s` has been an option's name, you cannot make it as the name of option `%s` in `Command.providesAs`"( 734 asName, name 735 )); 736 } 737 if (this._provide_arr.count(name) > 0) { 738 error(format!"the option name `%s` has been registered to be exposed, you cannot register it again in `Command.providesAs`"( 739 name)); 740 } 741 if (this._provide_arr.count(asName) > 0) { 742 error(format!"the option as-name `%s` has been registered to be exposed, you cannot register it again in `Command.providesAs`"( 743 asName)); 744 } 745 this._provide_arr.each!((str) { 746 if (str.canFind(':') && asName.length < str.length && asName == str[0 .. asName.length]) 747 error(format!"the option as-name `%s` has been registered to be exposed as `%s`, you cannot register it again in `Command.providesAs`"( 748 asName, str 749 )); 750 }); 751 this._provide_arr.each!((str) { 752 if (str.canFind(':') && name.length < str.length && name == str[$ - name.length .. $]) 753 error(format!"the option name `%s` has been registered to be exposed as `%s`, you cannot register it again in `Command.providesAs`"( 754 name, str 755 )); 756 }); 757 this._provide_arr ~= asName ~ ':' ~ name; 758 return this; 759 } 760 761 /// see also `Self providesAs(in string name, in string asName)`. 762 /// expose option values in new names at the same time 763 /// Params: 764 /// optsMap = the map of name and asName 765 /// Returns: `Self` for chain call 766 Self providesAs(in string[string] optsMap) { 767 foreach (name, asName; optsMap) { 768 this.providesAs(name, asName); 769 } 770 return this; 771 } 772 773 /// inject the options' values exposed by ancestor command by name that are registered 774 /// using `Command.provides` or `Command.providesAs`, so that we can access this option value. 775 /// Params: 776 /// name = the name of option to be injected 777 /// rest = the rest name of option to be injected 778 /// Returns: `Self` for chain call 779 Self injects(in string name, in string[] rest...) { 780 auto tmp = ([name] ~ rest).uniq.array; 781 foreach (string n; tmp) { 782 if (n.length == 0 || n[1] == '-') { 783 error(format!"the option name `\"%s\"` is not in legacy in `Command.injects`"(n)); 784 } 785 } 786 foreach (str; tmp) { 787 if (_findOption(str) !is null) 788 error(format!"connot inject option `%s` for this option has been exist in its option list using `Command.injects`"( 789 str)); 790 } 791 Command[] ancestors = this._getCommandAndAncestors()[1 .. $]; 792 auto get_front = () => tmp.length > 0 ? tmp.front : ""; 793 while (true) { 794 string str = get_front(); 795 if (str == "") 796 break; 797 this._inject_arr.each!((istr) { 798 if (istr.canFind(':') && str.length < istr.length 799 && (str == istr[$ - str.length .. $] || str == istr[0 .. str.length])) { 800 error(format!"the option name `%s` has been injected as `%s`, you cannot register it again in `Command.injects`"( 801 str, istr 802 )); 803 } 804 }); 805 foreach (Command cmd; ancestors) { 806 auto provide_arr = cmd._provide_arr; 807 if (provide_arr.any!((pstr) { 808 if (str.length <= pstr.length && str == pstr[0 .. str.length]) 809 return true; 810 else 811 return false; 812 })) { 813 this._inject_arr ~= str; 814 tmp.popFront; 815 break; 816 } 817 } 818 if (str == get_front()) 819 error(format!"cannnot inject the name `%s` for there is not any option match option name the ancestors provided using `Command.injects`"( 820 str)); 821 } 822 this._inject_arr = this._inject_arr.uniq.array; 823 return this; 824 } 825 826 /// see also `Self injects(string name, string[] rest...)`. 827 /// this member function are used for inject an option value as an new name. 828 /// this is usually used when you want to avoid name confilict. 829 /// Params: 830 /// name = the name of option to be injected 831 /// asName = the new name of option to be injected 832 /// Returns: `Self` for chain call 833 Self injectsAs(in string name, in string asName) { 834 if (name.length == 0 || name[1] == '-') { 835 error(format!"the option name `\"%s\"` is not in legacy in `Command.injectsAs`"(name)); 836 } 837 if (asName.length == 0 || asName[1] == '-') { 838 error(format!"the option asName `\"%s\"` is not in legacy in `Command.injectsAs`"( 839 asName)); 840 } 841 if (this._findOption(asName) !is null) { 842 this.error( 843 format!"the as-name `%s` has been an option's name, you cannot make it as the name of option `%s` in `Command.injectsAs`"( 844 asName, name 845 )); 846 } 847 if (this._inject_arr.count(name) > 0) 848 error(format!"the option name `%s` has been injected, you cannot inject it again in `Command.injectsAs`"( 849 name)); 850 if (this._inject_arr.count(asName) > 0) 851 error(format!"the option name `%s` has been injected, you cannot inject it again in `Command.injectsAs`"( 852 asName)); 853 this._inject_arr.each!((str) { 854 if (str.canFind(':') && asName.length < str.length && asName == str[0 .. asName.length]) 855 error(format!"the option as-name `%s` has been injected as `%s`, you cannot injected it again in `Command.providesAs`"( 856 asName, str 857 )); 858 }); 859 this._inject_arr.each!((str) { 860 if (str.canFind(':') && name.length < str.length && name == str[0 .. name.length]) 861 error(format!"the option name `%s` has been injected as `%s`, you cannot injected it again in `Command.providesAs`"( 862 name, str 863 )); 864 }); 865 Command[] ancestors = this._getCommandAndAncestors()[1 .. $]; 866 bool flag = false; 867 foreach (Command cmd; ancestors) { 868 auto provide_arr = cmd._provide_arr; 869 auto str = name; 870 if (provide_arr.any!((pstr) { 871 if (str.length <= pstr.length && str == pstr[0 .. str.length]) 872 return true; 873 else 874 return false; 875 })) { 876 flag = true; 877 break; 878 } 879 } 880 if (!flag) 881 error(format!"cannnot inject the name `%s` for there is not any option match option name the ancestors provided using `Command.injectsAs`"( 882 name)); 883 this._inject_arr ~= asName ~ ':' ~ name; 884 return this; 885 } 886 887 /// see also `Self injectsAs(in string name, in string asName)`. 888 /// inject option values in new names at the same time 889 /// Params: 890 /// optsMap = the map of name and asName 891 /// Returns: `Self` for chain call 892 Self injectsAs(in string[string] optsMap) { 893 foreach (name, asName; optsMap) { 894 injectsAs(name, asName); 895 } 896 return this; 897 } 898 899 package: 900 Self setOptionVal(Source src, T)(string key, T value) if (isOptionValueType!T) { 901 Option opt = this._findOption(key); 902 if (!opt) { 903 this.parsingError(format!"option `%s` doesn't exist"(key)); 904 } 905 switch (src) { 906 case Source.Default: 907 opt.defaultVal(value); 908 break; 909 case Source.Config: 910 opt.configVal(value); 911 break; 912 case Source.Imply: 913 opt.implyVal(value); 914 break; 915 case Source.Preset: 916 opt.preset(value); 917 break; 918 default: 919 this.error; 920 break; 921 } 922 return this; 923 } 924 925 Self setOptionVal(Source src)(string key) { 926 auto opt = this._findOption(key); 927 if (!opt) { 928 this.parsingError(format!"option `%s` doesn't exist"(key)); 929 } 930 switch (src) { 931 case Source.Default: 932 opt.defaultVal(); 933 break; 934 case Source.Config: 935 opt.configVal(); 936 break; 937 case Source.Imply: 938 opt.implyVal(); 939 break; 940 case Source.Preset: 941 opt.preset(); 942 break; 943 default: 944 this.error; 945 break; 946 } 947 return this; 948 } 949 950 Self setOptionVal(Source src : Source.Env)(string key) { 951 auto opt = this._findOption(key); 952 if (!opt) { 953 this.parsingError(format!"option `%s` doesn't exist"(key)); 954 } 955 opt.envVal(); 956 return this; 957 } 958 959 Self setOptionVal(Source src : Source.Cli, T: 960 string)(string key, T value, T[] rest...) { 961 auto opt = this._findOption(key); 962 if (!opt) { 963 this.parsingError(format!"option `%s` doesn't exist"(key)); 964 } 965 opt.cliVal(value, rest); 966 opt.found = true; 967 return this; 968 } 969 970 Self setOptionVal(Source src : Source.Cli, T: 971 string)(string key, T[] values) { 972 if (values.length == 0) { 973 this.parsingError( 974 format!"the value's num of option `%s` cannot be zero"(key)); 975 } 976 return this.setOptionVal!src(key, values[0], values[1 .. $]); 977 } 978 979 Self setOptionValDirectly(T)(string key, T value, Source src = Source.None) 980 if (isOptionValueType!T) { 981 auto opt = this._findOption(key); 982 if (!opt) { 983 this.parsingError(format!"option `%s` doesn't exist"(key)); 984 } 985 static if (!is(ElementType!T U == void) && !is(T == string)) { 986 VariadicOption!U derived = cast(VariadicOption!U) opt; 987 if (!derived) { 988 error(format!"connot set value `%s` in option `%s` directly using `Command.setOptionValDirectly`"( 989 value.to!string, 990 opt.flags 991 )); 992 } 993 derived.innerValueData = value; 994 derived.isValueData = true; 995 derived.source = src; 996 derived.settled = true; 997 } 998 else { 999 ValueOption!T derived = cast(ValueOption!T) opt; 1000 if (!derived) { 1001 error(format!"connot set value `%s` in option `%s` directly using `Command.setOptionValDirectly`"( 1002 value.to!string, 1003 opt.flags 1004 )); 1005 } 1006 derived.innerValueData = value; 1007 derived.isValueData = true; 1008 derived.source = src; 1009 derived.settled = true; 1010 } 1011 return this; 1012 } 1013 1014 Self setOptionValDirectly(T)(string key, Source src = Source.None) 1015 if (isOptionValueType!T) { 1016 auto opt = this._findOption(key); 1017 if (!opt) { 1018 this.parsingError(format!"option `%s` doesn't exist"(key)); 1019 } 1020 if (!opt.isOptional) { 1021 this.parsingError(format!"option `%s` must be optional"(key)); 1022 } 1023 static if (!is(ElementType!T U == void) && !is(T == string)) { 1024 VariadicOption!U derived = cast(VariadicOption!U) opt; 1025 if (!derived) { 1026 error(format!"connot set value `true` in option `%s` directly using `Command.setOptionValDirectly`"( 1027 opt.flags 1028 )); 1029 } 1030 derived.innerBoolData = true; 1031 derived.isValueData = false; 1032 derived.source = src; 1033 derived.settled = true; 1034 } 1035 else { 1036 ValueOption!T derived = cast(ValueOption!T) opt; 1037 if (!derived) { 1038 error(format!"connot set value `true` in option `%s` directly using `Command.setOptionValDirectly`"( 1039 opt.flags 1040 )); 1041 } 1042 derived.innerBoolData = true; 1043 derived.isValueData = false; 1044 derived.source = src; 1045 derived.settled = true; 1046 } 1047 return this; 1048 } 1049 1050 Self setOptionValDirectly(string key, bool value = true, Source src = Source.None) { 1051 auto opt = this._findOption(key); 1052 if (!opt) { 1053 this.parsingError(format!"option `%s` doesn't exist"(key)); 1054 } 1055 BoolOption derived = cast(BoolOption) opt; 1056 if (!derived) { 1057 error(format!"connot set value `%s` in option `%s` directly using `Command.setOptionValDirectly`"( 1058 value.to!string, 1059 opt.flags 1060 )); 1061 } 1062 derived.innerBoolData = value; 1063 derived.isValueData = false; 1064 derived.source = src; 1065 derived.settled = true; 1066 return this; 1067 } 1068 1069 public: 1070 /// get the inner value of a option by name wrapped by `ArgWrap`. 1071 /// remember better use it in action callabck or after parsing 1072 /// Params: 1073 /// key = the name of option 1074 /// Returns: the value wrapped by `ArgWrap`, which may be empty when cannot find the option 1075 ArgWrap getOptVal(string key) const { 1076 if (!this.opts) { 1077 this.error("the options has not been initialized, cannnot get the value of option now"); 1078 } 1079 auto ptr = key in this.opts; 1080 return ptr ? ArgWrap(*ptr) : ArgWrap(null); 1081 } 1082 1083 /// get the inner value in specified type of an option by flags 1084 /// Params: 1085 /// key = the long|short flag or the name of the option 1086 /// Returns: the inner value not wrapped 1087 T getOptVal(T)(string key) const { 1088 auto opt = this.findOption(key); 1089 if (!opt) { 1090 this.error(format!"connot find option `%s` when try to get its value of type `%s`"( 1091 opt.flags, 1092 T.stringof 1093 )); 1094 } 1095 return opt.get!T; 1096 } 1097 1098 // ArgWrap getOptionValWithGlobal(string key) const { 1099 // auto cmds = this._getCommandAndAncestors(); 1100 // foreach (cmd; cmds) { 1101 // if (cmd.opts && key in cmd.opts) 1102 // return ArgWrap(this.opts[key]); 1103 // auto opt = this._findOption(key); 1104 // if (!opt) { 1105 // this.error(format!"option `%s` doesn't exist"(key)); 1106 // } 1107 // return ArgWrap(opt.get); 1108 // } 1109 // this.error(format!"cannot get the option `%s`'s value"(key)); 1110 // assert(0); 1111 // } 1112 1113 /// get the source of option 1114 /// remember better use it in action callabck or after parsing 1115 /// Params: 1116 /// key = the flag of option 1117 /// Returns: the source of option final value in `Source` 1118 Source getOptionValSource(string key) const { 1119 auto opt = this._findOption(key); 1120 if (!opt || !opt.settled) { 1121 this.error(format!"option `%s` doesn't exist or not settled"(key)); 1122 } 1123 return opt.source; 1124 } 1125 1126 // Source getOptionValWithGlobalSource(string key) const { 1127 // auto cmds = this._getCommandAndAncestors(); 1128 // foreach (cmd; cmds) { 1129 // auto opt = this._findOption(key); 1130 // if (!opt) { 1131 // this.error(format!"option `%s` doesn't exist"(key)); 1132 // } 1133 // return opt.source; 1134 // } 1135 // this.error(format!"cannot get the option `%s`'s source"(key)); 1136 // assert(0); 1137 // } 1138 1139 package void execSubCommand(in string[] unknowns) { 1140 string sub_path = buildPath(_execDir, _execFile); 1141 const(string)[] inputs; 1142 if (!(sub_path[0] == '"' && sub_path[$ - 1] == '"') && sub_path.any!(c => c == ' ')) { 1143 sub_path = '"' ~ sub_path ~ '"'; 1144 } 1145 unknowns.each!((str) { 1146 if (!(str[0] == '"' && str[$ - 1] == '"') && str.any!(c => c == ' ')) { 1147 string new_str = '"' ~ str ~ '"'; 1148 inputs ~= new_str; 1149 } 1150 else { 1151 inputs ~= str; 1152 } 1153 }); 1154 auto result = executeShell(sub_path ~ " " ~ inputs.join(" ")); 1155 if (result.status == 0) { 1156 this._outputConfiguration.writeOut(result.output); 1157 this._exitSuccessfully(); 1158 } 1159 else { 1160 this._outputConfiguration.writeErr(result.output); 1161 this._exit(1); 1162 } 1163 } 1164 1165 /// parse the command line argument variables 1166 /// Params: 1167 /// argv = the command line argument variables 1168 void parse(in string[] argv) { 1169 auto user_argv = _prepareUserArgs(argv); 1170 try { 1171 _parseCommand(user_argv); 1172 } 1173 catch (InvalidArgumentError e) { 1174 parsingError(e.msg, e.code); 1175 } 1176 catch (InvalidOptionError e) { 1177 parsingError(e.msg, e.code); 1178 } 1179 } 1180 1181 package: 1182 string[] _prepareUserArgs(in string[] args) { 1183 auto arr = args.filter!(str => str.length).array; 1184 this._selfPath = arr[0]; 1185 this.rawFlags = arr.dup; 1186 return args[1 .. $].dup; 1187 } 1188 1189 void _parseCommand(in string[] unknowns) { 1190 auto has_cmd = (const string str) { 1191 auto _cmd = _findCommand(str); 1192 auto vcmd = this._versionCommand; 1193 auto hcmd = this._helpCommand; 1194 _cmd = !_cmd && vcmd && vcmd._name == str ? vcmd : _cmd; 1195 _cmd = !_cmd && hcmd && hcmd._name == str ? hcmd : _cmd; 1196 return _cmd ? true : (this._addImplicitHelpCommand && str == "help"); 1197 }; 1198 auto has_hvopt = (const string str) { 1199 auto vopt = this._versionOption; 1200 auto hopt = this._helpOption; 1201 auto _opt = vopt && vopt.isFlag(str) ? vopt : null; 1202 _opt = !_opt && hopt && hopt.isFlag(str) ? hopt : null; 1203 return _opt ? true : (this._addImplicitHelpOption && 1204 (str == "--help" || str == "-h")); 1205 }; 1206 if (!this._defaultCommandName.empty && 1207 unknowns.all!(str => !has_cmd(str) && !has_hvopt(str))) { 1208 auto cmd = _findCommand(this._defaultCommandName); 1209 if (!cmd) 1210 parsingError("cannot find the default sub command `" 1211 ~ this._defaultCommandName ~ "`"); 1212 if (this._configOption && !cmd._execHandler) { 1213 auto j_config = _processConfigFile(); 1214 if (j_config.length) { 1215 auto cmd_name = cmd._name; 1216 const(JSONValue)*[] tmp = []; 1217 foreach (v; j_config) { 1218 auto jtmp = cmd_name in *v; 1219 if (jtmp) 1220 tmp ~= jtmp; 1221 } 1222 cmd.jconfig = tmp; 1223 } 1224 } 1225 this._called_sub = cmd._name; 1226 cmd._parseCommand(unknowns); 1227 return; 1228 } 1229 this.parseOptionsEnv(); 1230 auto parsed = this.parseOptions(unknowns); 1231 this.argFlags = parsed[0]; 1232 this.unknownFlags = parsed[1]; 1233 this.parseOptionsConfig(); 1234 this.parseOptionsImply(); 1235 this._options 1236 .filter!(opt => opt.settled || opt.isValid) 1237 .each!((opt) { opt.initialize; }); 1238 this.parseArguments(parsed[0]); 1239 if (this._argToOptNames.length > 0) { 1240 this._options 1241 .filter!(opt => opt.settled || opt.isValid) 1242 .each!((opt) { opt.initialize; }); 1243 } 1244 _checkConfilctOption(); 1245 _checkMissingMandatoryOption(); 1246 this.opts = this._options 1247 .filter!(opt => opt.settled) 1248 .map!(opt => tuple(opt.name, opt.get)) 1249 .assocArray; 1250 if (this.parent && this.parent._allowExposeOptionValue) { 1251 auto popts = this.parent.opts; 1252 foreach (string pkey, ref OptionVariant pvalue; popts) { 1253 this.opts[':' ~ pkey] = pvalue; 1254 } 1255 } 1256 if (!this._inject_arr.empty) { 1257 foreach (string str; _inject_arr) { 1258 string key, fkey; 1259 if (str.canFind(':')) { 1260 auto tmp = str.split(':').array; 1261 key = tmp[0]; 1262 fkey = tmp[1]; 1263 } 1264 else 1265 key = fkey = str; 1266 auto ancestors = this._getCommandAndAncestors()[1 .. $]; 1267 foreach (Command cmd; ancestors) { 1268 auto provide_arr = cmd._provide_arr; 1269 provide_arr.each!((const str) { 1270 string pkey, pfkey; 1271 if (str.canFind(':')) { 1272 auto tmp = str.split(':').array; 1273 pkey = tmp[0]; 1274 pfkey = tmp[1]; 1275 } 1276 else 1277 pkey = pfkey = str; 1278 if (fkey == pkey) 1279 this.opts[key] = cmd.opts[pfkey]; 1280 }); 1281 } 1282 } 1283 } 1284 if (this.subCommand && this.subCommand._execHandler) { 1285 this._called_sub = this.subCommand._name; 1286 this.subCommand.execSubCommand(parsed[1]); 1287 } 1288 if (this.subCommand) { 1289 this._called_sub = this.subCommand._name; 1290 this.subCommand._parseCommand(parsed[1]); 1291 } 1292 else { 1293 this.emit("action:" ~ this._name); 1294 } 1295 } 1296 1297 private 1298 mixin template InitAfterDash(alias _args, alias get_front, alias find_cmd, alias operands, alias unknowns) { 1299 import std.range : popFront; 1300 1301 auto __xfn__InitFaterDash = { 1302 auto value = get_front(_args); 1303 if (value.length == 0) { 1304 this.parsingError( 1305 "cannot end with `--`"); 1306 } 1307 auto cmd = find_cmd(value); 1308 while (!cmd && value.length) { 1309 operands ~= value; 1310 popFront(_args); 1311 value = get_front(_args); 1312 cmd = find_cmd(value); 1313 } 1314 if (cmd) { 1315 this.subCommand = cmd; 1316 popFront(_args); 1317 if (subCommand.immediately) 1318 subCommand._parseCommand(_args); 1319 unknowns ~= _args; 1320 } 1321 return 1; 1322 }(); 1323 // auto x = __xfn__xfn__InitFaterDash(); 1324 } 1325 1326 private 1327 mixin template InitVariadicOpt(alias _args, alias get_front, alias maybe_opt, alias vvm, alias name) { 1328 import std.range : empty, popFront; 1329 1330 auto __xfn_InitVariadicOpt = { 1331 auto value = get_front(_args); 1332 string[] tmps = []; 1333 while (value.length && !maybe_opt(value)) { 1334 tmps ~= value; 1335 popFront(_args); 1336 value = get_front(_args); 1337 } 1338 auto ptr = name in vvm; 1339 if (ptr) 1340 *ptr ~= tmps; 1341 else 1342 vvm[name] = tmps; 1343 return 1; 1344 }(); 1345 } 1346 1347 private 1348 mixin template InitValueOpt(alias cmd, bool requried, alias _args, 1349 alias get_front, alias maybe_opt, alias opt, alias name) { 1350 import std.range : empty, popFront; 1351 1352 static if (requried) { 1353 auto __xfn_InitValueOpt = { 1354 auto value = get_front(_args); 1355 if (value.empty || maybe_opt(value)) 1356 cmd.optionMissingArgument(opt); 1357 cmd.emit("option:" ~ name, value); 1358 if (cmd !is this) { 1359 opt.settled = false; 1360 } 1361 _args.popFront; 1362 return 1; 1363 }(); 1364 } 1365 else { 1366 auto __xfn_InitValueOpt = { 1367 auto value = get_front(_args); 1368 if (value.empty || maybe_opt(value)) 1369 cmd.emit("option:" ~ name); 1370 else { 1371 cmd.emit("option:" ~ name, value); 1372 _args.popFront; 1373 } 1374 if (cmd !is this) { 1375 opt.settled = false; 1376 } 1377 return 1; 1378 }(); 1379 } 1380 } 1381 1382 private 1383 mixin template InitOptComb(alias cmd, alias _args, alias opt, alias vvm, alias arg) { 1384 import std.array : insertInPlace; 1385 import std.format; 1386 import cmdline.option; 1387 1388 auto __xfn_InitOptComp = { 1389 string name = opt.name; 1390 if (opt.isRequired || opt.isOptional) { 1391 bool is_variadic = opt.variadic; 1392 if (!is_variadic) 1393 cmd.emit("option:" ~ name, arg[2 .. $]); 1394 else { 1395 auto ptr = name in vvm; 1396 if (ptr) 1397 *ptr ~= arg[2 .. $]; 1398 else 1399 vvm[name] = [arg[2 .. $]]; 1400 } 1401 } 1402 else if (cmd._combineFlagAndOptionalValue) { 1403 cmd.emit("option:" ~ name); 1404 _args.insertInPlace(0, "-" ~ arg[2 .. $]); 1405 } 1406 else { 1407 cmd.parsingError(format!"invalid value: `%s` for bool option `%s`"( 1408 arg[2 .. $], 1409 opt.flags 1410 )); 1411 } 1412 if (cmd !is this) { 1413 opt.settled = false; 1414 } 1415 return 1; 1416 }(); 1417 } 1418 1419 private 1420 mixin template InitOptAssign(alias cmd, alias opt, alias vvm, alias value) { 1421 import cmdline.option; 1422 1423 auto __xfn_InitOptAssign = { 1424 string name = opt.name; 1425 if (opt.isRequired || opt.isOptional) { 1426 bool is_variadic = opt.variadic; 1427 if (!is_variadic) 1428 cmd.emit("option:" ~ name, value); 1429 else { 1430 auto ptr = name in vvm; 1431 if (ptr) 1432 *ptr ~= value; 1433 else 1434 vvm[name] = [value]; 1435 } 1436 } 1437 else 1438 cmd.parsingError("invalid value: `" ~ value ~ "` for bool option " ~ opt.flags); 1439 if (cmd !is this) { 1440 opt.settled = false; 1441 } 1442 return 1; 1443 }(); 1444 } 1445 1446 private bool maybe_opt(string str) { 1447 return (str.length > 1 && str[0] == '-' && 1448 (str[1] == '-' || (str[1] >= 'A' && str[1] <= 'Z') || 1449 (str[1] >= 'a' && str[1] <= 'z'))); 1450 } 1451 1452 private string get_front(string[] strs) { 1453 return strs.empty ? "" : strs.front; 1454 } 1455 1456 private bool init_opt(Command cmd, string arg, ref string[] _args, ref string[][string] vvm) { 1457 bool is_continue = false; 1458 if (auto opt = cmd._findOption(arg)) { 1459 auto name = opt.name; 1460 bool is_variadic = opt.variadic; 1461 if (opt.isRequired) { 1462 if (!is_variadic) { 1463 mixin InitValueOpt!(cmd, true, _args, get_front, maybe_opt, opt, name); 1464 } 1465 else { 1466 mixin InitVariadicOpt!(_args, get_front, maybe_opt, vvm, name); 1467 } 1468 } 1469 else if (opt.isOptional) { 1470 if (!is_variadic) { 1471 mixin InitValueOpt!(cmd, false, _args, get_front, maybe_opt, opt, name); 1472 } 1473 else { 1474 mixin InitVariadicOpt!(_args, get_front, maybe_opt, vvm, name); 1475 } 1476 } 1477 else { 1478 cmd.emit("option:" ~ name); 1479 } 1480 is_continue = true; 1481 } 1482 if (auto nopt = cmd._findNegateOption(arg)) { 1483 cmd.emit("negate:" ~ nopt.name); 1484 is_continue = true; 1485 } 1486 return is_continue; 1487 } 1488 1489 private bool init_comb(Command cmd, string flag, string arg, ref string[] _args, ref string[][string] vvm) { 1490 bool is_continue = false; 1491 if (Option opt = cmd._findOption(flag)) { 1492 mixin InitOptComb!(cmd, _args, opt, vvm, arg); 1493 is_continue = true; 1494 } 1495 if (NegateOption nopt = cmd._findNegateOption(flag)) { 1496 if (cmd._combineFlagAndOptionalValue) { 1497 cmd.emit("negate:" ~ nopt.name); 1498 _args.insertInPlace(0, "-" ~ arg[2 .. $]); 1499 } 1500 else { 1501 cmd.parsingError(format!"invalid value: `%s` for negate option `%s`"( 1502 arg[2 .. $], 1503 nopt.flags 1504 )); 1505 } 1506 is_continue = true; 1507 } 1508 return is_continue; 1509 } 1510 1511 private bool init_assign(Command cmd, string flag, string value, ref string[][string] vvm) { 1512 bool is_continue = false; 1513 if (Option opt = cmd._findOption(flag)) { 1514 mixin InitOptAssign!(cmd, opt, vvm, value); 1515 is_continue = true; 1516 } 1517 return is_continue; 1518 } 1519 1520 private void init_variadic(Command cmd, ref string[][string] vvm) { 1521 if (!vvm) 1522 return; 1523 foreach (key, ref value; vvm) { 1524 if (cmd is this && cmd._configOption && cmd._configOption.name == key) { 1525 cmd.emit("option:" ~ key, value.reverse); 1526 } 1527 else { 1528 cmd.emit("option:" ~ key, value); 1529 if (cmd !is this) { 1530 auto opt = cmd._findOption(key); 1531 opt.settled = false; 1532 } 1533 } 1534 } 1535 vvm = null; 1536 } 1537 1538 private void init_hv(Command cmd, string arg) { 1539 if (Option help_opt = cmd._helpOption) { 1540 if (help_opt.isFlag(arg)) { 1541 cmd.emit("option:" ~ help_opt.name); 1542 } 1543 } 1544 else if (cmd._addImplicitHelpOption) { 1545 if (arg == "-h" || arg == "--help") { 1546 auto hopt = cmd._getHelpOption(); 1547 cmd.emit("option:" ~ hopt.name); 1548 } 1549 } 1550 if (Option vopt = cmd._versionOption) { 1551 if (vopt.isFlag(arg)) { 1552 cmd.emit("option:" ~ vopt.name); 1553 } 1554 } 1555 } 1556 1557 private Command find_cmd(string str) { 1558 auto _cmd = _findCommand(str); 1559 auto vcmd = this._versionCommand; 1560 auto hcmd = this._helpCommand; 1561 _cmd = !_cmd && vcmd && vcmd._name == str ? vcmd : _cmd; 1562 _cmd = !_cmd && hcmd && hcmd._name == str ? hcmd : _cmd; 1563 _cmd = _cmd ? _cmd : (!hcmd && str == "help") ? this._getHelpCommand() : null; 1564 return _cmd; 1565 } 1566 1567 private bool init_opt_command(string arg, ref string[] _args, ref string[][string] vvm, ref string[][string] pvvm) { 1568 init_hv(this, arg); 1569 if (Option copt = this._configOption) { 1570 if (copt.isFlag(arg)) { 1571 string name = copt.name; 1572 mixin InitVariadicOpt!(_args, get_front, maybe_opt, vvm, name); 1573 return true; 1574 } 1575 } 1576 if (init_opt(this, arg, _args, vvm)) { 1577 return true; 1578 } 1579 if (this.parent && this.parent._passThroughOptionValue) { 1580 if (init_imex_command!false(arg, _args, pvvm)) 1581 return true; 1582 if (init_imex_command!true(arg, _args, pvvm)) 1583 return true; 1584 if (init_opt(this.parent, arg, _args, pvvm)) 1585 return true; 1586 } 1587 return false; 1588 } 1589 1590 private bool init_imex_command(bool isExport = false)(string arg, ref string[] _args, ref string[][string] pvvm) { 1591 static if (isExport) { 1592 Option[string] _map = this.parent._export_map; 1593 NegateOption[string] _n_map = this.parent._export_n_map; 1594 } 1595 else { 1596 Option[string] _map = this._import_map; 1597 NegateOption[string] _n_map = this._import_n_map; 1598 } 1599 foreach (key, opt; _map) { 1600 if (arg != key) 1601 continue; 1602 if (init_opt(this.parent, opt.shortFlag, _args, pvvm)) 1603 return true; 1604 } 1605 foreach (key, nopt; _n_map) { 1606 if (arg != key) 1607 continue; 1608 if (init_opt(this.parent, nopt.longFlag, _args, pvvm)) 1609 return true; 1610 } 1611 return false; 1612 } 1613 1614 private bool init_opt_comb(string arg, ref string[] _args, ref string[][string] vvm, ref string[][string] pvvm) { 1615 string flag = "-" ~ arg[1]; 1616 init_hv(this, flag); 1617 if (Option copt = this._configOption) { 1618 if (copt.isFlag(flag)) { 1619 string name = copt.name; 1620 auto ptr = name in vvm; 1621 if (ptr) 1622 *ptr ~= arg[2 .. $]; 1623 else 1624 vvm[name] = [arg[2 .. $]]; 1625 return true; 1626 } 1627 } 1628 if (init_comb(this, flag, arg, _args, vvm)) 1629 return true; 1630 if (this.parent && this.parent._passThroughOptionValue) { 1631 if (init_imex_comb!false(flag, arg, _args, pvvm)) 1632 return true; 1633 if (init_imex_comb!true(flag, arg, _args, pvvm)) 1634 return true; 1635 if (init_comb(this.parent, flag, arg, _args, pvvm)) 1636 return true; 1637 } 1638 return false; 1639 } 1640 1641 private bool init_imex_comb(bool isExport = false)(string flag, string arg, 1642 ref string[] _args, ref string[][string] pvvm) { 1643 static if (isExport) { 1644 Option[string] _map = this.parent._export_map; 1645 NegateOption[string] _n_map = this.parent._export_n_map; 1646 } 1647 else { 1648 Option[string] _map = this._import_map; 1649 NegateOption[string] _n_map = this._import_n_map; 1650 } 1651 foreach (key, opt; _map) { 1652 if (flag != key) 1653 continue; 1654 if (init_comb(this.parent, opt.longFlag, arg, _args, pvvm)) 1655 return true; 1656 1657 } 1658 foreach (key, nopt; _n_map) { 1659 if (flag != key) 1660 continue; 1661 if (init_comb(this.parent, nopt.longFlag, arg, _args, pvvm)) 1662 return true; 1663 } 1664 return false; 1665 } 1666 1667 private bool init_opt_assign(in Captures!string cp, ref string[][string] vvm, ref string[][string] pvvm) { 1668 string flag = cp[1]; 1669 string value = cp[2]; 1670 if (Option copt = this._configOption) { 1671 if (copt.isFlag(flag)) { 1672 name = copt.name; 1673 auto ptr = name in vvm; 1674 if (ptr) 1675 *ptr ~= value; 1676 else 1677 vvm[name] = [value]; 1678 return true; 1679 } 1680 } 1681 if (init_assign(this, flag, value, vvm)) 1682 return true; 1683 if (this.parent && this.parent._passThroughOptionValue) { 1684 foreach (key, opt; _import_map) { 1685 if (flag != key) 1686 continue; 1687 if (init_assign(this.parent, opt.longFlag, value, pvvm)) 1688 return true; 1689 } 1690 foreach (key, opt; this.parent._export_map) { 1691 if (flag != key) 1692 continue; 1693 if (init_assign(this.parent, opt.longFlag, value, pvvm)) 1694 return true; 1695 } 1696 if (init_assign(this.parent, flag, value, pvvm)) 1697 return true; 1698 } 1699 return false; 1700 } 1701 1702 Tuple!(string[], string[]) parseOptions(in string[] argv) { 1703 string[] operands = []; 1704 string[] unknowns = []; 1705 string[] _args = argv.dup; 1706 string[][string] variadic_val_map = null; 1707 string[][string] pvariadic_val_map = null; 1708 1709 while (_args.length) { 1710 auto arg = _args.front; 1711 _args.popFront; 1712 1713 if (arg == "--") { 1714 mixin InitAfterDash!(_args, get_front, find_cmd, operands, unknowns); 1715 break; 1716 } 1717 1718 if (maybe_opt(arg)) { 1719 if (init_opt_command(arg, _args, variadic_val_map, pvariadic_val_map)) 1720 continue; 1721 } 1722 1723 if (arg.length > 2 && arg[0] == '-' && arg[1] != '-') { 1724 if (init_opt_comb(arg, _args, variadic_val_map, pvariadic_val_map)) 1725 continue; 1726 this.unknownOption("-" ~ arg[1]); 1727 } 1728 1729 auto cp = matchFirst(arg, PTN_LONGASSIGN); 1730 if (cp.length) { 1731 if (init_opt_assign(cp, variadic_val_map, pvariadic_val_map)) 1732 continue; 1733 this.unknownOption(cp[1]); 1734 } 1735 1736 if (auto _cmd = find_cmd(arg)) { 1737 this.subCommand = _cmd; 1738 if (subCommand.immediately && subCommand._actionHandler !is null) { 1739 this.parseOptionsConfig(); 1740 subCommand._parseCommand(_args); 1741 } 1742 unknowns ~= _args; 1743 break; 1744 } 1745 1746 if (maybe_opt(arg)) 1747 unknownOption(arg); 1748 operands ~= arg; 1749 } 1750 1751 init_variadic(this, variadic_val_map); 1752 init_variadic(this.parent, pvariadic_val_map); 1753 if (this.parent && this.parent._passThroughOptionValue) { 1754 this.parent 1755 ._options 1756 .filter!(opt => opt.isValid && !opt.settled) 1757 .each!((opt) { opt.initialize; }); 1758 this.parent.opts = this.parent 1759 ._options 1760 .filter!(opt => opt.settled) 1761 .map!(opt => tuple(opt.name, opt.get)) 1762 .assocArray; 1763 } 1764 return tuple(operands, unknowns); 1765 } 1766 1767 void optionMissingArgument(in Option opt) const { 1768 string message = format("option '%s' argument missing", opt.flags); 1769 this.parsingError(message); 1770 } 1771 1772 void unknownOption(string flag) const { 1773 string msg = ""; 1774 auto any_abandon = this._abandons.find!((const Option opt) => opt.isFlag( 1775 flag) || opt.name == flag); 1776 if (!any_abandon.empty) { 1777 msg = format("this option `%s` has been disable by its related negate option `--%s`", 1778 any_abandon[0].flags, any_abandon[0].name); 1779 this.parsingError(msg, "command.disableOption"); 1780 } 1781 else { 1782 string suggestion = this.getSuggestion(flag); 1783 if (this.parent && this.parent._passThroughOptionValue && suggestion.length == 0) { 1784 suggestion = this.parent.getSuggestion(flag); 1785 } 1786 msg = format("unknown option `%s` %s", flag, suggestion); 1787 this.parsingError(msg, "command.unknownOption"); 1788 } 1789 } 1790 1791 string getSuggestion(string flag) const { 1792 auto cmd = this; 1793 auto hlp = cmd._helpConfiguration; 1794 string suggestion = ""; 1795 const(string)[] more_flags; 1796 string[] candidate_flags = []; 1797 if (flag[0 .. 2] == "--" && this._showSuggestionAfterError) { 1798 more_flags = hlp.visibleOptions(cmd).map!(opt => opt.longFlag).array; 1799 candidate_flags ~= more_flags; 1800 } 1801 suggestion = suggestSimilar(flag, candidate_flags); 1802 return suggestion; 1803 } 1804 1805 void excessArguments() const { 1806 if (!this._allowExcessArguments) { 1807 this.parsingError("too much args!!!"); 1808 } 1809 } 1810 1811 void _checkMissingMandatoryOption() const { 1812 auto f = this._options.filter!(opt => opt.mandatory && !opt.settled); 1813 if (!f.empty) { 1814 auto strs = f.map!(opt => format!"`%s`"(opt.name)).join(" and "); 1815 this.parsingError(format!"the option: %s must have a valid value!"(strs)); 1816 } 1817 } 1818 1819 void _checkConfilctOption() const { 1820 auto opts = this._options 1821 .filter!(opt => opt.settled) 1822 .filter!(opt => !(opt.source == Source.Default || opt.source == Source.None || opt.source == Source 1823 .Imply)) 1824 .array; 1825 auto is_conflict = (const Option opt) { 1826 const string[] confilcts = opt.conflictsWith; 1827 foreach (name; confilcts) { 1828 opts.each!((o) { 1829 if (opt !is o && o.name == name) 1830 this.parsingError( 1831 format!"cannot set option `%s` and `%s` at the same time"(o.name, name)); 1832 }); 1833 } 1834 }; 1835 opts.each!(is_conflict); 1836 } 1837 1838 void parseArguments(in string[] _args) { 1839 auto args = _args.dup; 1840 auto get_front = () => args.empty ? "" : args.front; 1841 foreach (argument; this._arguments) { 1842 auto is_v = argument.variadic; 1843 if (!is_v) { 1844 auto value = get_front(); 1845 if (!value.length) 1846 break; 1847 argument.cliVal(value); 1848 popFront(args); 1849 } 1850 else { 1851 if (!args.length) 1852 break; 1853 argument.cliVal(args[0], args[1 .. $]); 1854 args = []; 1855 break; 1856 } 1857 } 1858 if (args.length && this._argToOptNames.length) { 1859 auto len = min(args.length, this._argToOptNames.length); 1860 foreach (index; 0 .. len) { 1861 auto opt = this._findOption(this._argToOptNames[index]); 1862 if (!opt) 1863 unknownOption(this._argToOptNames[index]); 1864 if (opt.variadic) { 1865 this.setOptionVal!(Source.Cli)(opt.name, args); 1866 opt.settled = false; 1867 args = []; 1868 break; 1869 } 1870 if (!opt.isValid || !opt.settled || opt.source != Source.Cli) { 1871 if (opt.isBoolean) { 1872 try { 1873 bool value = args[index].to!bool; 1874 this.setOptionValDirectly(opt.name, value, Source.Cli); 1875 args.popFront; 1876 } 1877 catch (ConvException e) { 1878 parsingError(format!"on bool option `%s` cannot convert the input `%s` to type `%s`"( 1879 opt.flags, 1880 args[index], 1881 bool.stringof 1882 )); 1883 } 1884 } 1885 else { 1886 this.setOptionVal!(Source.Cli)(opt.name, get_front()); 1887 opt.settled = false; 1888 args.popFront; 1889 } 1890 } 1891 } 1892 } 1893 if (args.length) 1894 this.excessArguments(); 1895 this._arguments.each!((Argument arg) { 1896 if (arg.isRequired && !arg.isValid) 1897 this.parsingError( 1898 format!"argument `%s` is required but its value is invalid"(arg._name)); 1899 }); 1900 this._arguments.each!((Argument arg) { 1901 if (arg.isValid || arg.settled) 1902 arg.initialize; 1903 }); 1904 Argument prev = null; 1905 foreach (i, arg; this._arguments) { 1906 if (prev && arg.settled && !prev.settled) 1907 this.parsingError( 1908 format!"arg should be valid in row, the prev arg `%s` is invalid, while cur arg `%s` is valid"( 1909 prev._name, arg._name 1910 )); 1911 prev = arg; 1912 } 1913 this.args = this._arguments 1914 .filter!(arg => arg.settled) 1915 .map!(arg => arg.get) 1916 .array; 1917 } 1918 1919 void parseOptionsEnv() { 1920 this._options.each!((Option opt) { opt.envVal(); }); 1921 } 1922 1923 void parseOptionsImply() { 1924 auto set_imply = (Option option) { 1925 auto imply_map = option.implyMap; 1926 foreach (string key, OptionVariant value; imply_map) { 1927 auto tmp = split(key, ':'); 1928 string name = tmp[0]; 1929 string type = tmp[1]; 1930 Option opt = _findOption(name); 1931 if (opt && opt._isMerge) { 1932 opt.settled = false; 1933 opt.implyVal(value); 1934 } 1935 else if (opt) { 1936 if (!opt.isValid) 1937 opt.implyVal(value); 1938 if (opt.source == Source.Default) { 1939 opt.settled = false; 1940 opt.implyVal(value); 1941 } 1942 } 1943 auto any_abandon = this._abandons.find!( 1944 (const Option opt) => opt.name == name); 1945 if (!opt && !any_abandon.empty) { 1946 this.unknownOption(name); 1947 } 1948 if (!opt) { 1949 string flag = format("--%s <%s-value>", name, name); 1950 string flag2 = format("--%s", name); 1951 string flag3 = format("--%s <%s-value...>", name, name); 1952 switch (type) { 1953 case int.stringof: 1954 opt = createOption!int(flag); 1955 break; 1956 case double.stringof: 1957 opt = createOption!double(flag); 1958 break; 1959 case string.stringof: 1960 opt = createOption!string(flag); 1961 break; 1962 case bool.stringof: 1963 opt = createOption!bool(flag2); 1964 break; 1965 case (int[]).stringof: 1966 opt = createOption!int(flag3); 1967 break; 1968 case (double[]).stringof: 1969 opt = createOption!double(flag3); 1970 break; 1971 case (string[]).stringof: 1972 opt = createOption!string(flag3); 1973 break; 1974 default: 1975 break; 1976 } 1977 if (opt) { 1978 opt.implyVal(value); 1979 this.addOption(opt); 1980 } 1981 } 1982 } 1983 }; 1984 this._options 1985 .filter!(opt => opt.settled || opt.isValid) 1986 .each!((opt) { opt.initialize; }); 1987 this._options 1988 .filter!(opt => opt.settled) 1989 .filter!(opt => !(opt.source == Source.Default || opt.source == Source.None || opt 1990 .source == Source 1991 .Imply)) 1992 .each!(set_imply); 1993 } 1994 1995 void parseOptionsConfig() { 1996 alias Value = const(JSONValue)*; 1997 if (this._configOption) { 1998 Value[] j_config = _processConfigFile(); 1999 this.jconfig ~= j_config; 2000 this.jconfig = this.jconfig.uniq.array; 2001 } 2002 this.jconfig.each!((jconfig) { parseConfigOptionsImpl(jconfig); }); 2003 } 2004 2005 public: 2006 /// set the name of command 2007 Self name(string str) { 2008 this._name = str; 2009 return this; 2010 } 2011 2012 /// get the name of command 2013 string name() const { 2014 return this._name.idup; 2015 } 2016 2017 /// set the version of the command. 2018 /// if `flags` and `desc` not defined, then it will automatically set the version option flag `-V, --version` and version command name `version` 2019 /// Params: 2020 /// str = the version string like `0.0.1` 2021 /// flags = the version option flag, which is a `bool` option 2022 /// desc = the description of the version command and version option 2023 /// Returns: `Self` for chain call 2024 Self setVersion(string str, string flags = "", string desc = "") { 2025 this._version = str; 2026 setVersionOption(flags, desc); 2027 string vname = this._versionOption.name; 2028 setVersionCommand(vname, desc); 2029 return this; 2030 } 2031 2032 /// get the version string of this command, if not set version, the default is `*` 2033 string getVersion() const { 2034 return this._version.idup; 2035 } 2036 2037 /// set the version option of the command, see also `Self setVersion(string str, string flags = "", string desc = "")` 2038 Self setVersionOption(string flags = "", string desc = "") { 2039 assert(!this._versionOption); 2040 flags = flags == "" ? "-V, --version" : flags; 2041 desc = desc == "" ? "output the version number" : desc; 2042 auto vopt = createOption(flags, desc); 2043 if (auto help_opt = this._helpOption) { 2044 if (vopt.matchFlag(help_opt)) 2045 this.error(format!"Cannot add option '%s' due to confliction help option `%s`"(vopt.flags, help_opt 2046 .flags)); 2047 } 2048 else if (this._addImplicitHelpOption && (vopt.shortFlag == "-h" || vopt.longFlag == "--help")) { 2049 this.error(format!"Cannot add option '%s' due to confliction help option `%s`"(vopt.flags, "-h, --help")); 2050 } 2051 if (auto config_opt = this._configOption) { 2052 if (vopt.matchFlag(config_opt)) 2053 this.error(format!"Cannot add option '%s' due to confliction config option `%s`"(vopt.flags, config_opt 2054 .flags)); 2055 } 2056 this._versionOption = vopt; 2057 string vname = vopt.name; 2058 this.on("option:" ~ vname, () { 2059 this._outputConfiguration.writeOut(this._version ~ "\n"); 2060 this._exitSuccessfully(); 2061 }); 2062 return this; 2063 } 2064 2065 /// set the version option of the command, see also `Self setVersion(string str, string flags = "", string desc = "")` 2066 Self addVersionOption(string flags = "", string desc = "") { 2067 return setVersionOption(flags, desc); 2068 } 2069 2070 /// custom the version option 2071 /// Params: 2072 /// opt = the version option 2073 /// action = the action callback when parsing the flag of version option, if `null`, then do nothing and exit mutely 2074 /// Returns: `Self` for chain call 2075 Self addVersionOption(Option opt, void delegate() action = null) { 2076 assert(!this._versionOption); 2077 this._versionOption = opt; 2078 if (!action) { 2079 this.on("option:" ~ opt.name, () { 2080 this._outputConfiguration.writeOut(this._version ~ "\n"); 2081 this._exitSuccessfully(); 2082 }); 2083 } 2084 else 2085 this.on("option:" ~ opt.name, () { 2086 action(); 2087 this._exitSuccessfully(); 2088 }); 2089 return this; 2090 } 2091 2092 /// set version command, see also `Self setVersion(string str, string flags = "", string desc = "")` 2093 Self setVersionCommand(string flags = "", string desc = "") { 2094 assert(!this._versionCommand); 2095 flags = flags == "" ? "version" : flags; 2096 desc = desc == "" ? "output the version number" : desc; 2097 Command cmd = createCommand(flags).description(desc); 2098 cmd.setHelpOption(false); 2099 cmd.setHelpCommand(false); 2100 string vname = cmd._name; 2101 if (auto help_cmd = this._helpCommand) { 2102 auto help_cmd_name_arr = help_cmd._aliasNames ~ help_cmd._name; 2103 auto none = help_cmd_name_arr.find!(name => vname == name).empty; 2104 if (!none) { 2105 string help_cmd_names = help_cmd_name_arr.join("|"); 2106 this.error( 2107 format!"cannot add command `%s` as this command name cannot be same as the name of help command `%s`"( 2108 vname, help_cmd_names)); 2109 } 2110 } 2111 else if (this._addImplicitHelpCommand) { 2112 string help_cmd_names = "help"; 2113 if (vname == help_cmd_names) { 2114 this.error( 2115 format!"cannot add command `%s` as this command name cannot be same as the name of help command `%s`"( 2116 vname, help_cmd_names)); 2117 } 2118 } 2119 this.on("command:" ~ vname, () { 2120 this._outputConfiguration.writeOut(this._version ~ "\n"); 2121 this._exitSuccessfully(); 2122 }); 2123 ActionCallback fn = () { 2124 this._outputConfiguration.writeOut(this._version ~ "\n"); 2125 this._exitSuccessfully(); 2126 }; 2127 cmd.parent = this; 2128 cmd.action(fn, true); 2129 this._versionCommand = cmd; 2130 return this; 2131 } 2132 2133 /// set version command, see also `Self setVersion(string str, string flags = "", string desc = "")` 2134 Self addVersionCommand(string flags = "", string desc = "") { 2135 return setVersionCommand(flags, desc); 2136 } 2137 2138 /// set version command, see also `Self setVersion(string str, string flags = "", string desc = "")` 2139 Self addVersionCommand(Command cmd) { 2140 assert(!this._versionCommand); 2141 string vname = cmd._name; 2142 if (auto help_cmd = this._helpCommand) { 2143 auto help_cmd_name_arr = help_cmd._aliasNames ~ help_cmd._name; 2144 auto none = help_cmd_name_arr.find!(name => vname == name).empty; 2145 if (!none) { 2146 string help_cmd_names = help_cmd_name_arr.join("|"); 2147 this.error( 2148 format!"cannot add command `%s` as this command name cannot be same as the name of help command `%s`"( 2149 vname, help_cmd_names)); 2150 } 2151 } 2152 else if (this._addImplicitHelpCommand) { 2153 string help_cmd_names = "help"; 2154 if (vname == help_cmd_names) { 2155 this.error(format!"cannot add command `%s` as this command name cannot be same as the name of help command `%s`"( 2156 vname, help_cmd_names)); 2157 } 2158 } 2159 cmd.parent = this; 2160 this._versionCommand = cmd; 2161 return this; 2162 } 2163 2164 /// set the config option, if `flags` not defined, then config option's flags is `-C, --config <config-dirs...>`; 2165 /// if `desc` not defined, then description is by default; 2166 /// if `defaultDir` not defined, then the `defaultDir` would be `${CURRENT_WORKER_DIR}`, which automatically store the path to dir of config file 2167 /// Params: 2168 /// flags = the flags of config option 2169 /// desc = the description of config option 2170 /// defaultDir = the built-in path to the dir of config file, which priority is higher than `${YOUR_PROGRAM_DIR}/{YOUR_PROGRAM_NAME}.config.json` 2171 /// Returns: `Self` for chain call 2172 Self setConfigOption(string flags = "", string desc = "", string defaultDir = "") { 2173 assert(!this._configOption); 2174 flags = flags == "" ? "-C, --config <config-dirs...>" : flags; 2175 defaultDir = defaultDir == "" ? thisExePath.dirName : defaultDir; 2176 string cwd = getcwd(); 2177 desc = desc == "" ? 2178 format("define the directories of the config file," ~ 2179 "if not specified, the config file name would be" ~ 2180 " `%s.config.json` and it is on the dir `%s` and current woker dir `%s`", 2181 this._name, defaultDir, cwd) : desc; 2182 this._configPaths ~= defaultDir; 2183 this._configPaths ~= cwd; 2184 this._configPaths = this._configPaths.uniq.array; 2185 auto copt = createOption!string(flags, desc); 2186 if (auto help_opt = this._helpOption) { 2187 if ( 2188 copt.matchFlag( 2189 help_opt)) 2190 this.error( 2191 format!"Cannot add option '%s' 2192 due to confliction help option `%s`"(copt.flags, help_opt.flags)); 2193 } 2194 else if (this._addImplicitHelpOption && (copt.shortFlag == "-h" || copt.longFlag == "--help")) { 2195 this.error( 2196 format!"Cannot add option '%s' 2197 due to confliction help option `%s`"(copt.flags, "-h, --help")); 2198 } 2199 if ( 2200 auto version_opt = this 2201 ._versionOption) { 2202 if ( 2203 copt.matchFlag( 2204 version_opt)) 2205 this.error( 2206 format!"Cannot add option '%s' 2207 due to confliction version option `%s`"(copt.flags, version_opt.flags)); 2208 } 2209 this._configOption = copt; 2210 string cname = copt.name; 2211 this.on("option:" ~ cname, ( 2212 string[] configPaths) { 2213 string current_dir = getcwd(); 2214 configPaths.each!((path) { 2215 auto rpath = buildPath(current_dir, path); 2216 auto npath = exists(rpath) ? rpath : path; 2217 if (!exists(npath)) { 2218 parsingError(format!"invalid path `%s` or `%s`"(rpath, path)); 2219 } 2220 this._configPaths ~= npath; 2221 }); 2222 this._configPaths = this._configPaths.uniq.array; 2223 }); 2224 return this; 2225 } 2226 2227 package void parseConfigOptionsImpl(const(JSONValue)* config) { 2228 alias Value = const(JSONValue)*; 2229 if (this.subCommand && !this.subCommand._execHandler) { 2230 string sub_name = this.subCommand._name; 2231 Value sub_config = sub_name in *config; 2232 if (sub_config && this.subCommand.jconfig.count(sub_config) == 0) 2233 this.subCommand.jconfig ~= sub_config; 2234 } 2235 if (this.subCommand && this.subCommand.immediately) 2236 return; 2237 Value[string] copts; 2238 Value[] cargs; 2239 if (Value ptr = "arguments" in *config) { 2240 if (ptr.type != JSONType.ARRAY) { 2241 this.parsingError("the `arguments`'s value must be array!"); 2242 } 2243 ptr.array.each!((const ref JSONValue ele) { 2244 if (ele.type == JSONType.NULL || ele.type == JSONType.OBJECT) { 2245 this.parsingError("the `argument`'s element value cannot be object or null!"); 2246 } 2247 cargs ~= &ele; 2248 }); 2249 } 2250 if (Value ptr = "options" in *config) { 2251 if (ptr.type != JSONType.OBJECT) { 2252 this.parsingError("the `options`'s value must be object!"); 2253 } 2254 foreach (string key, const ref JSONValue ele; ptr.object) { 2255 if (ele.type == JSONType.NULL || ele.type == JSONType.OBJECT) { 2256 this.parsingError("the `option`'s value cannot be object or null!"); 2257 } 2258 if (ele.type == JSONType.ARRAY) { 2259 if (ele.array.length < 1) 2260 this.parsingError( 2261 "if the `option`'s value is array, then its length cannot be 0"); 2262 bool all_int = ele.array.all!((ref e) => e.type == JSONType.INTEGER); 2263 bool all_double = ele.array.all!((ref e) => e.type == JSONType.FLOAT); 2264 bool all_string = ele.array.all!((ref e) => e.type == JSONType.STRING); 2265 if (!(all_int || all_double || all_string)) { 2266 this.parsingError("if the `option`'s value is array, then its element type must all be int or double or string the same"); 2267 } 2268 } 2269 copts[key] = &ele; 2270 } 2271 } 2272 auto get_front = () => cargs.empty ? null : cargs.front; 2273 auto test_regulra = () { 2274 bool all_int = cargs.all!((ref e) => e.type == JSONType.INTEGER); 2275 bool all_double = cargs.all!((ref e) => e.type == JSONType.FLOAT); 2276 bool all_string = cargs.all!((ref e) => e.type == JSONType.STRING); 2277 if (!(all_int || all_double || all_string)) { 2278 this.parsingError( 2279 "the variadic `arguments`'s element type must all be int or double or string the same"); 2280 } 2281 }; 2282 auto assign_arg_arr = (Argument arg) { 2283 test_regulra(); 2284 auto tmp = cargs[0]; 2285 switch (tmp.type) { 2286 case JSONType.INTEGER: 2287 arg.configVal(cargs.map!((ref ele) => cast(int) ele.get!int).array); 2288 break; 2289 case JSONType.FLOAT: 2290 arg.configVal(cargs.map!((ref ele) => cast(double) ele.get!double).array); 2291 break; 2292 case JSONType.STRING: 2293 arg.configVal(cargs.map!((ref ele) => cast(string) ele.get!string).array); 2294 break; 2295 default: 2296 break; 2297 } 2298 }; 2299 foreach (Argument argument; this._arguments) { 2300 auto is_v = argument.variadic; 2301 if (!is_v) { 2302 if (Value value = get_front()) { 2303 mixin AssignOptOrArg!(argument, value); 2304 cargs.popFront(); 2305 } 2306 else 2307 break; 2308 } 2309 else { 2310 if (cargs.length) { 2311 assign_arg_arr(argument); 2312 break; 2313 } 2314 } 2315 } 2316 foreach (string key, Value value; copts) { 2317 Option opt = _findOption(key); 2318 if (opt) { 2319 mixin AssignOptOrArg!(opt, value); 2320 } 2321 else { 2322 this.unknownOption(key); 2323 } 2324 } 2325 } 2326 2327 private mixin template AssignOptOrArg(alias target, alias src) 2328 if (is(typeof(src) == const(JSONValue)*)) { 2329 static if (is(typeof(target) == Argument)) { 2330 Argument arg = target; 2331 } 2332 else static if (is(typeof(target) == Option)) { 2333 Option arg = target; 2334 } 2335 else { 2336 static assert(false); 2337 } 2338 const(JSONValue)* val = src; 2339 2340 static if (is(typeof(target) == Option)) { 2341 auto assign_arr = () { 2342 auto tmp = (val.array)[0]; 2343 switch (tmp.type) { 2344 case JSONType.INTEGER: 2345 arg.configVal(val.array.map!((ref ele) => cast(int) ele.get!int).array); 2346 break; 2347 case JSONType.FLOAT: 2348 arg.configVal(val.array.map!((ref ele) => cast(double) ele.get!double).array); 2349 break; 2350 case JSONType.STRING: 2351 arg.configVal(val.array.map!((ref ele) => cast(string) ele.get!string).array); 2352 break; 2353 default: 2354 break; 2355 } 2356 }; 2357 } 2358 2359 auto assign = () { 2360 switch (val.type) { 2361 case JSONType.INTEGER: 2362 arg.configVal(cast(int) val.get!int); 2363 break; 2364 case JSONType.FLOAT: 2365 arg.configVal(cast(double) val.get!double); 2366 break; 2367 case JSONType.STRING: 2368 arg.configVal(cast(string) val.get!string); 2369 break; 2370 case JSONType.FALSE, JSONType.TRUE: 2371 arg.configVal(cast(bool) val.get!bool); 2372 break; 2373 static if (is(typeof(target) == Option)) { 2374 case JSONType.ARRAY: 2375 assign_arr(); 2376 break; 2377 } 2378 default: 2379 break; 2380 } 2381 return 0; 2382 }; 2383 auto _x_Inner_ff = assign(); 2384 } 2385 2386 package const(JSONValue)*[] _processConfigFile() const { 2387 const(JSONValue)*[] tmp = []; 2388 foreach (path; this._configPaths) { 2389 string config_file = buildPath(path, format("%s.config.json", this._name)); 2390 if (config_file.length > 12 && config_file[$ - 12 .. $] == ".config.json" 2391 && exists(config_file)) { 2392 try { 2393 string raw = readText(config_file); 2394 auto rele = new JSONValue(parseJSON(raw)); 2395 if (rele.type != JSONType.OBJECT) { 2396 parsingError( 2397 format!"the json must be object in json file `%s`"(config_file)); 2398 } 2399 tmp ~= rele; 2400 } 2401 catch (Exception e) { 2402 this.parsingError(e.msg); 2403 } 2404 } 2405 } 2406 return tmp; 2407 } 2408 2409 package void _helpCommandAction(in OptsWrap _, in ArgWrap hcommand) { 2410 if (hcommand.isValid) { 2411 auto sub_cmd_name = hcommand.get!string; 2412 auto sub_cmd = this._findCommand(sub_cmd_name); 2413 auto vcmd = this._versionCommand; 2414 sub_cmd = sub_cmd ? sub_cmd : vcmd && vcmd._name == sub_cmd_name ? vcmd : null; 2415 if (!sub_cmd || sub_cmd._hidden) 2416 this.parsingError("can not find the sub command `" ~ sub_cmd_name ~ "`!"); 2417 if (sub_cmd._execHandler) { 2418 sub_cmd.execSubCommand([ 2419 this._externalCmdHelpFlagMap[sub_cmd._name] 2420 ]); 2421 } 2422 sub_cmd.help(); 2423 } 2424 this.help(); 2425 } 2426 2427 /// set the help command 2428 Self setHelpCommand(string flags = "", string desc = "") { 2429 assert(!this._helpCommand); 2430 bool has_sub_cmd = this._versionCommand !is null || !this._commands.find!( 2431 cmd => !cmd._hidden).empty; 2432 flags = flags == "" ? has_sub_cmd ? "help [command]" : "help" : flags; 2433 desc = desc == "" ? "display help for command" : desc; 2434 Command help_cmd = has_sub_cmd ? createCommand!(string)(flags, desc) : createCommand(flags, desc); 2435 help_cmd.setHelpOption(false); 2436 help_cmd.setHelpCommand(false); 2437 string hname = help_cmd._name; 2438 if (auto verison_cmd = this._versionCommand) { 2439 auto version_cmd_name_arr = verison_cmd._aliasNames ~ verison_cmd._name; 2440 auto none = version_cmd_name_arr.find!(name => hname == name).empty; 2441 if (!none) { 2442 string version_cmd_names = version_cmd_name_arr.join("|"); 2443 this.error( 2444 format!"cannot add command `%s` as this command name cannot be same as 2445 the name of version command `%s`"( 2446 hname, version_cmd_names)); 2447 } 2448 } 2449 help_cmd.parent = this; 2450 help_cmd.action(&this._helpCommandAction, true); 2451 this._helpCommand = help_cmd; 2452 return setHelpCommand(true); 2453 } 2454 2455 /// enable or disable the help command 2456 Self setHelpCommand(bool enable) { 2457 this._addImplicitHelpCommand = enable; 2458 return this; 2459 } 2460 2461 /// add the help command 2462 Self addHelpCommand(Command cmd) { 2463 assert(!this._helpCommand); 2464 string[] hnames = cmd._aliasNames ~ cmd._name; 2465 if (auto verison_cmd = this._versionCommand) { 2466 auto version_cmd_name_arr = verison_cmd._aliasNames ~ verison_cmd._name; 2467 auto none = version_cmd_name_arr.find!((name) { 2468 return !hnames.find!(h => h == name).empty; 2469 }).empty; 2470 if (!none) { 2471 string version_cmd_names = version_cmd_name_arr.join("|"); 2472 this.error( 2473 format!"cannot add command `%s` as this command name cannot be same as 2474 the name of version command `%s`"( 2475 hnames.join("|"), version_cmd_names)); 2476 } 2477 } 2478 cmd.parent = this; 2479 cmd.action(&this._helpCommandAction, true); 2480 this._helpCommand = cmd; 2481 return setHelpCommand(true); 2482 } 2483 2484 /// add the help command 2485 Self addHelpCommand(string flags = "", string desc = "") { 2486 return this.setHelpCommand(flags, desc); 2487 } 2488 2489 package Command _getHelpCommand() { 2490 if (!this._addImplicitHelpCommand) 2491 return null; 2492 if (!this._helpCommand) 2493 this.setHelpCommand(); 2494 return this._helpCommand; 2495 } 2496 2497 /// set the help option 2498 Self setHelpOption(string flags = "", string desc = "") { 2499 assert(!this._helpOption); 2500 flags = flags == "" ? "-h, --help" : flags; 2501 desc = desc == "" ? "display help for command" : desc; 2502 auto hopt = createOption(flags, desc); 2503 if (auto config_opt = this._configOption) { 2504 if (hopt.matchFlag(config_opt)) 2505 this.error( 2506 format!"Cannot add option '%s' 2507 due to confliction config option `%s`"(hopt.flags, config_opt.flags)); 2508 } 2509 if (auto version_opt = this._versionOption) { 2510 if (hopt.matchFlag(version_opt)) 2511 this.error( 2512 format!"Cannot add option '%s' 2513 due to confliction version option `%s`"(hopt.flags, version_opt.flags)); 2514 } 2515 this._helpOption = hopt; 2516 this.on("option:" ~ hopt.name, () { this.help(); }); 2517 return setHelpOption(true); 2518 } 2519 2520 /// enable the help option or not 2521 Self setHelpOption(bool enable) { 2522 this._addImplicitHelpOption = enable; 2523 return this; 2524 } 2525 2526 /// add the help option 2527 Self addHelpOption(Option option, void delegate() action = null) { 2528 assert(!this._helpOption); 2529 this._helpOption = option; 2530 if (!action) 2531 this.on("option:" ~ option.name, () { this.help(); }); 2532 else 2533 this.on("option:" ~ option.name, () { 2534 action(); 2535 this._exitSuccessfully(); 2536 }); 2537 return setHelpOption(true); 2538 } 2539 2540 /// add the help option 2541 Self addHelpOption(string flags = "", string desc = "") { 2542 return this.setHelpOption(flags, desc); 2543 } 2544 2545 /// disable the help support 2546 Self disableHelp() { 2547 setHelpCommand(false); 2548 setHelpOption(false); 2549 return this; 2550 } 2551 2552 package Option _getHelpOption() { 2553 if (!this._addImplicitHelpCommand) 2554 return null; 2555 if (!this._helpOption) 2556 this.setHelpOption(); 2557 return this._helpOption; 2558 } 2559 2560 package void outputHelp(bool isErrorMode = false) const { 2561 auto writer = isErrorMode ? 2562 this._outputConfiguration.writeErr : this._outputConfiguration.writeOut; 2563 auto ancestors = cast(Command[]) _getCommandAndAncestors(); 2564 ancestors.reverse.each!( 2565 cmd => cmd.emit("beforeAllHelp", isErrorMode) 2566 ); 2567 this.emit("beforeHelp", isErrorMode); 2568 writer(helpInfo(isErrorMode) ~ "\n"); 2569 this.emit("afterHelp", isErrorMode); 2570 ancestors.each!(cmd => cmd.emit("afterAllHelp", isErrorMode) 2571 ); 2572 } 2573 2574 /// generate the help info 2575 /// Params: 2576 /// isErrorMode = turn on the error mode, which would make the info output to this command's error output 2577 /// Returns: the string of help info 2578 string helpInfo(bool isErrorMode = false) const { 2579 auto helper = cast(Help) this._helpConfiguration; 2580 helper.helpWidth = isErrorMode ? 2581 this._outputConfiguration.getErrHelpWidth() : this._outputConfiguration.getOutHelpWidth(); 2582 return helper.formatHelp(this); 2583 } 2584 2585 /// invoke the help if the help support is not disabled 2586 /// Params: 2587 /// isErrorMode = turn on the error mode, which would make the info output to this command's error output 2588 void help(bool isErrorMode = false) { 2589 this.outputHelp(isErrorMode); 2590 if (isErrorMode) 2591 this._exitErr("(outputHelp)", "command.help"); 2592 this._exit(0); 2593 } 2594 2595 /// add a appendent help text at specified position 2596 /// Params: 2597 /// pos = the postion to insert at 2598 /// text = the appendent hlp text 2599 /// Returns: `Self` for chain call 2600 Self addHelpText(AddHelpPos pos, string text) { 2601 assert(this._addImplicitHelpCommand || this._addImplicitHelpOption); 2602 string help_event = pos ~ "Help"; 2603 this.on(help_event, (bool isErrMode) { 2604 if (text.length) { 2605 auto writer = isErrMode ? 2606 this._outputConfiguration.writeErr : this._outputConfiguration.writeOut; 2607 writer(text ~ "\n"); 2608 } 2609 }); 2610 return this; 2611 } 2612 2613 /// whether sort sub commands when invoke help, default: false 2614 Self sortSubCommands(bool enable = true) { 2615 this._helpConfiguration.sortSubCommands = enable; 2616 return this; 2617 } 2618 2619 /// whether sort sub options when invoke help, default: false 2620 Self sortOptions(bool enable = true) { 2621 this._helpConfiguration.sortOptions = enable; 2622 return this; 2623 } 2624 2625 /// whether show the global options when invoke help, default: false 2626 Self showGlobalOptions(bool enable = true) { 2627 this._helpConfiguration.showGlobalOptions = enable; 2628 return this; 2629 } 2630 2631 package void _outputHelpIfRequested(string[] flags) { 2632 auto help_opt = this._getHelpOption(); 2633 bool help_requested = help_opt !is null && !flags.find!( 2634 flag => help_opt.isFlag(flag)).empty; 2635 if (help_requested) { 2636 this.outputHelp(); 2637 this._exitSuccessfully(); 2638 } 2639 } 2640 2641 static foreach (Action; ActionCallBackSeq) { 2642 /// define the action at the end of parsing 2643 /// Params: 2644 /// Fn = the action call back 2645 mixin SetActionFn!Action; 2646 } 2647 2648 private mixin template SetActionFn(Fn) { 2649 public Self action(Fn fn, bool immediately = false) { 2650 this.immediately = immediately; 2651 enum len = Parameters!(fn).length; 2652 auto listener = () { 2653 static if (len == 0) { 2654 fn(); 2655 } 2656 else { 2657 this.opts = this.opts is null ? 2658 this._options 2659 .filter!(opt => opt.settled) 2660 .map!(opt => tuple(opt.name, opt.get)) 2661 .assocArray : this.opts; 2662 this.args = this.args.empty ? 2663 this._arguments 2664 .filter!(arg => arg.settled) 2665 .map!(arg => arg.get) 2666 .array : this.args; 2667 OptsWrap wopts = OptsWrap(this.opts); 2668 static if (len == 1) { 2669 fn(wopts); 2670 } 2671 else { 2672 auto nlen = len - 1; 2673 ArgWrap[] wargs; 2674 if (this.args.length >= nlen) { 2675 wargs = this.args[0 .. nlen].map!((return a) => ArgWrap(a)).array; 2676 } 2677 else { 2678 wargs = this.args.map!(a => ArgWrap(a)).array; 2679 ulong less_num = nlen - this.args.length; 2680 foreach (_; 0 .. less_num) { 2681 wargs ~= ArgWrap(null); 2682 } 2683 } 2684 static if (len == 2) { 2685 fn(wopts, wargs[0]); 2686 } 2687 static if (len == 3) { 2688 fn(wopts, wargs[0], wargs[1]); 2689 } 2690 static if (len == 4) { 2691 fn(wopts, wargs[0], wargs[1], wargs[2]); 2692 } 2693 static if (len == 5) { 2694 fn(wopts, wargs[0], wargs[1], wargs[2], wargs[3]); 2695 } 2696 static if (len == 6) { 2697 fn(wopts, wargs[0], wargs[1], wargs[2], wargs[3], wargs[4]); 2698 } 2699 } 2700 } 2701 this._exitSuccessfully(); 2702 }; 2703 this._actionHandler = listener; 2704 this.on("action:" ~ this._name, () { 2705 if (this._actionHandler) 2706 this._actionHandler(); 2707 }); 2708 return this; 2709 } 2710 } 2711 2712 /// get the options' value map wrap by `OptsWrap`. remember use it after parsing or at the action callabck 2713 inout(OptsWrap) getOpts() inout { 2714 return inout OptsWrap(this.opts); 2715 } 2716 2717 /// get the array of arguments' value wrap by `ArgWrap`. remember use it after parsing or at the action callabck 2718 ArgWrap[] getArgs() const { 2719 auto len = this._arguments.length; 2720 ArgWrap[] wargs; 2721 if (this.args.length >= len) { 2722 wargs = this.args[0 .. len].map!((return a) => ArgWrap(a)).array; 2723 } 2724 else { 2725 wargs = this.args.map!(a => ArgWrap(a)).array; 2726 ulong less_num = len - this.args.length; 2727 foreach (_; 0 .. less_num) { 2728 wargs ~= ArgWrap(null); 2729 } 2730 } 2731 return wargs; 2732 } 2733 2734 /// set the alias of command 2735 Self aliasName(string aliasStr) { 2736 Command command = this; 2737 if (this._commands.length != 0 && this._commands[$ - 1]._execHandler) { 2738 command = this._commands[$ - 1]; 2739 } 2740 if (aliasStr == command._name) 2741 this.error(format!"cannot add alias `%s` to command `%s` as they cannot be same"( 2742 aliasStr, command 2743 ._name 2744 )); 2745 auto matchingCommand = this.parent ? this._findCommand(aliasStr) : null; 2746 if (matchingCommand) { 2747 auto exitCmdNames = [matchingCommand.name()]; 2748 exitCmdNames ~= matchingCommand.aliasNames; 2749 auto namesStr = exitCmdNames.join("|"); 2750 this.error( 2751 format!"cannot add alias %s to command %s as already have command %s"( 2752 aliasStr, this.name, namesStr)); 2753 } 2754 command._aliasNames ~= aliasStr; 2755 return this; 2756 } 2757 2758 /// get the first alias of command 2759 string aliasName() const { 2760 if (this._aliasNames.length == 0) { 2761 this.error("the num of alias names cannot be zero"); 2762 } 2763 return this._aliasNames[0]; 2764 } 2765 2766 /// set a sequence of aliases of command 2767 Self aliasNames(string[] aliasStrs) { 2768 aliasStrs.each!(str => this.aliasName(str)); 2769 return this; 2770 } 2771 2772 /// get the sequence of aliases of command 2773 const(string[]) aliasNames() const { 2774 return this._aliasNames; 2775 } 2776 2777 inout(Argument) _findArgument(string name) inout { 2778 auto validate = (inout Argument arg) { return name == arg._name; }; 2779 auto tmp = this._arguments.find!validate; 2780 return tmp.empty ? null : tmp[0]; 2781 } 2782 2783 inout(Command) _findCommand(string name) inout { 2784 auto validate = (inout Command cmd) { 2785 auto result = cmd._name == name || cmd._aliasNames.any!(n => n == name); 2786 return result; 2787 }; 2788 auto tmp = this._commands.find!validate; 2789 return tmp.empty ? null : tmp[0]; 2790 } 2791 2792 inout(Option) _findOption(string flag) inout { 2793 auto tmp = this._options.find!(opt => opt.isFlag(flag) || flag == opt.name); 2794 return tmp.empty ? null : tmp[0]; 2795 } 2796 2797 inout(NegateOption) _findNegateOption(string flag) inout { 2798 auto tmp = this._negates.find!(opt => opt.isFlag(flag) || flag == opt.name); 2799 return tmp.empty ? null : tmp[0]; 2800 } 2801 2802 /// add argument for command 2803 Self addArgument(Argument argument) { 2804 auto args = this._arguments; 2805 Argument prev_arg = args.length ? args[$ - 1] : null; 2806 if (prev_arg && prev_arg.variadic) { 2807 this.error(format!"cannot add argument `%s` after the variadic argument `%s`"( 2808 argument._name, prev_arg 2809 ._name 2810 )); 2811 } 2812 if (prev_arg && prev_arg.isOptional && argument.isRequired) { 2813 this.error(format!"cannot add required argument `%s` after the optional argument `%s`"( 2814 argument._name, prev_arg 2815 ._name 2816 )); 2817 } 2818 _registerArgument(argument); 2819 return this; 2820 } 2821 2822 /// define the argument for command 2823 Self argument(T)(string name, string desc = "") if (isArgValueType!T) { 2824 auto arg = createArgument!T(name, desc); 2825 this.addArgument(arg); 2826 return this; 2827 } 2828 2829 /// define the argument with default value for command, used for no-variadic argument 2830 Self argument(T)(string name, string desc, T val) if (isBaseArgValueType!T) { 2831 auto arg = createArgument!T(name, desc); 2832 arg.defaultVal(val); 2833 this.addArgument(arg); 2834 return this; 2835 } 2836 2837 /// define the argument with default value for command, used for variadic argument 2838 Self argument(T)(string name, string desc, T val, T[] rest...) 2839 if (isBaseArgValueType!T && !is(T == bool)) { 2840 auto arg = createArgument!T(name, desc); 2841 arg.defaultVal(val, rest); 2842 this.addArgument(arg); 2843 return this; 2844 } 2845 2846 /// define the argument with default value for command, used for variadic argument 2847 Self argument(T : U[], U)(string name, string desc, T defaultVal) 2848 if (!is(U == bool) && isBaseArgValueType!U) { 2849 assert(defaultVal.length >= 1); 2850 auto arg = createArgument!T(name, desc); 2851 arg.defaultVal(defaultVal); 2852 this.addArgument(arg); 2853 return this; 2854 } 2855 2856 /// define manly arguments for command 2857 Self arguments(Args...)(string names) { 2858 enum args_num = Args.length; 2859 auto arg_strs = names.strip().split(" "); 2860 assert(args_num == arg_strs.length); 2861 static foreach (index, T; Args) { 2862 this.argument!T(arg_strs[index]); 2863 } 2864 return this; 2865 } 2866 2867 /// configure the output of command 2868 Self configureOutput(OutputConfiguration config) { 2869 this._outputConfiguration = config; 2870 return this; 2871 } 2872 2873 /// get the output cofiguration of command 2874 inout(OutputConfiguration) configureOutput() inout { 2875 return this._outputConfiguration; 2876 } 2877 2878 /// whether show help info when parsing occur error, default: `true` 2879 Self showHelpAfterError(bool displayHelp = true) { 2880 this._showHelpAfterError = displayHelp; 2881 return this; 2882 } 2883 2884 /// whether show suggestion info when parsing occur error, default: `true` 2885 Self showSuggestionAfterError(bool displaySuggestion = true) { 2886 this._showSuggestionAfterError = displaySuggestion; 2887 return this; 2888 } 2889 2890 /// whether allow combine flag mode like `ls -al`, default: `true` 2891 Self comineFlagAndOptionValue(bool combine) { 2892 this._combineFlagAndOptionalValue = combine; 2893 return this; 2894 } 2895 2896 /// whether allow excess argument, default: `true` 2897 Self allowExcessArguments(bool allow) { 2898 this._allowExcessArguments = allow; 2899 return this; 2900 } 2901 2902 /// whether allow variadic options' final values merge from different source, default: `true` 2903 Self allowVariadicMerge(bool allow) { 2904 this._allowVariadicMerge = allow; 2905 return this; 2906 } 2907 2908 /// whether allow expose options' values to its sub commands, default: `false` 2909 Self allowExposeOptionValue(bool allow) { 2910 this._allowExposeOptionValue = allow; 2911 return this; 2912 } 2913 2914 /// whether allow pass through options' flags(config, help, version options are not included) 2915 /// behind its sub commands, default: `false` 2916 Self passThrough(bool allow = true) { 2917 this._passThroughOptionValue = allow; 2918 return this; 2919 } 2920 2921 /// import the option flag as new flag from parent command, so that this new flag can be pased as the 2922 /// the option flag of parent command. this member function aims at avoidind flag conflict between 2923 /// the command and its parent command, after using `passThrough` on parent command. 2924 /// the parent command must exist and parent command must have used `passThrough` and the `flag` must 2925 /// be the flag of one of the parent command's option or negate option. 2926 /// remeber that when parsing the new flag, the option's new flag is prior to the negate option ones. 2927 /// when parsing command line option flags, imported flag is prior to exported one, and the export one is 2928 /// prior to the ordinary pass-through option flag. 2929 /// `isNegate` decide whether import negate option or non-negate one, default: `false` 2930 /// Params: 2931 /// flag = the flag(or name) of one of the parent command's option or negate option 2932 /// aliasFlags = the new flags 2933 /// Returns: `Self` for chain call 2934 Self importAs(bool isNegate = false)(string flag, string[] aliasFlags...) { 2935 if (!this.parent || !this.parent._passThroughOptionValue) 2936 this.error(format("cannot use member function `Command.importAs` in command `%s` for 2937 the parent command is not found or its parent 2938 command has not used member function `Command.passThrough`", this._name)); 2939 assert(aliasFlags.length); 2940 static if (isNegate) { 2941 auto flg = imExAsImpl!(NegateOption)(flag, aliasFlags); 2942 enum string word = "negate "; 2943 } 2944 else { 2945 auto flg = imExAsImpl!(Option)(flag, aliasFlags); 2946 enum string word = ""; 2947 } 2948 if (!flg) 2949 this.error(format("cannot find the %soption `%s` in `Command.importAs` in command `%s`", 2950 word, flag, this._name)); 2951 return this; 2952 } 2953 2954 /// import the neagte option flag, see `Command.importAs` 2955 alias importNAs = importAs!true; 2956 2957 /// export the option flag as new flag to sub command, so that this new flag can be pased as the 2958 /// the option flag of parent command. this member function aims at avoidind flag conflict between 2959 /// the command and its sub command, after using `passThrough` on this command. 2960 /// this function would be automatically call `Command.passThrough`. 2961 /// the `flag` must be the flag of one of the command's option or negate option. 2962 /// remeber that when parsing the new flag, the option's new flag is prior to the negate option ones. 2963 /// when parsing command line option flags, imported flag is prior to exported one, and the export one is 2964 /// prior to the ordinary pass-through option flag. 2965 /// `isNegate` decide whether export negate option or non-negate one, default: `false` 2966 /// Params: 2967 /// flag = the flag(or name) of one of the command's option or negate option 2968 /// aliasFlags = the new flags 2969 /// Returns: `Self` for chain call 2970 Self exportAs(bool isNegate = false)(string flag, string[] aliasFlags...) { 2971 assert(aliasFlags.length); 2972 if (!this._passThroughOptionValue) 2973 this.passThrough(); 2974 static if (isNegate) { 2975 auto flg = imExAsImpl!(NegateOption, true)(flag, aliasFlags); 2976 enum string word = "negate "; 2977 } 2978 else { 2979 auto flg = imExAsImpl!(Option, true)(flag, aliasFlags); 2980 enum string word = ""; 2981 } 2982 if (!flg) 2983 this.error(format("cannot find the %soption `%s` in `Command.exportAs` in command `%s`", 2984 word, flag, this._name)); 2985 return this; 2986 } 2987 2988 /// export the neagte option flag, see `Command.exportAs` 2989 alias exportNAs = exportAs!true; 2990 2991 private bool imExAsImpl(T, bool isExport = false)(string flag, string[] aliasFlags...) 2992 if (is(T == Option) || is(T == NegateOption)) { 2993 static if (is(T == Option)) { 2994 alias find_opt(alias cmd) = cmd._findOption; 2995 static if (isExport) 2996 alias _map = this._export_map; 2997 else 2998 alias _map = this._import_map; 2999 } 3000 else { 3001 alias find_opt(alias cmd) = cmd._findNegateOption; 3002 static if (isExport) 3003 alias _map = this._export_n_map; 3004 else 3005 alias _map = this._import_n_map; 3006 } 3007 if (T opt = find_opt!(this)(flag)) { 3008 auto tmp = aliasFlags.uniq; 3009 if (_map is null) { 3010 _map = tmp.map!(f => tuple(f, opt)).assocArray; 3011 return true; 3012 } 3013 tmp.filter!(f => _map.byKey.count(f) == 0) 3014 .each!((f) { _map[f] = opt; }); 3015 return true; 3016 } 3017 return false; 3018 } 3019 3020 package: 3021 void _exitErr(string msg, string code = "") const { 3022 this._outputConfiguration.writeErr("ERROR:\t" ~ msg ~ " " ~ code ~ "\n"); 3023 if (this._showHelpAfterError) { 3024 this._outputConfiguration.writeErr("\n"); 3025 this.outputHelp(true); 3026 } 3027 debug this.error(); 3028 this._exit(1); 3029 } 3030 3031 void _exitSuccessfully() const { 3032 _exit(0); 3033 } 3034 3035 void _exit(ubyte exitCode) const { 3036 import core.stdc.stdlib : exit; 3037 3038 exit(exitCode); 3039 } 3040 3041 public: 3042 /// invoke error when parsing command line 3043 /// Params: 3044 /// msg = the error message 3045 /// code = the code to tag the error 3046 void parsingError(string msg = "", string code = "command.error") const { 3047 this._exitErr(msg, code); 3048 } 3049 3050 /// invoke error when configuring command 3051 /// Params: 3052 /// msg = the error message 3053 /// code = the code to tag the error 3054 void error(string msg = "", string code = "command.error") const { 3055 throw new CMDLineError(msg, 1, code); 3056 } 3057 3058 /// get the usage of command 3059 string usage() const { 3060 if (this._usage == "") { 3061 string[] args_str = _arguments.map!(arg => arg.readableArgName).array; 3062 foreach (string key; this._argToOptNames) { 3063 auto opt = _findOption(key); 3064 args_str ~= "[(" ~ opt.name ~ ")]"; 3065 } 3066 string[] seed = [ 3067 ]; 3068 return "" ~ ( 3069 seed ~ 3070 (_options.length || _addImplicitHelpOption ? "[options]" : [ 3071 ]) ~ 3072 (_commands.length ? "[command]" : [ 3073 ]) ~ 3074 (_arguments.length || this._argToOptNames.length ? args_str : [ 3075 ]) 3076 ).join(" "); 3077 } 3078 return this._usage; 3079 } 3080 3081 /// set the usage of command 3082 /// Params: 3083 /// str = the usage string. if `str` is `""`, then will automatically generate the usage for command 3084 /// Returns: `Self` for chain call 3085 Self usage(string str) { 3086 Command command = this; 3087 if (this._commands.length != 0 && this 3088 ._commands[$ - 1] 3089 ._execHandler) { 3090 command = this._commands[$ - 1]; 3091 } 3092 if (str == "") { 3093 string[] args_str = _arguments.map!(arg => arg.readableArgName).array; 3094 foreach (string key; this._argToOptNames) { 3095 auto opt = _findOption(key); 3096 args_str ~= "[(" ~ opt.name ~ ")]"; 3097 } 3098 string[] seed = [ 3099 ]; 3100 command._usage = "" ~ ( 3101 seed ~ 3102 (_options.length || _addImplicitHelpOption ? "[options]" : [ 3103 ]) ~ 3104 (_commands.length ? "[command]" : [ 3105 ]) ~ 3106 (_arguments.length || this._argToOptNames.length ? args_str : [ 3107 ]) 3108 ).join(" "); 3109 } 3110 else 3111 command._usage = str; 3112 return this; 3113 } 3114 3115 /// get sub command by name 3116 alias findCommand = _findCommand; 3117 /// get option by name, short flag and long flag 3118 alias findOption = _findOption; 3119 /// get negate option by name, short flag and long flag 3120 alias findNOption = _findNegateOption; 3121 /// get argument by name 3122 alias findArgument = _findArgument; 3123 } 3124 3125 unittest { 3126 auto cmd = new Command("cmdline"); 3127 assert(cmd._allowExcessArguments); 3128 cmd.description("this is test"); 3129 assert("description: this is test" == cmd.description); 3130 cmd.description("this is test", [ 3131 "first": "1st", 3132 "second": "2nd" 3133 ]); 3134 assert( 3135 cmd._argsDescription == [ 3136 "first": "1st", 3137 "second": "2nd" 3138 ]); 3139 cmd.setVersion("0.0.1"); 3140 // cmd.emit("command:version"); 3141 // cmd.emit("option:version"); 3142 } 3143 3144 /// create a command by name 3145 Command createCommand(string name) { 3146 return new Command(name); 3147 } 3148 3149 /// create a command by the flag that contains its name and arguments with description 3150 /// see `Command.command(Args...)(string nameAndArgs, string desc = "")` 3151 Command createCommand(Args...)(string nameAndArgs, string desc = "") { 3152 auto caputures = matchFirst(nameAndArgs, PTN_CMDNAMEANDARGS); 3153 string name = caputures[1], _args = caputures[2]; 3154 assert(name != ""); 3155 auto cmd = createCommand(name); 3156 cmd.description(desc); 3157 if (_args != "") 3158 cmd.arguments!Args(_args); 3159 return cmd; 3160 } 3161 3162 /// the output type used in command 3163 class OutputConfiguration { 3164 void function(string str) writeOut = ( 3165 string str) => stdout.write( 3166 str); 3167 void function( 3168 string str) writeErr = ( 3169 string str) => stderr.write( 3170 str); 3171 int function() getOutHelpWidth = &_getOutHelpWidth; 3172 int function() getErrHelpWidth = &_getOutHelpWidth; 3173 void outputError(alias fn)(string str) { 3174 fn(str); 3175 } 3176 } 3177 3178 version (Windows) { 3179 package int _getOutHelpWidth() { 3180 HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 3181 CONSOLE_SCREEN_BUFFER_INFO csbi; 3182 GetConsoleScreenBufferInfo(hConsole, &csbi); 3183 return csbi.srWindow.Right - csbi.srWindow.Left + 1; 3184 } 3185 } 3186 else version (Posix) { 3187 package int _getOutHelpWidth() { 3188 Tuple!(int, string) result = executeShell("stty size"); 3189 string tmp = result[1].strip; 3190 return tmp.split(" ")[1].to!int; 3191 } 3192 } 3193 3194 /// the wrap of option value map 3195 struct OptsWrap { 3196 private OptionVariant[string] innerValue; 3197 3198 /// enable getting option value wrapped by `ArgWrap` by name using call form 3199 ArgWrap opCall(string member) const { 3200 auto ptr = member in innerValue; 3201 if (ptr) { 3202 return ArgWrap(*ptr); 3203 } 3204 else 3205 return ArgWrap(null); 3206 } 3207 3208 package this(inout OptionVariant[string] v) inout { 3209 innerValue = v; 3210 } 3211 } 3212 3213 /// the wrap of option and argument value 3214 struct ArgWrap { 3215 private ArgNullable innerValue; 3216 3217 package this(in ArgVariant value) { 3218 this.innerValue = value; 3219 } 3220 3221 package this(typeof(null) n) { 3222 this.innerValue = n; 3223 } 3224 3225 /// test wheter the inner value is valid 3226 @property 3227 bool isValid() inout { 3228 return !innerValue.isNull; 3229 } 3230 3231 /// enable implicitly convert to `bool` type representing `this.isValid` 3232 alias isValid this; 3233 3234 /// get the innner type, remember use it after test whether it is valid 3235 T get(T)() const if (isArgValueType!T) { 3236 bool is_type = testType!T(this.innerValue); 3237 if (!is_type) { 3238 throw new CMDLineError("the inner type is not " ~ T.stringof); 3239 } 3240 return cast(T) this.innerValue.get!T; 3241 } 3242 3243 /// test whether the type is the innner vlalue type 3244 bool verifyType(T)() const { 3245 return testType!T(this.innerValue); 3246 } 3247 3248 /// enable the assign oparator with innner type that allow by `ArgWrap` 3249 auto opAssign(T)(T value) if (isArgValueType!T || is(T == typeof(null))) { 3250 this.innerValue = value; 3251 return this; 3252 } 3253 3254 /// enbale the explicity cast that can get the inner value 3255 T opCast(T)() const if (isArgValueType!T) { 3256 return this.get!T; 3257 } 3258 } 3259 3260 unittest { 3261 writeln(_getOutHelpWidth()); 3262 3263 OutputConfiguration outputConfig = new OutputConfiguration; 3264 assert(outputConfig.getOutHelpWidth() == _getOutHelpWidth()); 3265 }