blob: b28280fb93ad972c6e0c3ea353fdef58df7e2e0e [file] [log] [blame]
/* -*- 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));
/* 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.
*/
MOZ_ASSERT(limit - it > 0);
size_t tokLen = limit - it;
MOZ_ASSERT(tokLen);
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", 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;
}