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