1 /** 2 * Argument parsing functionality. 3 * 4 * See_Also: 5 * parse, parseArgs 6 */ 7 module commandr.parser; 8 9 import commandr.program; 10 import commandr.option; 11 import commandr.args; 12 import commandr.help; 13 import commandr.utils; 14 15 import std.algorithm : canFind, count, each; 16 import std.stdio : writeln, writefln, stderr; 17 import std..string : startsWith, indexOf, format; 18 import std.range : empty; 19 import std.typecons : Tuple; 20 private import core.stdc.stdlib; 21 22 23 /** 24 * Parses program arguments. 25 * 26 * Returns instance of `ProgramArgs`, which allows working on parsed data. 27 * 28 * On top of parsing arguments, this function catches `InvalidArgumentsException`, 29 * handles `--version` and `--help` flags as well as `help` subcommand. 30 * Exception is handled by printing out the error message along with program usage information 31 * and exiting. 32 * 33 * Version and help is handled by prining out information and exiting. 34 * 35 * If you want to only parse argument without additional handling, see `parseArgs`. 36 * 37 * Similarly to `parseArgs`, `args` array is taken by reference, after call it points to first 38 * not-parsed argument (after `--`). 39 * 40 * See_Also: 41 * `parseArgs` 42 */ 43 public ProgramArgs parse(Program program, ref string[] args, HelpOutput helpConfig = HelpOutput.init) { 44 try { 45 auto res = parseArgs(program, args, helpConfig); 46 47 if (res.flag("version")) { 48 writeln(program.version_); 49 exit(0); 50 assert(0); 51 } 52 53 return res; 54 } catch(InvalidArgumentsException e) { 55 stderr.writeln("Error: ", e.msg); 56 program.printUsage(helpConfig); 57 exit(0); 58 assert(0); 59 } 60 } 61 62 63 /** 64 * Parses args. 65 * 66 * Returns instance of `ProgramArgs`, which allows working on parsed data. 67 * 68 * Program model by default adds version flag and help flags and subcommand which need to be 69 * handled by the caller. If you want to have the default behavior, use `parse` which handles 70 * above flags. 71 * 72 * `args` array is taken by reference, after call it points to first not-parsed argument (after `--`). 73 * 74 * Throws: 75 * InvalidArgumentException 76 * 77 * See_Also: 78 * `parse` 79 */ 80 public ProgramArgs parseArgs(Program program, ref string[] args, HelpOutput helpConfig = HelpOutput.init) { 81 args = args[1..$]; 82 return program.parseArgs(args, new ProgramArgs(), helpConfig); 83 } 84 85 private ProgramArgs parseArgs( 86 Command program, 87 ref string[] args, 88 ProgramArgs init, 89 HelpOutput helpConfig = HelpOutput.init 90 ) { 91 // TODO: Split 92 ProgramArgs result = init; 93 result.name = program.name; 94 size_t argIndex = 0; 95 96 while (args.length) { 97 string arg = args[0]; 98 args = args[1..$]; 99 immutable bool hasNext = args.length > 0; 100 101 // end of args 102 if (arg == "--") { 103 break; 104 } 105 // option/flag 106 else if (arg.startsWith("-")) { 107 immutable bool isLong = arg.startsWith("--"); 108 auto raw = parseRawOption(arg[1 + isLong..$]); 109 110 // try matching flag, then fallback to option 111 auto flag = isLong ? program.getFlagByFull(raw.name) : program.getFlagByShort(raw.name); 112 int flagValue = 1; 113 114 // repeating (-vvvv) 115 if (!isLong && flag.isNull && raw.name.length > 1) { 116 char letter = raw.name[0]; 117 // all same character 118 if (!raw.name.canFind!(l => l != letter)) { 119 flagValue = cast(int)raw.name.length; 120 raw.name = raw.name[0..1]; 121 flag = program.getFlagByShort(raw.name); 122 } 123 } 124 125 // flag exists, has value 126 if (!flag.isNull && raw.value != null) { 127 throw new InvalidArgumentsException("-%s is a flag, and cannot accept value".format(raw.name)); 128 } 129 // just exists 130 else if (!flag.isNull) { 131 auto flagName = flag.get().name; 132 result._flags.setOrIncrease(flagName, flagValue); 133 134 if (result._flags[flagName] > 1 && !flag.get().isRepeating) { 135 throw new InvalidArgumentsException("flag -%s cannot be repeated".format(raw.name)); 136 } 137 138 continue; 139 } 140 141 // trying to match option 142 auto option = isLong ? program.getOptionByFull(raw.name) : program.getOptionByShort(raw.name); 143 if (option.isNull) { 144 string suggestion = (isLong ? program.fullNames : program.abbrevations).matchingCandidate(raw.name); 145 146 if (suggestion) { 147 throw new InvalidArgumentsException( 148 "unknown flag/option %s, did you mean %s%s?".format(arg, isLong ? "--" : "-", suggestion) 149 ); 150 } 151 else { 152 throw new InvalidArgumentsException("unknown flag/option %s".format(arg)); 153 } 154 } 155 156 // no value 157 if (raw.value is null) { 158 if (!hasNext) { 159 throw new InvalidArgumentsException( 160 "option %s%s is missing value".format(isLong ? "--" : "-", raw.name) 161 ); 162 } 163 auto next = args[0]; 164 args = args[1..$]; 165 if (next.startsWith("-")) { 166 throw new InvalidArgumentsException( 167 "option %s%s is missing value (if value starts with \'-\' character, prefix it with '\\')" 168 .format(isLong ? "--" : "-", raw.name) 169 ); 170 } 171 raw.value = next; 172 } 173 result._options.setOrAppend(option.get().name, raw.value); 174 } 175 // argument 176 else if (argIndex < program.arguments.length) { 177 Argument argument = program.arguments[argIndex]; 178 if (!argument.isRepeating) { 179 argIndex += 1; 180 } 181 result._args.setOrAppend(argument.name, arg); 182 } 183 // command 184 else { 185 if (program.commands.length == 0) { 186 throw new InvalidArgumentsException("unknown (excessive) parameter %s".format(arg)); 187 } 188 else if ((arg in program.commands) is null) { 189 string suggestion = program.commands.keys.matchingCandidate(arg); 190 throw new InvalidArgumentsException("unknown command %s, did you mean %s?".format(arg, suggestion)); 191 } 192 else { 193 result._command = program.commands[arg].parseArgs(args, result.copy()); 194 result._command._parent = result; 195 break; 196 } 197 } 198 } 199 200 if (result.flag("help")) { 201 program.printHelp(helpConfig); 202 exit(0); 203 } 204 205 // fill defaults (before required) 206 foreach(option; program.options) { 207 if (result.option(option.name) is null && option.defaultValue) { 208 result._options[option.name] = option.defaultValue; 209 } 210 } 211 212 foreach(arg; program.arguments) { 213 if (result.arg(arg.name) is null && arg.defaultValue) { 214 result._args[arg.name] = arg.defaultValue; 215 } 216 } 217 218 // post-process options: check required opts, illegal repetitions and validate 219 foreach (option; program.options) { 220 if (option.isRequired && result.option(option.name) is null) { 221 throw new InvalidArgumentsException("missing required option %s".format(option.name)); 222 } 223 224 if (!option.isRepeating && result.options(option.name, []).length > 1) { 225 throw new InvalidArgumentsException("expected only one value for option %s".format(option.name)); 226 } 227 228 if (option.validators.empty) { 229 continue; 230 } 231 232 auto values = result.options(option.name); 233 foreach (validator; option.validators) { 234 validator.validate(option, values); 235 } 236 } 237 238 // check required args & illegal repetitions 239 foreach (arg; program.arguments) { 240 if (arg.isRequired && result.arg(arg.name) is null) { 241 throw new InvalidArgumentsException("missing required argument %s".format(arg.name)); 242 } 243 244 if (arg.validators.empty) { 245 continue; 246 } 247 248 auto values = result.args(arg.name); 249 foreach (validator; arg.validators) { 250 validator.validate(arg, values); 251 } 252 } 253 254 if (result.command is null && program.commands.length > 0) { 255 if (program.defaultCommand !is null) { 256 result._command = program.commands[program.defaultCommand].parseArgs(args, result.copy()); 257 result._command._parent = result; 258 } 259 else { 260 throw new InvalidArgumentsException("missing required subcommand"); 261 } 262 } 263 264 return result; 265 } 266 267 package ProgramArgs parseArgsNoRef(Program p, string[] args) { 268 return p.parseArgs(args); 269 } 270 271 // internal type for holding name-value pair 272 private alias RawOption = Tuple!(string, "name", string, "value"); 273 274 275 /* 276 * Splits --option=value into a pair of strings on match, otherwise 277 * returns a tuple with option name and null. 278 */ 279 private RawOption parseRawOption(string argument) { 280 RawOption result; 281 282 auto index = argument.indexOf("="); 283 if (index > 0) { 284 result.name = argument[0..index]; 285 result.value = argument[index+1..$]; 286 } 287 else { 288 result.name = argument; 289 result.value = null; 290 } 291 292 return result; 293 } 294 295 private void setOrAppend(T)(ref T[][string] array, string name, T value) { 296 if (name in array) { 297 array[name] ~= value; 298 } else { 299 array[name] = [value]; 300 } 301 } 302 303 private void setOrIncrease(ref int[string] array, string name, int value) { 304 if (name in array) { 305 array[name] += value; 306 } else { 307 array[name] = value; 308 } 309 } 310 311 unittest { 312 import std.exception : assertThrown, assertNotThrown; 313 314 assertNotThrown!InvalidArgumentsException( 315 new Program("test").parseArgsNoRef(["test"]) 316 ); 317 } 318 319 // flags 320 unittest { 321 import std.exception : assertThrown, assertNotThrown; 322 323 ProgramArgs a; 324 325 a = new Program("test") 326 .add(new Flag("t", "test", "")) 327 .parseArgsNoRef(["test"]); 328 assert(!a.flag("test")); 329 assert(a.option("test") is null); 330 assert(a.occurencesOf("test") == 0); 331 332 a = new Program("test") 333 .add(new Flag("t", "test", "")) 334 .parseArgsNoRef(["test", "-t"]); 335 assert(a.flag("test")); 336 assert(a.option("test") is null); 337 assert(a.occurencesOf("test") == 1); 338 339 a = new Program("test") 340 .add(new Flag("t", "test", "")) 341 .parseArgsNoRef(["test", "--test"]); 342 assert(a.flag("test")); 343 assert(a.occurencesOf("test") == 1); 344 345 assertThrown!InvalidArgumentsException( 346 new Program("test") 347 .add(new Flag("t", "test", "")) // no repeating 348 .parseArgsNoRef(["test", "--test", "-t"]) 349 ); 350 351 assertThrown!InvalidArgumentsException( 352 new Program("test") 353 .add(new Flag("t", "test", "")) // no repeating 354 .parseArgsNoRef(["test", "-tt"]) 355 ); 356 357 assertThrown!InvalidArgumentsException( 358 new Program("test") 359 .add(new Flag("t", "test", "")) // no repeating 360 .parseArgsNoRef(["test", "--tt"]) 361 ); 362 } 363 364 // options 365 unittest { 366 import std.exception : assertThrown, assertNotThrown; 367 import std.range : empty; 368 369 ProgramArgs a; 370 371 a = new Program("test") 372 .add(new Option("t", "test", "")) 373 .parseArgsNoRef(["test"]); 374 assert(a.option("test") is null); 375 assert(a.occurencesOf("test") == 0); 376 377 a = new Program("test") 378 .add(new Option("t", "test", "")) 379 .parseArgsNoRef(["test", "-t", "5"]); 380 assert(a.option("test") == "5"); 381 assert(a.occurencesOf("test") == 0); 382 383 a = new Program("test") 384 .add(new Option("t", "test", "")) 385 .parseArgsNoRef(["test", "-t=5"]); 386 assert(a.option("test") == "5"); 387 assert(a.occurencesOf("test") == 0); 388 389 a = new Program("test") 390 .add(new Option("t", "test", "")) 391 .parseArgsNoRef(["test", "--test", "bar"]); 392 assert(a.option("test") == "bar"); 393 assert(a.occurencesOf("test") == 0); 394 395 a = new Program("test") 396 .add(new Option("t", "test", "")) 397 .parseArgsNoRef(["test", "--test=bar"]); 398 assert(a.option("test") == "bar"); 399 assert(a.occurencesOf("test") == 0); 400 401 assertThrown!InvalidArgumentsException( 402 new Program("test") 403 .add(new Option("t", "test", "")) 404 .parseArgsNoRef(["test", "--test=a", "-t", "k"]) 405 ); 406 407 assertThrown!InvalidArgumentsException( 408 new Program("test") 409 .add(new Option("t", "test", "")) // no repeating 410 .parseArgsNoRef(["test", "--test", "-t"]) 411 ); 412 413 assertThrown!InvalidArgumentsException( 414 new Program("test") 415 .add(new Option("t", "test", "")) // no value 416 .parseArgsNoRef(["test", "--test"]) 417 ); 418 } 419 420 // arguments 421 unittest { 422 import std.exception : assertThrown, assertNotThrown; 423 import std.range : empty; 424 425 ProgramArgs a; 426 427 a = new Program("test") 428 .add(new Argument("test", "").optional) 429 .parseArgsNoRef(["test"]); 430 assert(a.occurencesOf("test") == 0); 431 432 a = new Program("test") 433 .add(new Argument("test", "")) 434 .parseArgsNoRef(["test", "t"]); 435 assert(a.occurencesOf("test") == 0); 436 assert(a.arg("test") == "t"); 437 438 assertThrown!InvalidArgumentsException( 439 new Program("test") 440 .parseArgsNoRef(["test", "test", "t"]) 441 ); 442 443 assertThrown!InvalidArgumentsException( 444 new Program("test") 445 .add(new Argument("test", "")) // no value 446 .parseArgsNoRef(["test", "test", "test"]) 447 ); 448 } 449 450 // required 451 unittest { 452 import std.exception : assertThrown, assertNotThrown; 453 454 assertThrown!InvalidArgumentsException( 455 new Program("test") 456 .add(new Option("t", "test", "").required) 457 .parseArgsNoRef(["test"]) 458 ); 459 460 assertThrown!InvalidArgumentsException( 461 new Program("test") 462 .add(new Option("t", "test", "")) 463 .add(new Argument("path", "").required) 464 .parseArgsNoRef(["test", "--test", "bar"]) 465 ); 466 467 assertThrown!InvalidArgumentsException( 468 new Program("test") 469 .add(new Argument("test", "").required) // no value 470 .parseArgsNoRef(["test"]) 471 ); 472 } 473 474 // repating 475 unittest { 476 ProgramArgs a; 477 478 a = new Program("test") 479 .add(new Flag("t", "test", "").repeating) 480 .parseArgsNoRef(["test", "--test", "-t"]); 481 assert(a.flag("test")); 482 assert(a.occurencesOf("test") == 2); 483 484 a = new Program("test") 485 .add(new Option("t", "test", "").repeating) 486 .parseArgsNoRef(["test", "--test=a", "-t", "k"]); 487 assert(a.option("test") == "k"); 488 assert(a.optionAll("test") == ["a", "k"]); 489 assert(a.occurencesOf("test") == 0); 490 } 491 492 // default value 493 unittest { 494 ProgramArgs a; 495 496 a = new Program("test") 497 .add(new Option("t", "test", "") 498 .defaultValue("reee")) 499 .parseArgsNoRef(["test"]); 500 assert(a.option("test") == "reee"); 501 502 a = new Program("test") 503 .add(new Option("t", "test", "") 504 .defaultValue("reee")) 505 .parseArgsNoRef(["test", "--test", "aaa"]); 506 assert(a.option("test") == "aaa"); 507 508 a = new Program("test") 509 .add(new Argument("test", "") 510 .optional 511 .defaultValue("reee")) 512 .parseArgsNoRef(["test"]); 513 assert(a.arg("test") == "reee"); 514 515 a = new Program("test") 516 .add(new Argument("test", "") 517 .optional 518 .defaultValue("reee")) 519 .parseArgsNoRef(["test", "bar"]); 520 assert(a.args("test") == ["bar"]); 521 } 522 523 // rest 524 unittest { 525 ProgramArgs a; 526 auto args = ["test", "--", "bar"]; 527 a = new Program("test") 528 .add(new Argument("test", "") 529 .optional 530 .defaultValue("reee")) 531 .parseArgs(args); 532 assert(a.args("test") == ["reee"]); 533 assert(args == ["bar"]); 534 } 535 536 // subcommands 537 unittest { 538 import std.exception : assertThrown; 539 540 assertThrown!InvalidArgumentsException( 541 new Program("test") 542 .add(new Argument("test", "")) 543 .add(new Command("a")) 544 .add(new Command("b") 545 .add(new Command("c"))) 546 .parseArgsNoRef(["test", "cccc"]) 547 ); 548 549 assertThrown!InvalidArgumentsException( 550 new Program("test") 551 .add(new Argument("test", "")) 552 .add(new Command("a")) 553 .add(new Command("b") 554 .add(new Command("c"))) 555 .parseArgsNoRef(["test", "cccc", "a", "c"]) 556 ); 557 558 ProgramArgs a; 559 a = new Program("test") 560 .add(new Argument("test", "")) 561 .add(new Command("a")) 562 .add(new Command("b") 563 .add(new Command("c"))) 564 .parseArgsNoRef(["test", "cccc", "b", "c"]); 565 assert(a.args("test") == ["cccc"]); 566 assert(a.command !is null); 567 assert(a.command.name == "b"); 568 assert(a.command.command !is null); 569 assert(a.command.command.name == "c"); 570 571 572 auto args = ["test", "cccc", "a", "--", "c"]; 573 a = new Program("test") 574 .add(new Argument("test", "")) 575 .add(new Command("a")) 576 .add(new Command("b") 577 .add(new Command("c"))) 578 .parseArgs(args); 579 assert(a.args("test") == ["cccc"]); 580 assert(a.command !is null); 581 assert(a.command.name == "a"); 582 assert(args == ["c"]); 583 584 assertThrown!InvalidArgumentsException( 585 new Program("test") 586 .add(new Argument("test", "")) 587 .add(new Command("a")) 588 .add(new Command("b") 589 .add(new Command("c"))) 590 .parseArgsNoRef(["test", "cccc", "b", "--", "c"]) 591 ); 592 } 593 594 // default subcommand 595 unittest { 596 ProgramArgs a; 597 598 a = new Program("test") 599 .add(new Command("a")) 600 .add(new Command("b") 601 .add(new Command("c"))) 602 .defaultCommand("a") 603 .parseArgsNoRef(["test"]); 604 605 assert(a.command !is null); 606 assert(a.command.name == "a"); 607 608 609 a = new Program("test") 610 .add(new Command("a")) 611 .add(new Command("b") 612 .add(new Command("c")) 613 .defaultCommand("c")) 614 .defaultCommand("b") 615 .parseArgsNoRef(["test"]); 616 617 assert(a.command !is null); 618 assert(a.command.name == "b"); 619 assert(a.command.command !is null); 620 assert(a.command.command.name == "c"); 621 }