jsoptparse.cpp (18791B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "shell/jsoptparse.h" 8 9 #include <algorithm> 10 #include <stdarg.h> 11 #include <string_view> 12 13 #include "util/Unicode.h" 14 15 using namespace js; 16 using namespace js::cli; 17 using namespace js::cli::detail; 18 19 #define OPTION_CONVERT_IMPL(__cls) \ 20 bool Option::is##__cls##Option() const { return kind == OptionKind##__cls; } \ 21 __cls##Option* Option::as##__cls##Option() { \ 22 MOZ_ASSERT(is##__cls##Option()); \ 23 return static_cast<__cls##Option*>(this); \ 24 } \ 25 const __cls##Option* Option::as##__cls##Option() const { \ 26 return const_cast<Option*>(this)->as##__cls##Option(); \ 27 } 28 29 ValuedOption* Option::asValued() { 30 MOZ_ASSERT(isValued()); 31 return static_cast<ValuedOption*>(this); 32 } 33 34 const ValuedOption* Option::asValued() const { 35 return const_cast<Option*>(this)->asValued(); 36 } 37 38 OPTION_CONVERT_IMPL(Bool) 39 OPTION_CONVERT_IMPL(String) 40 OPTION_CONVERT_IMPL(Int) 41 OPTION_CONVERT_IMPL(MultiString) 42 43 void OptionParser::setArgTerminatesOptions(const char* name, bool enabled) { 44 findArgument(name)->setTerminatesOptions(enabled); 45 } 46 47 void OptionParser::setIgnoresUnknownOptions(const char* name, bool enabled) { 48 auto* opt = findOption(name); 49 MOZ_ASSERT(opt); 50 opt->setIgnoresUnknownOptions(enabled); 51 } 52 53 void OptionParser::setArgCapturesRest(const char* name) { 54 MOZ_ASSERT(restArgument == -1, 55 "only one argument may be set to capture the rest"); 56 restArgument = findArgumentIndex(name); 57 MOZ_ASSERT(restArgument != -1, 58 "unknown argument name passed to setArgCapturesRest"); 59 } 60 61 OptionParser::Result OptionParser::error(const char* fmt, ...) { 62 va_list args; 63 va_start(args, fmt); 64 fprintf(stderr, "Error: "); 65 vfprintf(stderr, fmt, args); 66 va_end(args); 67 fputs("\n\n", stderr); 68 return ParseError; 69 } 70 71 /* Quick and dirty paragraph printer. */ 72 static void PrintParagraph(const char* text, unsigned startColno, 73 const unsigned limitColno, bool padFirstLine) { 74 unsigned colno = startColno; 75 unsigned indent = 0; 76 const char* it = text; 77 78 if (padFirstLine) { 79 printf("%*s", int(startColno), ""); 80 } 81 82 /* Skip any leading spaces. */ 83 while (*it != '\0' && unicode::IsSpace(*it)) { 84 ++it; 85 } 86 87 while (*it != '\0') { 88 MOZ_ASSERT(!unicode::IsSpace(*it) || *it == '\n'); 89 90 /* Delimit the current token. */ 91 const char* limit = it; 92 while (!unicode::IsSpace(*limit) && *limit != '\0') { 93 ++limit; 94 } 95 96 /* 97 * If the current token is longer than the available number of columns, 98 * then make a line break before printing the token. 99 */ 100 size_t tokLen = limit - it; 101 if (tokLen + colno >= limitColno) { 102 printf("\n%*s%.*s", int(startColno + indent), "", int(tokLen), it); 103 colno = startColno + tokLen; 104 } else { 105 printf("%.*s", int(tokLen), it); 106 colno += tokLen; 107 } 108 109 switch (*limit) { 110 case '\0': 111 return; 112 case ' ': 113 putchar(' '); 114 colno += 1; 115 it = limit; 116 while (*it == ' ') { 117 ++it; 118 } 119 break; 120 case '\n': 121 /* |text| wants to force a newline here. */ 122 printf("\n%*s", int(startColno), ""); 123 colno = startColno; 124 it = limit + 1; 125 /* Could also have line-leading spaces. */ 126 indent = 0; 127 while (*it == ' ') { 128 putchar(' '); 129 ++colno; 130 ++indent; 131 ++it; 132 } 133 break; 134 default: 135 MOZ_CRASH("unhandled token splitting character in text"); 136 } 137 } 138 } 139 140 static const char* OptionFlagsToFormatInfo(char shortflag, bool isValued, 141 size_t* length) { 142 static const char* const fmt[4] = {" -%c --%s ", " --%s ", " -%c --%s=%s ", 143 " --%s=%s "}; 144 145 /* How mny chars w/o longflag? */ 146 size_t lengths[4] = {strlen(fmt[0]) - 3, strlen(fmt[1]) - 3, 147 strlen(fmt[2]) - 5, strlen(fmt[3]) - 5}; 148 int index = isValued ? 2 : 0; 149 if (!shortflag) { 150 index++; 151 } 152 153 *length = lengths[index]; 154 return fmt[index]; 155 } 156 157 OptionParser::Result OptionParser::printHelp(const char* progname) { 158 constexpr std::string_view prognameMeta = "{progname}"; 159 160 const char* prefixEnd = strstr(usage, prognameMeta.data()); 161 if (prefixEnd) { 162 printf("%.*s%s%s\n", int(prefixEnd - usage), usage, progname, 163 prefixEnd + prognameMeta.length()); 164 } else { 165 puts(usage); 166 } 167 168 if (descr) { 169 putchar('\n'); 170 PrintParagraph(descr, 2, descrWidth, true); 171 putchar('\n'); 172 } 173 174 if (version) { 175 printf("\nVersion: %s\n\n", version); 176 } 177 178 if (!arguments.empty()) { 179 printf("Arguments:\n"); 180 181 static const char fmt[] = " %s "; 182 size_t fmtChars = sizeof(fmt) - 2; 183 size_t lhsLen = 0; 184 for (Option* arg : arguments) { 185 lhsLen = std::max(lhsLen, strlen(arg->longflag) + fmtChars); 186 } 187 188 for (Option* arg : arguments) { 189 size_t chars = printf(fmt, arg->longflag); 190 for (; chars < lhsLen; ++chars) { 191 putchar(' '); 192 } 193 PrintParagraph(arg->help, lhsLen, helpWidth, false); 194 putchar('\n'); 195 } 196 putchar('\n'); 197 } 198 199 if (!options.empty()) { 200 printf("Options:\n"); 201 202 /* Calculate sizes for column alignment. */ 203 size_t lhsLen = 0; 204 for (Option* opt : options) { 205 size_t longflagLen = strlen(opt->longflag); 206 207 size_t fmtLen; 208 OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen); 209 210 size_t len = fmtLen + longflagLen; 211 if (opt->isValued()) { 212 len += strlen(opt->asValued()->metavar); 213 } 214 lhsLen = std::max(lhsLen, len); 215 } 216 217 /* Print option help text. */ 218 for (Option* opt : options) { 219 size_t fmtLen; 220 const char* fmt = 221 OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen); 222 size_t chars; 223 if (opt->isValued()) { 224 if (opt->shortflag) { 225 chars = printf(fmt, opt->shortflag, opt->longflag, 226 opt->asValued()->metavar); 227 } else { 228 chars = printf(fmt, opt->longflag, opt->asValued()->metavar); 229 } 230 } else { 231 if (opt->shortflag) { 232 chars = printf(fmt, opt->shortflag, opt->longflag); 233 } else { 234 chars = printf(fmt, opt->longflag); 235 } 236 } 237 for (; chars < lhsLen; ++chars) { 238 putchar(' '); 239 } 240 PrintParagraph(opt->help, lhsLen, helpWidth, false); 241 putchar('\n'); 242 } 243 } 244 245 return EarlyExit; 246 } 247 248 OptionParser::Result OptionParser::printVersion() { 249 MOZ_ASSERT(version); 250 printf("%s\n", version); 251 return EarlyExit; 252 } 253 254 OptionParser::Result OptionParser::extractValue(size_t argc, char** argv, 255 size_t* i, char** value) { 256 MOZ_ASSERT(*i < argc); 257 char* eq = strchr(argv[*i], '='); 258 if (eq) { 259 *value = eq + 1; 260 if (*value[0] == '\0') { 261 return error("A value is required for option %.*s", (int)(eq - argv[*i]), 262 argv[*i]); 263 } 264 return Okay; 265 } 266 267 if (argc == *i + 1) { 268 return error("Expected a value for option %s", argv[*i]); 269 } 270 271 *i += 1; 272 *value = argv[*i]; 273 return Okay; 274 } 275 276 OptionParser::Result OptionParser::handleOption(Option* opt, size_t argc, 277 char** argv, size_t* i, 278 bool* optionsAllowed) { 279 if (opt->getTerminatesOptions()) { 280 *optionsAllowed = false; 281 } 282 283 switch (opt->kind) { 284 case OptionKindBool: { 285 if (opt == &helpOption) { 286 return printHelp(argv[0]); 287 } 288 if (opt == &versionOption) { 289 return printVersion(); 290 } 291 opt->asBoolOption()->value = true; 292 return Okay; 293 } 294 /* 295 * Valued options are allowed to specify their values either via 296 * successive arguments or a single --longflag=value argument. 297 */ 298 case OptionKindString: { 299 char* value = nullptr; 300 if (Result r = extractValue(argc, argv, i, &value)) { 301 return r; 302 } 303 opt->asStringOption()->value = value; 304 return Okay; 305 } 306 case OptionKindInt: { 307 char* value = nullptr; 308 if (Result r = extractValue(argc, argv, i, &value)) { 309 return r; 310 } 311 opt->asIntOption()->value = atoi(value); 312 return Okay; 313 } 314 case OptionKindMultiString: { 315 char* value = nullptr; 316 if (Result r = extractValue(argc, argv, i, &value)) { 317 return r; 318 } 319 StringArg arg(value, *i); 320 return opt->asMultiStringOption()->strings.append(arg) ? Okay : Fail; 321 } 322 default: 323 MOZ_CRASH("unhandled option kind"); 324 } 325 } 326 327 OptionParser::Result OptionParser::handleArg(size_t argc, char** argv, 328 size_t* i, bool* optionsAllowed) { 329 if (nextArgument >= arguments.length()) { 330 return error("Too many arguments provided"); 331 } 332 333 Option* arg = arguments[nextArgument]; 334 335 if (arg->getTerminatesOptions()) { 336 *optionsAllowed = false; 337 } 338 339 switch (arg->kind) { 340 case OptionKindString: 341 arg->asStringOption()->value = argv[*i]; 342 nextArgument += 1; 343 return Okay; 344 case OptionKindMultiString: { 345 // Don't advance the next argument -- there can only be one (final) 346 // variadic argument. 347 StringArg value(argv[*i], *i); 348 return arg->asMultiStringOption()->strings.append(value) ? Okay : Fail; 349 } 350 default: 351 MOZ_CRASH("unhandled argument kind"); 352 } 353 } 354 355 OptionParser::Result OptionParser::parseArgs(int inputArgc, char** argv) { 356 MOZ_ASSERT(inputArgc >= 0); 357 size_t argc = inputArgc; 358 359 // Permit a "no more options" capability, like |--| offers in many shell 360 // interfaces. 361 bool optionsAllowed = true; 362 363 // Whether unknown options should report a warning instead of an error. This 364 // is enabled by setIgnoresUnknownOptions and used for --fuzzing-safe. 365 bool ignoreUnknownOptions = false; 366 367 for (size_t i = 1; i < argc; ++i) { 368 char* arg = argv[i]; 369 Result r; 370 /* Note: solo dash option is actually a 'stdin' argument. */ 371 if (arg[0] == '-' && arg[1] != '\0' && optionsAllowed) { 372 /* Option. */ 373 Option* opt; 374 if (arg[1] == '-') { 375 if (arg[2] == '\0') { 376 /* End of options */ 377 optionsAllowed = false; 378 nextArgument = restArgument; 379 continue; 380 } else { 381 /* Long option. */ 382 opt = findOption(arg + 2); 383 if (!opt) { 384 if (ignoreUnknownOptions) { 385 fprintf(stderr, "Warning: Ignoring unknown shell flag: %s\n", 386 arg); 387 continue; 388 } 389 return error("Invalid long option: %s", arg); 390 } 391 } 392 } else { 393 /* Short option */ 394 if (arg[2] != '\0') { 395 return error("Short option followed by junk: %s", arg); 396 } 397 opt = findOption(arg[1]); 398 if (!opt) { 399 return error("Invalid short option: %s", arg); 400 } 401 } 402 403 if (opt->getIgnoresUnknownOptions()) { 404 ignoreUnknownOptions = true; 405 } 406 407 r = handleOption(opt, argc, argv, &i, &optionsAllowed); 408 } else { 409 /* Argument. */ 410 r = handleArg(argc, argv, &i, &optionsAllowed); 411 } 412 413 if (r != Okay) { 414 return r; 415 } 416 } 417 return Okay; 418 } 419 420 void OptionParser::setHelpOption(char shortflag, const char* longflag, 421 const char* help) { 422 helpOption.setFlagInfo(shortflag, longflag, help); 423 } 424 425 bool OptionParser::getHelpOption() const { return helpOption.value; } 426 427 bool OptionParser::getBoolOption(char shortflag) const { 428 return tryFindOption(shortflag)->asBoolOption()->value; 429 } 430 431 int OptionParser::getIntOption(char shortflag) const { 432 return tryFindOption(shortflag)->asIntOption()->value; 433 } 434 435 const char* OptionParser::getStringOption(char shortflag) const { 436 return tryFindOption(shortflag)->asStringOption()->value; 437 } 438 439 MultiStringRange OptionParser::getMultiStringOption(char shortflag) const { 440 const MultiStringOption* mso = 441 tryFindOption(shortflag)->asMultiStringOption(); 442 return MultiStringRange(mso->strings.begin(), mso->strings.end()); 443 } 444 445 bool OptionParser::getBoolOption(const char* longflag) const { 446 return tryFindOption(longflag)->asBoolOption()->value; 447 } 448 449 int OptionParser::getIntOption(const char* longflag) const { 450 return tryFindOption(longflag)->asIntOption()->value; 451 } 452 453 const char* OptionParser::getStringOption(const char* longflag) const { 454 return tryFindOption(longflag)->asStringOption()->value; 455 } 456 457 MultiStringRange OptionParser::getMultiStringOption( 458 const char* longflag) const { 459 const MultiStringOption* mso = tryFindOption(longflag)->asMultiStringOption(); 460 return MultiStringRange(mso->strings.begin(), mso->strings.end()); 461 } 462 463 OptionParser::~OptionParser() { 464 for (Option* opt : options) { 465 js_delete<Option>(opt); 466 } 467 for (Option* arg : arguments) { 468 js_delete<Option>(arg); 469 } 470 } 471 472 Option* OptionParser::findOption(char shortflag) { 473 for (Option* opt : options) { 474 if (opt->shortflag == shortflag) { 475 return opt; 476 } 477 } 478 479 if (versionOption.shortflag == shortflag) { 480 return &versionOption; 481 } 482 483 return helpOption.shortflag == shortflag ? &helpOption : nullptr; 484 } 485 486 const Option* OptionParser::findOption(char shortflag) const { 487 return const_cast<OptionParser*>(this)->findOption(shortflag); 488 } 489 490 const Option* OptionParser::tryFindOption(char shortflag) const { 491 const Option* maybeOption = findOption(shortflag); 492 if (!maybeOption) { 493 fprintf(stderr, "Failed to find short option %c\n", shortflag); 494 MOZ_CRASH(); 495 } 496 return maybeOption; 497 } 498 499 Option* OptionParser::findOption(const char* longflag) { 500 for (Option* opt : options) { 501 const char* target = opt->longflag; 502 if (opt->isValued()) { 503 size_t targetLen = strlen(target); 504 /* Permit a trailing equals sign on the longflag argument. */ 505 for (size_t i = 0; i < targetLen; ++i) { 506 if (longflag[i] == '\0' || longflag[i] != target[i]) { 507 goto no_match; 508 } 509 } 510 if (longflag[targetLen] == '\0' || longflag[targetLen] == '=') { 511 return opt; 512 } 513 } else { 514 if (strcmp(target, longflag) == 0) { 515 return opt; 516 } 517 } 518 no_match:; 519 } 520 521 if (strcmp(versionOption.longflag, longflag) == 0) { 522 return &versionOption; 523 } 524 525 return strcmp(helpOption.longflag, longflag) ? nullptr : &helpOption; 526 } 527 528 const Option* OptionParser::findOption(const char* longflag) const { 529 return const_cast<OptionParser*>(this)->findOption(longflag); 530 } 531 532 const Option* OptionParser::tryFindOption(const char* longflag) const { 533 const Option* maybeOption = findOption(longflag); 534 if (!maybeOption) { 535 fprintf(stderr, "Failed to find long option %s\n", longflag); 536 MOZ_CRASH(); 537 } 538 return maybeOption; 539 } 540 541 /* Argument accessors */ 542 543 int OptionParser::findArgumentIndex(const char* name) const { 544 for (Option* const* it = arguments.begin(); it != arguments.end(); ++it) { 545 const char* target = (*it)->longflag; 546 if (strcmp(target, name) == 0) { 547 return it - arguments.begin(); 548 } 549 } 550 return -1; 551 } 552 553 Option* OptionParser::findArgument(const char* name) { 554 int index = findArgumentIndex(name); 555 return (index == -1) ? nullptr : arguments[index]; 556 } 557 558 const Option* OptionParser::findArgument(const char* name) const { 559 int index = findArgumentIndex(name); 560 return (index == -1) ? nullptr : arguments[index]; 561 } 562 563 const char* OptionParser::getStringArg(const char* name) const { 564 return findArgument(name)->asStringOption()->value; 565 } 566 567 MultiStringRange OptionParser::getMultiStringArg(const char* name) const { 568 const MultiStringOption* mso = findArgument(name)->asMultiStringOption(); 569 return MultiStringRange(mso->strings.begin(), mso->strings.end()); 570 } 571 572 /* Option builders */ 573 574 // Use vanilla malloc for allocations. See OptionAllocPolicy. 575 JS_DECLARE_NEW_METHODS(opt_new, malloc, static MOZ_ALWAYS_INLINE) 576 577 bool OptionParser::addIntOption(char shortflag, const char* longflag, 578 const char* metavar, const char* help, 579 int defaultValue) { 580 if (!options.reserve(options.length() + 1)) { 581 return false; 582 } 583 IntOption* io = 584 opt_new<IntOption>(shortflag, longflag, help, metavar, defaultValue); 585 if (!io) { 586 return false; 587 } 588 options.infallibleAppend(io); 589 return true; 590 } 591 592 bool OptionParser::addBoolOption(char shortflag, const char* longflag, 593 const char* help) { 594 if (!options.reserve(options.length() + 1)) { 595 return false; 596 } 597 BoolOption* bo = opt_new<BoolOption>(shortflag, longflag, help); 598 if (!bo) { 599 return false; 600 } 601 options.infallibleAppend(bo); 602 return true; 603 } 604 605 bool OptionParser::addStringOption(char shortflag, const char* longflag, 606 const char* metavar, const char* help) { 607 if (!options.reserve(options.length() + 1)) { 608 return false; 609 } 610 StringOption* so = opt_new<StringOption>(shortflag, longflag, help, metavar); 611 if (!so) { 612 return false; 613 } 614 options.infallibleAppend(so); 615 return true; 616 } 617 618 bool OptionParser::addMultiStringOption(char shortflag, const char* longflag, 619 const char* metavar, const char* help) { 620 if (!options.reserve(options.length() + 1)) { 621 return false; 622 } 623 MultiStringOption* mso = 624 opt_new<MultiStringOption>(shortflag, longflag, help, metavar); 625 if (!mso) { 626 return false; 627 } 628 options.infallibleAppend(mso); 629 return true; 630 } 631 632 /* Argument builders */ 633 634 bool OptionParser::addOptionalStringArg(const char* name, const char* help) { 635 if (!arguments.reserve(arguments.length() + 1)) { 636 return false; 637 } 638 StringOption* so = opt_new<StringOption>(1, name, help, (const char*)nullptr); 639 if (!so) { 640 return false; 641 } 642 arguments.infallibleAppend(so); 643 return true; 644 } 645 646 bool OptionParser::addOptionalMultiStringArg(const char* name, 647 const char* help) { 648 MOZ_ASSERT_IF(!arguments.empty(), !arguments.back()->isVariadic()); 649 if (!arguments.reserve(arguments.length() + 1)) { 650 return false; 651 } 652 MultiStringOption* mso = 653 opt_new<MultiStringOption>(1, name, help, (const char*)nullptr); 654 if (!mso) { 655 return false; 656 } 657 arguments.infallibleAppend(mso); 658 return true; 659 }