diff options
Diffstat (limited to 'js/src/shell/jsoptparse.cpp')
-rw-r--r-- | js/src/shell/jsoptparse.cpp | 644 |
1 files changed, 644 insertions, 0 deletions
diff --git a/js/src/shell/jsoptparse.cpp b/js/src/shell/jsoptparse.cpp new file mode 100644 index 000000000..eaf4042d3 --- /dev/null +++ b/js/src/shell/jsoptparse.cpp @@ -0,0 +1,644 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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 <ctype.h> +#include <stdarg.h> + +#include "jsutil.h" + +using namespace js; +using namespace js::cli; +using namespace js::cli::detail; + +const char OptionParser::prognameMeta[] = "{progname}"; + +#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<Option*>(this)->as##__cls##Option(); \ + } + +ValuedOption* +Option::asValued() +{ + MOZ_ASSERT(isValued()); + return static_cast<ValuedOption*>(this); +} + +const ValuedOption* +Option::asValued() const +{ + return const_cast<Option*>(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", startColno, ""); + + /* Skip any leading spaces. */ + while (*it != '\0' && isspace(*it)) + ++it; + + while (*it != '\0') { + MOZ_ASSERT(!isspace(*it) || *it == '\n'); + + /* Delimit the current token. */ + const char* limit = it; + while (!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", 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", 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) +{ + const char* prefixEnd = strstr(usage, prognameMeta); + if (prefixEnd) { + printf("%.*s%s%s\n", int(prefixEnd - usage), usage, progname, + prefixEnd + sizeof(prognameMeta) - 1); + } 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 = 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 = 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 findOption(shortflag)->asBoolOption()->value; +} + +int +OptionParser::getIntOption(char shortflag) const +{ + return findOption(shortflag)->asIntOption()->value; +} + +const char* +OptionParser::getStringOption(char shortflag) const +{ + return findOption(shortflag)->asStringOption()->value; +} + +MultiStringRange +OptionParser::getMultiStringOption(char shortflag) const +{ + const MultiStringOption* mso = findOption(shortflag)->asMultiStringOption(); + return MultiStringRange(mso->strings.begin(), mso->strings.end()); +} + +bool +OptionParser::getBoolOption(const char* longflag) const +{ + return findOption(longflag)->asBoolOption()->value; +} + +int +OptionParser::getIntOption(const char* longflag) const +{ + return findOption(longflag)->asIntOption()->value; +} + +const char* +OptionParser::getStringOption(const char* longflag) const +{ + return findOption(longflag)->asStringOption()->value; +} + +MultiStringRange +OptionParser::getMultiStringOption(const char* longflag) const +{ + const MultiStringOption* mso = findOption(longflag)->asMultiStringOption(); + return MultiStringRange(mso->strings.begin(), mso->strings.end()); +} + +OptionParser::~OptionParser() +{ + for (Option* opt : options) + js_delete<Option>(opt); + for (Option* arg : arguments) + js_delete<Option>(arg); +} + +Option* +OptionParser::findOption(char shortflag) +{ + for (Option* opt : options) { + if (opt->shortflag == shortflag) + return opt; + } + + if (versionOption.shortflag == shortflag) + return &versionOption; + + return helpOption.shortflag == shortflag ? &helpOption : nullptr; +} + +const Option* +OptionParser::findOption(char shortflag) const +{ + return const_cast<OptionParser*>(this)->findOption(shortflag); +} + +Option* +OptionParser::findOption(const char* longflag) +{ + for (Option* opt : options) { + const char* target = opt->longflag; + if (opt->isValued()) { + size_t targetLen = strlen(target); + /* Permit a trailing equals sign on the longflag argument. */ + for (size_t i = 0; i < targetLen; ++i) { + if (longflag[i] == '\0' || longflag[i] != target[i]) + goto no_match; + } + if (longflag[targetLen] == '\0' || longflag[targetLen] == '=') + return opt; + } else { + if (strcmp(target, longflag) == 0) + return opt; + } + no_match:; + } + + if (strcmp(versionOption.longflag, longflag) == 0) + return &versionOption; + + return strcmp(helpOption.longflag, longflag) ? nullptr : &helpOption; +} + +const Option* +OptionParser::findOption(const char* longflag) const +{ + return const_cast<OptionParser*>(this)->findOption(longflag); +} + +/* Argument accessors */ + +int +OptionParser::findArgumentIndex(const char* name) const +{ + for (Option * const* it = arguments.begin(); it != arguments.end(); ++it) { + const char* target = (*it)->longflag; + if (strcmp(target, name) == 0) + return it - arguments.begin(); + } + return -1; +} + +Option* +OptionParser::findArgument(const char* name) +{ + int index = findArgumentIndex(name); + return (index == -1) ? nullptr : arguments[index]; +} + +const Option* +OptionParser::findArgument(const char* name) const +{ + int index = findArgumentIndex(name); + return (index == -1) ? nullptr : arguments[index]; +} + +const char* +OptionParser::getStringArg(const char* name) const +{ + return findArgument(name)->asStringOption()->value; +} + +MultiStringRange +OptionParser::getMultiStringArg(const char* name) const +{ + const MultiStringOption* mso = findArgument(name)->asMultiStringOption(); + return MultiStringRange(mso->strings.begin(), mso->strings.end()); +} + +/* Option builders */ + +bool +OptionParser::addIntOption(char shortflag, const char* longflag, const char* metavar, + const char* help, int defaultValue) +{ + if (!options.reserve(options.length() + 1)) + return false; + IntOption* io = js_new<IntOption>(shortflag, longflag, help, metavar, defaultValue); + if (!io) + return false; + options.infallibleAppend(io); + return true; +} + +bool +OptionParser::addBoolOption(char shortflag, const char* longflag, const char* help) +{ + if (!options.reserve(options.length() + 1)) + return false; + BoolOption* bo = js_new<BoolOption>(shortflag, longflag, help); + if (!bo) + return false; + options.infallibleAppend(bo); + return true; +} + +bool +OptionParser::addStringOption(char shortflag, const char* longflag, const char* metavar, + const char* help) +{ + if (!options.reserve(options.length() + 1)) + return false; + StringOption* so = js_new<StringOption>(shortflag, longflag, help, metavar); + if (!so) + return false; + options.infallibleAppend(so); + return true; +} + +bool +OptionParser::addMultiStringOption(char shortflag, const char* longflag, const char* metavar, + const char* help) +{ + if (!options.reserve(options.length() + 1)) + return false; + MultiStringOption* mso = js_new<MultiStringOption>(shortflag, longflag, help, metavar); + if (!mso) + return false; + options.infallibleAppend(mso); + return true; +} + +/* Argument builders */ + +bool +OptionParser::addOptionalStringArg(const char* name, const char* help) +{ + if (!arguments.reserve(arguments.length() + 1)) + return false; + StringOption* so = js_new<StringOption>(1, name, help, (const char*) nullptr); + if (!so) + return false; + arguments.infallibleAppend(so); + return true; +} + +bool +OptionParser::addOptionalMultiStringArg(const char* name, const char* help) +{ + MOZ_ASSERT_IF(!arguments.empty(), !arguments.back()->isVariadic()); + if (!arguments.reserve(arguments.length() + 1)) + return false; + MultiStringOption* mso = js_new<MultiStringOption>(1, name, help, (const char*) nullptr); + if (!mso) + return false; + arguments.infallibleAppend(mso); + return true; +} |