/* -*- 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; }