/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "shell/jsoptparse.h" #include #include #include #include "util/Unicode.h" using namespace js; using namespace js::cli; using namespace js::cli::detail; #define OPTION_CONVERT_IMPL(__cls) \ bool Option::is##__cls##Option() const { return kind == OptionKind##__cls; } \ __cls##Option* Option::as##__cls##Option() { \ MOZ_ASSERT(is##__cls##Option()); \ return static_cast<__cls##Option*>(this); \ } \ const __cls##Option* Option::as##__cls##Option() const { \ return const_cast(this)->as##__cls##Option(); \ } ValuedOption* Option::asValued() { MOZ_ASSERT(isValued()); return static_cast(this); } const ValuedOption* Option::asValued() const { return const_cast(this)->asValued(); } OPTION_CONVERT_IMPL(Bool) OPTION_CONVERT_IMPL(String) OPTION_CONVERT_IMPL(Int) OPTION_CONVERT_IMPL(MultiString) void OptionParser::setArgTerminatesOptions(const char* name, bool enabled) { findArgument(name)->setTerminatesOptions(enabled); } void OptionParser::setArgCapturesRest(const char* name) { MOZ_ASSERT(restArgument == -1, "only one argument may be set to capture the rest"); restArgument = findArgumentIndex(name); MOZ_ASSERT(restArgument != -1, "unknown argument name passed to setArgCapturesRest"); } OptionParser::Result OptionParser::error(const char* fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "Error: "); vfprintf(stderr, fmt, args); va_end(args); fputs("\n\n", stderr); return ParseError; } /* Quick and dirty paragraph printer. */ static void PrintParagraph(const char* text, unsigned startColno, const unsigned limitColno, bool padFirstLine) { unsigned colno = startColno; unsigned indent = 0; const char* it = text; if (padFirstLine) { printf("%*s", int(startColno), ""); } /* Skip any leading spaces. */ while (*it != '\0' && unicode::IsSpace(*it)) { ++it; } while (*it != '\0') { MOZ_ASSERT(!unicode::IsSpace(*it) || *it == '\n'); /* Delimit the current token. */ const char* limit = it; while (!unicode::IsSpace(*limit) && *limit != '\0') { ++limit; } /* * If the current token is longer than the available number of columns, * then make a line break before printing the token. */ size_t tokLen = limit - it; if (tokLen + colno >= limitColno) { printf("\n%*s%.*s", int(startColno + indent), "", int(tokLen), it); colno = startColno + tokLen; } else { printf("%.*s", int(tokLen), it); colno += tokLen; } switch (*limit) { case '\0': return; case ' ': putchar(' '); colno += 1; it = limit; while (*it == ' ') { ++it; } break; case '\n': /* |text| wants to force a newline here. */ printf("\n%*s", int(startColno), ""); colno = startColno; it = limit + 1; /* Could also have line-leading spaces. */ indent = 0; while (*it == ' ') { putchar(' '); ++colno; ++indent; ++it; } break; default: MOZ_CRASH("unhandled token splitting character in text"); } } } static const char* OptionFlagsToFormatInfo(char shortflag, bool isValued, size_t* length) { static const char* const fmt[4] = {" -%c --%s ", " --%s ", " -%c --%s=%s ", " --%s=%s "}; /* How mny chars w/o longflag? */ size_t lengths[4] = {strlen(fmt[0]) - 3, strlen(fmt[1]) - 3, strlen(fmt[2]) - 5, strlen(fmt[3]) - 5}; int index = isValued ? 2 : 0; if (!shortflag) { index++; } *length = lengths[index]; return fmt[index]; } OptionParser::Result OptionParser::printHelp(const char* progname) { constexpr std::string_view prognameMeta = "{progname}"; const char* prefixEnd = strstr(usage, prognameMeta.data()); if (prefixEnd) { printf("%.*s%s%s\n", int(prefixEnd - usage), usage, progname, prefixEnd + prognameMeta.length()); } else { puts(usage); } if (descr) { putchar('\n'); PrintParagraph(descr, 2, descrWidth, true); putchar('\n'); } if (version) { printf("\nVersion: %s\n\n", version); } if (!arguments.empty()) { printf("Arguments:\n"); static const char fmt[] = " %s "; size_t fmtChars = sizeof(fmt) - 2; size_t lhsLen = 0; for (Option* arg : arguments) { lhsLen = std::max(lhsLen, strlen(arg->longflag) + fmtChars); } for (Option* arg : arguments) { size_t chars = printf(fmt, arg->longflag); for (; chars < lhsLen; ++chars) { putchar(' '); } PrintParagraph(arg->help, lhsLen, helpWidth, false); putchar('\n'); } putchar('\n'); } if (!options.empty()) { printf("Options:\n"); /* Calculate sizes for column alignment. */ size_t lhsLen = 0; for (Option* opt : options) { size_t longflagLen = strlen(opt->longflag); size_t fmtLen; OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen); size_t len = fmtLen + longflagLen; if (opt->isValued()) { len += strlen(opt->asValued()->metavar); } lhsLen = std::max(lhsLen, len); } /* Print option help text. */ for (Option* opt : options) { size_t fmtLen; const char* fmt = OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen); size_t chars; if (opt->isValued()) { if (opt->shortflag) { chars = printf(fmt, opt->shortflag, opt->longflag, opt->asValued()->metavar); } else { chars = printf(fmt, opt->longflag, opt->asValued()->metavar); } } else { if (opt->shortflag) { chars = printf(fmt, opt->shortflag, opt->longflag); } else { chars = printf(fmt, opt->longflag); } } for (; chars < lhsLen; ++chars) { putchar(' '); } PrintParagraph(opt->help, lhsLen, helpWidth, false); putchar('\n'); } } return EarlyExit; } OptionParser::Result OptionParser::printVersion() { MOZ_ASSERT(version); printf("%s\n", version); return EarlyExit; } OptionParser::Result OptionParser::extractValue(size_t argc, char** argv, size_t* i, char** value) { MOZ_ASSERT(*i < argc); char* eq = strchr(argv[*i], '='); if (eq) { *value = eq + 1; if (*value[0] == '\0') { return error("A value is required for option %.*s", (int)(eq - argv[*i]), argv[*i]); } return Okay; } if (argc == *i + 1) { return error("Expected a value for option %s", argv[*i]); } *i += 1; *value = argv[*i]; return Okay; } OptionParser::Result OptionParser::handleOption(Option* opt, size_t argc, char** argv, size_t* i, bool* optionsAllowed) { if (opt->getTerminatesOptions()) { *optionsAllowed = false; } switch (opt->kind) { case OptionKindBool: { if (opt == &helpOption) { return printHelp(argv[0]); } if (opt == &versionOption) { return printVersion(); } opt->asBoolOption()->value = true; return Okay; } /* * Valued options are allowed to specify their values either via * successive arguments or a single --longflag=value argument. */ case OptionKindString: { char* value = nullptr; if (Result r = extractValue(argc, argv, i, &value)) { return r; } opt->asStringOption()->value = value; return Okay; } case OptionKindInt: { char* value = nullptr; if (Result r = extractValue(argc, argv, i, &value)) { return r; } opt->asIntOption()->value = atoi(value); return Okay; } case OptionKindMultiString: { char* value = nullptr; if (Result r = extractValue(argc, argv, i, &value)) { return r; } StringArg arg(value, *i); return opt->asMultiStringOption()->strings.append(arg) ? Okay : Fail; } default: MOZ_CRASH("unhandled option kind"); } } OptionParser::Result OptionParser::handleArg(size_t argc, char** argv, size_t* i, bool* optionsAllowed) { if (nextArgument >= arguments.length()) { return error("Too many arguments provided"); } Option* arg = arguments[nextArgument]; if (arg->getTerminatesOptions()) { *optionsAllowed = false; } switch (arg->kind) { case OptionKindString: arg->asStringOption()->value = argv[*i]; nextArgument += 1; return Okay; case OptionKindMultiString: { // Don't advance the next argument -- there can only be one (final) // variadic argument. StringArg value(argv[*i], *i); return arg->asMultiStringOption()->strings.append(value) ? Okay : Fail; } default: MOZ_CRASH("unhandled argument kind"); } } OptionParser::Result OptionParser::parseArgs(int inputArgc, char** argv) { MOZ_ASSERT(inputArgc >= 0); size_t argc = inputArgc; // Permit a "no more options" capability, like |--| offers in many shell // interfaces. bool optionsAllowed = true; for (size_t i = 1; i < argc; ++i) { char* arg = argv[i]; Result r; /* Note: solo dash option is actually a 'stdin' argument. */ if (arg[0] == '-' && arg[1] != '\0' && optionsAllowed) { /* Option. */ Option* opt; if (arg[1] == '-') { if (arg[2] == '\0') { /* End of options */ optionsAllowed = false; nextArgument = restArgument; continue; } else { /* Long option. */ opt = findOption(arg + 2); if (!opt) { return error("Invalid long option: %s", arg); } } } else { /* Short option */ if (arg[2] != '\0') { return error("Short option followed by junk: %s", arg); } opt = findOption(arg[1]); if (!opt) { return error("Invalid short option: %s", arg); } } r = handleOption(opt, argc, argv, &i, &optionsAllowed); } else { /* Argument. */ r = handleArg(argc, argv, &i, &optionsAllowed); } if (r != Okay) { return r; } } return Okay; } void OptionParser::setHelpOption(char shortflag, const char* longflag, const char* help) { helpOption.setFlagInfo(shortflag, longflag, help); } bool OptionParser::getHelpOption() const { return helpOption.value; } bool OptionParser::getBoolOption(char shortflag) const { return tryFindOption(shortflag)->asBoolOption()->value; } int OptionParser::getIntOption(char shortflag) const { return tryFindOption(shortflag)->asIntOption()->value; } const char* OptionParser::getStringOption(char shortflag) const { return tryFindOption(shortflag)->asStringOption()->value; } MultiStringRange OptionParser::getMultiStringOption(char shortflag) const { const MultiStringOption* mso = tryFindOption(shortflag)->asMultiStringOption(); return MultiStringRange(mso->strings.begin(), mso->strings.end()); } bool OptionParser::getBoolOption(const char* longflag) const { return tryFindOption(longflag)->asBoolOption()->value; } int OptionParser::getIntOption(const char* longflag) const { return tryFindOption(longflag)->asIntOption()->value; } const char* OptionParser::getStringOption(const char* longflag) const { return tryFindOption(longflag)->asStringOption()->value; } MultiStringRange OptionParser::getMultiStringOption( const char* longflag) const { const MultiStringOption* mso = tryFindOption(longflag)->asMultiStringOption(); return MultiStringRange(mso->strings.begin(), mso->strings.end()); } OptionParser::~OptionParser() { for (Option* opt : options) { js_delete