| /* -*- 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 "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() \ |
| { \ |
| JS_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() |
| { |
| JS_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; |
| const char *it = text; |
| |
| if (padFirstLine) |
| printf("%*s", startColno, ""); |
| |
| while (*it != '\0') { |
| JS_ASSERT(!isspace(*it)); |
| |
| /* 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. |
| */ |
| JS_ASSERT(limit - it > 0); |
| size_t tokLen = limit - it; |
| JS_ASSERT(tokLen); |
| if (tokLen + colno >= limitColno) { |
| printf("\n%*s%.*s", startColno, "", 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. */ |
| while (*it == ' ') { |
| putchar(' '); |
| ++colno; |
| ++it; |
| } |
| break; |
| default: |
| JS_NOT_REACHED("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 (ver) |
| printf("\nVersion: %s\n\n", ver); |
| |
| if (!arguments.empty()) { |
| printf("Arguments:\n"); |
| |
| static const char fmt[] = " %s "; |
| size_t fmtChars = sizeof(fmt) - 2; |
| size_t lhsLen = 0; |
| for (Option **it = arguments.begin(), **end = arguments.end(); it != end; ++it) |
| lhsLen = Max(lhsLen, strlen((*it)->longflag) + fmtChars); |
| |
| for (Option **it = arguments.begin(), **end = arguments.end(); it != end; ++it) { |
| Option *arg = *it; |
| 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 **it = options.begin(), **end = options.end(); it != end; ++it) { |
| Option *opt = *it; |
| 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 **it = options.begin(), **end = options.end(); it != end; ++it) { |
| Option *opt = *it; |
| 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 ParseHelp; |
| } |
| |
| OptionParser::Result |
| OptionParser::extractValue(size_t argc, char **argv, size_t *i, char **value) |
| { |
| JS_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", 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]); |
| 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 = NULL; |
| if (Result r = extractValue(argc, argv, i, &value)) |
| return r; |
| opt->asStringOption()->value = value; |
| return Okay; |
| } |
| case OptionKindInt: |
| { |
| char *value = NULL; |
| if (Result r = extractValue(argc, argv, i, &value)) |
| return r; |
| opt->asIntOption()->value = atoi(value); |
| return Okay; |
| } |
| case OptionKindMultiString: |
| { |
| char *value = NULL; |
| if (Result r = extractValue(argc, argv, i, &value)) |
| return r; |
| StringArg arg(value, *i); |
| return opt->asMultiStringOption()->strings.append(arg) ? Okay : Fail; |
| } |
| default: |
| JS_NOT_REACHED("unhandled option kind"); |
| return Fail; |
| } |
| } |
| |
| 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: |
| JS_NOT_REACHED("unhandled argument kind"); |
| return Fail; |
| } |
| } |
| |
| OptionParser::Result |
| OptionParser::parseArgs(int inputArgc, char **argv) |
| { |
| JS_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 **it = options.begin(), **end = options.end(); it != end; ++it) |
| js_delete<Option>(*it); |
| for (Option **it = arguments.begin(), **end = arguments.end(); it != end; ++it) |
| js_delete<Option>(*it); |
| } |
| |
| Option * |
| OptionParser::findOption(char shortflag) |
| { |
| for (Option **it = options.begin(), **end = options.end(); it != end; ++it) { |
| if ((*it)->shortflag == shortflag) |
| return *it; |
| } |
| |
| return helpOption.shortflag == shortflag ? &helpOption : NULL; |
| } |
| |
| const Option * |
| OptionParser::findOption(char shortflag) const |
| { |
| return const_cast<OptionParser *>(this)->findOption(shortflag); |
| } |
| |
| Option * |
| OptionParser::findOption(const char *longflag) |
| { |
| for (Option **it = options.begin(), **end = options.end(); it != end; ++it) { |
| const char *target = (*it)->longflag; |
| if ((*it)->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 *it; |
| } else { |
| if (strcmp(target, longflag) == 0) |
| return *it; |
| } |
| no_match:; |
| } |
| |
| return strcmp(helpOption.longflag, longflag) ? NULL : &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) ? NULL : arguments[index]; |
| } |
| |
| const Option * |
| OptionParser::findArgument(const char *name) const |
| { |
| int index = findArgumentIndex(name); |
| return (index == -1) ? NULL : 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 *) NULL); |
| if (!so) |
| return false; |
| arguments.infallibleAppend(so); |
| return true; |
| } |
| |
| bool |
| OptionParser::addOptionalMultiStringArg(const char *name, const char *help) |
| { |
| JS_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 *) NULL); |
| if (!mso) |
| return false; |
| arguments.infallibleAppend(mso); |
| return true; |
| } |