blob: 18be826a4b196834018f3af0d0dde54353574002 [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 "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;
}