1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "shell/jsoptparse.h"
8
9 #include <algorithm>
10 #include <stdarg.h>
11
12 #include "util/Unicode.h"
13
14 using namespace js;
15 using namespace js::cli;
16 using namespace js::cli::detail;
17
18 const char OptionParser::prognameMeta[] = "{progname}";
19
20 #define OPTION_CONVERT_IMPL(__cls) \
21 bool Option::is##__cls##Option() const { return kind == OptionKind##__cls; } \
22 __cls##Option* Option::as##__cls##Option() { \
23 MOZ_ASSERT(is##__cls##Option()); \
24 return static_cast<__cls##Option*>(this); \
25 } \
26 const __cls##Option* Option::as##__cls##Option() const { \
27 return const_cast<Option*>(this)->as##__cls##Option(); \
28 }
29
asValued()30 ValuedOption* Option::asValued() {
31 MOZ_ASSERT(isValued());
32 return static_cast<ValuedOption*>(this);
33 }
34
asValued() const35 const ValuedOption* Option::asValued() const {
36 return const_cast<Option*>(this)->asValued();
37 }
38
39 OPTION_CONVERT_IMPL(Bool)
OPTION_CONVERT_IMPL(String)40 OPTION_CONVERT_IMPL(String)
41 OPTION_CONVERT_IMPL(Int)
42 OPTION_CONVERT_IMPL(MultiString)
43
44 void OptionParser::setArgTerminatesOptions(const char* name, bool enabled) {
45 findArgument(name)->setTerminatesOptions(enabled);
46 }
47
setArgCapturesRest(const char * name)48 void OptionParser::setArgCapturesRest(const char* name) {
49 MOZ_ASSERT(restArgument == -1,
50 "only one argument may be set to capture the rest");
51 restArgument = findArgumentIndex(name);
52 MOZ_ASSERT(restArgument != -1,
53 "unknown argument name passed to setArgCapturesRest");
54 }
55
error(const char * fmt,...)56 OptionParser::Result OptionParser::error(const char* fmt, ...) {
57 va_list args;
58 va_start(args, fmt);
59 fprintf(stderr, "Error: ");
60 vfprintf(stderr, fmt, args);
61 va_end(args);
62 fputs("\n\n", stderr);
63 return ParseError;
64 }
65
66 /* Quick and dirty paragraph printer. */
PrintParagraph(const char * text,unsigned startColno,const unsigned limitColno,bool padFirstLine)67 static void PrintParagraph(const char* text, unsigned startColno,
68 const unsigned limitColno, bool padFirstLine) {
69 unsigned colno = startColno;
70 unsigned indent = 0;
71 const char* it = text;
72
73 if (padFirstLine) {
74 printf("%*s", int(startColno), "");
75 }
76
77 /* Skip any leading spaces. */
78 while (*it != '\0' && unicode::IsSpace(*it)) {
79 ++it;
80 }
81
82 while (*it != '\0') {
83 MOZ_ASSERT(!unicode::IsSpace(*it) || *it == '\n');
84
85 /* Delimit the current token. */
86 const char* limit = it;
87 while (!unicode::IsSpace(*limit) && *limit != '\0') {
88 ++limit;
89 }
90
91 /*
92 * If the current token is longer than the available number of columns,
93 * then make a line break before printing the token.
94 */
95 size_t tokLen = limit - it;
96 if (tokLen + colno >= limitColno) {
97 printf("\n%*s%.*s", int(startColno + indent), "", int(tokLen), it);
98 colno = startColno + tokLen;
99 } else {
100 printf("%.*s", int(tokLen), it);
101 colno += tokLen;
102 }
103
104 switch (*limit) {
105 case '\0':
106 return;
107 case ' ':
108 putchar(' ');
109 colno += 1;
110 it = limit;
111 while (*it == ' ') {
112 ++it;
113 }
114 break;
115 case '\n':
116 /* |text| wants to force a newline here. */
117 printf("\n%*s", int(startColno), "");
118 colno = startColno;
119 it = limit + 1;
120 /* Could also have line-leading spaces. */
121 indent = 0;
122 while (*it == ' ') {
123 putchar(' ');
124 ++colno;
125 ++indent;
126 ++it;
127 }
128 break;
129 default:
130 MOZ_CRASH("unhandled token splitting character in text");
131 }
132 }
133 }
134
OptionFlagsToFormatInfo(char shortflag,bool isValued,size_t * length)135 static const char* OptionFlagsToFormatInfo(char shortflag, bool isValued,
136 size_t* length) {
137 static const char* const fmt[4] = {" -%c --%s ", " --%s ", " -%c --%s=%s ",
138 " --%s=%s "};
139
140 /* How mny chars w/o longflag? */
141 size_t lengths[4] = {strlen(fmt[0]) - 3, strlen(fmt[1]) - 3,
142 strlen(fmt[2]) - 5, strlen(fmt[3]) - 5};
143 int index = isValued ? 2 : 0;
144 if (!shortflag) {
145 index++;
146 }
147
148 *length = lengths[index];
149 return fmt[index];
150 }
151
printHelp(const char * progname)152 OptionParser::Result OptionParser::printHelp(const char* progname) {
153 const char* prefixEnd = strstr(usage, prognameMeta);
154 if (prefixEnd) {
155 printf("%.*s%s%s\n", int(prefixEnd - usage), usage, progname,
156 prefixEnd + sizeof(prognameMeta) - 1);
157 } else {
158 puts(usage);
159 }
160
161 if (descr) {
162 putchar('\n');
163 PrintParagraph(descr, 2, descrWidth, true);
164 putchar('\n');
165 }
166
167 if (version) {
168 printf("\nVersion: %s\n\n", version);
169 }
170
171 if (!arguments.empty()) {
172 printf("Arguments:\n");
173
174 static const char fmt[] = " %s ";
175 size_t fmtChars = sizeof(fmt) - 2;
176 size_t lhsLen = 0;
177 for (Option* arg : arguments) {
178 lhsLen = std::max(lhsLen, strlen(arg->longflag) + fmtChars);
179 }
180
181 for (Option* arg : arguments) {
182 size_t chars = printf(fmt, arg->longflag);
183 for (; chars < lhsLen; ++chars) {
184 putchar(' ');
185 }
186 PrintParagraph(arg->help, lhsLen, helpWidth, false);
187 putchar('\n');
188 }
189 putchar('\n');
190 }
191
192 if (!options.empty()) {
193 printf("Options:\n");
194
195 /* Calculate sizes for column alignment. */
196 size_t lhsLen = 0;
197 for (Option* opt : options) {
198 size_t longflagLen = strlen(opt->longflag);
199
200 size_t fmtLen;
201 OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen);
202
203 size_t len = fmtLen + longflagLen;
204 if (opt->isValued()) {
205 len += strlen(opt->asValued()->metavar);
206 }
207 lhsLen = std::max(lhsLen, len);
208 }
209
210 /* Print option help text. */
211 for (Option* opt : options) {
212 size_t fmtLen;
213 const char* fmt =
214 OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen);
215 size_t chars;
216 if (opt->isValued()) {
217 if (opt->shortflag) {
218 chars = printf(fmt, opt->shortflag, opt->longflag,
219 opt->asValued()->metavar);
220 } else {
221 chars = printf(fmt, opt->longflag, opt->asValued()->metavar);
222 }
223 } else {
224 if (opt->shortflag) {
225 chars = printf(fmt, opt->shortflag, opt->longflag);
226 } else {
227 chars = printf(fmt, opt->longflag);
228 }
229 }
230 for (; chars < lhsLen; ++chars) {
231 putchar(' ');
232 }
233 PrintParagraph(opt->help, lhsLen, helpWidth, false);
234 putchar('\n');
235 }
236 }
237
238 return EarlyExit;
239 }
240
printVersion()241 OptionParser::Result OptionParser::printVersion() {
242 MOZ_ASSERT(version);
243 printf("%s\n", version);
244 return EarlyExit;
245 }
246
extractValue(size_t argc,char ** argv,size_t * i,char ** value)247 OptionParser::Result OptionParser::extractValue(size_t argc, char** argv,
248 size_t* i, char** value) {
249 MOZ_ASSERT(*i < argc);
250 char* eq = strchr(argv[*i], '=');
251 if (eq) {
252 *value = eq + 1;
253 if (*value[0] == '\0') {
254 return error("A value is required for option %.*s", (int)(eq - argv[*i]),
255 argv[*i]);
256 }
257 return Okay;
258 }
259
260 if (argc == *i + 1) {
261 return error("Expected a value for option %s", argv[*i]);
262 }
263
264 *i += 1;
265 *value = argv[*i];
266 return Okay;
267 }
268
handleOption(Option * opt,size_t argc,char ** argv,size_t * i,bool * optionsAllowed)269 OptionParser::Result OptionParser::handleOption(Option* opt, size_t argc,
270 char** argv, size_t* i,
271 bool* optionsAllowed) {
272 if (opt->getTerminatesOptions()) {
273 *optionsAllowed = false;
274 }
275
276 switch (opt->kind) {
277 case OptionKindBool: {
278 if (opt == &helpOption) {
279 return printHelp(argv[0]);
280 }
281 if (opt == &versionOption) {
282 return printVersion();
283 }
284 opt->asBoolOption()->value = true;
285 return Okay;
286 }
287 /*
288 * Valued options are allowed to specify their values either via
289 * successive arguments or a single --longflag=value argument.
290 */
291 case OptionKindString: {
292 char* value = nullptr;
293 if (Result r = extractValue(argc, argv, i, &value)) {
294 return r;
295 }
296 opt->asStringOption()->value = value;
297 return Okay;
298 }
299 case OptionKindInt: {
300 char* value = nullptr;
301 if (Result r = extractValue(argc, argv, i, &value)) {
302 return r;
303 }
304 opt->asIntOption()->value = atoi(value);
305 return Okay;
306 }
307 case OptionKindMultiString: {
308 char* value = nullptr;
309 if (Result r = extractValue(argc, argv, i, &value)) {
310 return r;
311 }
312 StringArg arg(value, *i);
313 return opt->asMultiStringOption()->strings.append(arg) ? Okay : Fail;
314 }
315 default:
316 MOZ_CRASH("unhandled option kind");
317 }
318 }
319
handleArg(size_t argc,char ** argv,size_t * i,bool * optionsAllowed)320 OptionParser::Result OptionParser::handleArg(size_t argc, char** argv,
321 size_t* i, bool* optionsAllowed) {
322 if (nextArgument >= arguments.length()) {
323 return error("Too many arguments provided");
324 }
325
326 Option* arg = arguments[nextArgument];
327
328 if (arg->getTerminatesOptions()) {
329 *optionsAllowed = false;
330 }
331
332 switch (arg->kind) {
333 case OptionKindString:
334 arg->asStringOption()->value = argv[*i];
335 nextArgument += 1;
336 return Okay;
337 case OptionKindMultiString: {
338 // Don't advance the next argument -- there can only be one (final)
339 // variadic argument.
340 StringArg value(argv[*i], *i);
341 return arg->asMultiStringOption()->strings.append(value) ? Okay : Fail;
342 }
343 default:
344 MOZ_CRASH("unhandled argument kind");
345 }
346 }
347
parseArgs(int inputArgc,char ** argv)348 OptionParser::Result OptionParser::parseArgs(int inputArgc, char** argv) {
349 MOZ_ASSERT(inputArgc >= 0);
350 size_t argc = inputArgc;
351 // Permit a "no more options" capability, like |--| offers in many shell
352 // interfaces.
353 bool optionsAllowed = true;
354
355 for (size_t i = 1; i < argc; ++i) {
356 char* arg = argv[i];
357 Result r;
358 /* Note: solo dash option is actually a 'stdin' argument. */
359 if (arg[0] == '-' && arg[1] != '\0' && optionsAllowed) {
360 /* Option. */
361 Option* opt;
362 if (arg[1] == '-') {
363 if (arg[2] == '\0') {
364 /* End of options */
365 optionsAllowed = false;
366 nextArgument = restArgument;
367 continue;
368 } else {
369 /* Long option. */
370 opt = findOption(arg + 2);
371 if (!opt) {
372 return error("Invalid long option: %s", arg);
373 }
374 }
375 } else {
376 /* Short option */
377 if (arg[2] != '\0') {
378 return error("Short option followed by junk: %s", arg);
379 }
380 opt = findOption(arg[1]);
381 if (!opt) {
382 return error("Invalid short option: %s", arg);
383 }
384 }
385
386 r = handleOption(opt, argc, argv, &i, &optionsAllowed);
387 } else {
388 /* Argument. */
389 r = handleArg(argc, argv, &i, &optionsAllowed);
390 }
391
392 if (r != Okay) {
393 return r;
394 }
395 }
396 return Okay;
397 }
398
setHelpOption(char shortflag,const char * longflag,const char * help)399 void OptionParser::setHelpOption(char shortflag, const char* longflag,
400 const char* help) {
401 helpOption.setFlagInfo(shortflag, longflag, help);
402 }
403
getHelpOption() const404 bool OptionParser::getHelpOption() const { return helpOption.value; }
405
getBoolOption(char shortflag) const406 bool OptionParser::getBoolOption(char shortflag) const {
407 return findOption(shortflag)->asBoolOption()->value;
408 }
409
getIntOption(char shortflag) const410 int OptionParser::getIntOption(char shortflag) const {
411 return findOption(shortflag)->asIntOption()->value;
412 }
413
getStringOption(char shortflag) const414 const char* OptionParser::getStringOption(char shortflag) const {
415 return findOption(shortflag)->asStringOption()->value;
416 }
417
getMultiStringOption(char shortflag) const418 MultiStringRange OptionParser::getMultiStringOption(char shortflag) const {
419 const MultiStringOption* mso = findOption(shortflag)->asMultiStringOption();
420 return MultiStringRange(mso->strings.begin(), mso->strings.end());
421 }
422
getBoolOption(const char * longflag) const423 bool OptionParser::getBoolOption(const char* longflag) const {
424 return findOption(longflag)->asBoolOption()->value;
425 }
426
getIntOption(const char * longflag) const427 int OptionParser::getIntOption(const char* longflag) const {
428 return findOption(longflag)->asIntOption()->value;
429 }
430
getStringOption(const char * longflag) const431 const char* OptionParser::getStringOption(const char* longflag) const {
432 return findOption(longflag)->asStringOption()->value;
433 }
434
getMultiStringOption(const char * longflag) const435 MultiStringRange OptionParser::getMultiStringOption(
436 const char* longflag) const {
437 const MultiStringOption* mso = findOption(longflag)->asMultiStringOption();
438 return MultiStringRange(mso->strings.begin(), mso->strings.end());
439 }
440
~OptionParser()441 OptionParser::~OptionParser() {
442 for (Option* opt : options) {
443 js_delete<Option>(opt);
444 }
445 for (Option* arg : arguments) {
446 js_delete<Option>(arg);
447 }
448 }
449
findOption(char shortflag)450 Option* OptionParser::findOption(char shortflag) {
451 for (Option* opt : options) {
452 if (opt->shortflag == shortflag) {
453 return opt;
454 }
455 }
456
457 if (versionOption.shortflag == shortflag) {
458 return &versionOption;
459 }
460
461 return helpOption.shortflag == shortflag ? &helpOption : nullptr;
462 }
463
findOption(char shortflag) const464 const Option* OptionParser::findOption(char shortflag) const {
465 return const_cast<OptionParser*>(this)->findOption(shortflag);
466 }
467
findOption(const char * longflag)468 Option* OptionParser::findOption(const char* longflag) {
469 for (Option* opt : options) {
470 const char* target = opt->longflag;
471 if (opt->isValued()) {
472 size_t targetLen = strlen(target);
473 /* Permit a trailing equals sign on the longflag argument. */
474 for (size_t i = 0; i < targetLen; ++i) {
475 if (longflag[i] == '\0' || longflag[i] != target[i]) {
476 goto no_match;
477 }
478 }
479 if (longflag[targetLen] == '\0' || longflag[targetLen] == '=') {
480 return opt;
481 }
482 } else {
483 if (strcmp(target, longflag) == 0) {
484 return opt;
485 }
486 }
487 no_match:;
488 }
489
490 if (strcmp(versionOption.longflag, longflag) == 0) {
491 return &versionOption;
492 }
493
494 return strcmp(helpOption.longflag, longflag) ? nullptr : &helpOption;
495 }
496
findOption(const char * longflag) const497 const Option* OptionParser::findOption(const char* longflag) const {
498 return const_cast<OptionParser*>(this)->findOption(longflag);
499 }
500
501 /* Argument accessors */
502
findArgumentIndex(const char * name) const503 int OptionParser::findArgumentIndex(const char* name) const {
504 for (Option* const* it = arguments.begin(); it != arguments.end(); ++it) {
505 const char* target = (*it)->longflag;
506 if (strcmp(target, name) == 0) {
507 return it - arguments.begin();
508 }
509 }
510 return -1;
511 }
512
findArgument(const char * name)513 Option* OptionParser::findArgument(const char* name) {
514 int index = findArgumentIndex(name);
515 return (index == -1) ? nullptr : arguments[index];
516 }
517
findArgument(const char * name) const518 const Option* OptionParser::findArgument(const char* name) const {
519 int index = findArgumentIndex(name);
520 return (index == -1) ? nullptr : arguments[index];
521 }
522
getStringArg(const char * name) const523 const char* OptionParser::getStringArg(const char* name) const {
524 return findArgument(name)->asStringOption()->value;
525 }
526
getMultiStringArg(const char * name) const527 MultiStringRange OptionParser::getMultiStringArg(const char* name) const {
528 const MultiStringOption* mso = findArgument(name)->asMultiStringOption();
529 return MultiStringRange(mso->strings.begin(), mso->strings.end());
530 }
531
532 /* Option builders */
533
addIntOption(char shortflag,const char * longflag,const char * metavar,const char * help,int defaultValue)534 bool OptionParser::addIntOption(char shortflag, const char* longflag,
535 const char* metavar, const char* help,
536 int defaultValue) {
537 if (!options.reserve(options.length() + 1)) {
538 return false;
539 }
540 IntOption* io =
541 js_new<IntOption>(shortflag, longflag, help, metavar, defaultValue);
542 if (!io) {
543 return false;
544 }
545 options.infallibleAppend(io);
546 return true;
547 }
548
addBoolOption(char shortflag,const char * longflag,const char * help)549 bool OptionParser::addBoolOption(char shortflag, const char* longflag,
550 const char* help) {
551 if (!options.reserve(options.length() + 1)) {
552 return false;
553 }
554 BoolOption* bo = js_new<BoolOption>(shortflag, longflag, help);
555 if (!bo) {
556 return false;
557 }
558 options.infallibleAppend(bo);
559 return true;
560 }
561
addStringOption(char shortflag,const char * longflag,const char * metavar,const char * help)562 bool OptionParser::addStringOption(char shortflag, const char* longflag,
563 const char* metavar, const char* help) {
564 if (!options.reserve(options.length() + 1)) {
565 return false;
566 }
567 StringOption* so = js_new<StringOption>(shortflag, longflag, help, metavar);
568 if (!so) {
569 return false;
570 }
571 options.infallibleAppend(so);
572 return true;
573 }
574
addMultiStringOption(char shortflag,const char * longflag,const char * metavar,const char * help)575 bool OptionParser::addMultiStringOption(char shortflag, const char* longflag,
576 const char* metavar, const char* help) {
577 if (!options.reserve(options.length() + 1)) {
578 return false;
579 }
580 MultiStringOption* mso =
581 js_new<MultiStringOption>(shortflag, longflag, help, metavar);
582 if (!mso) {
583 return false;
584 }
585 options.infallibleAppend(mso);
586 return true;
587 }
588
589 /* Argument builders */
590
addOptionalStringArg(const char * name,const char * help)591 bool OptionParser::addOptionalStringArg(const char* name, const char* help) {
592 if (!arguments.reserve(arguments.length() + 1)) {
593 return false;
594 }
595 StringOption* so = js_new<StringOption>(1, name, help, (const char*)nullptr);
596 if (!so) {
597 return false;
598 }
599 arguments.infallibleAppend(so);
600 return true;
601 }
602
addOptionalMultiStringArg(const char * name,const char * help)603 bool OptionParser::addOptionalMultiStringArg(const char* name,
604 const char* help) {
605 MOZ_ASSERT_IF(!arguments.empty(), !arguments.back()->isVariadic());
606 if (!arguments.reserve(arguments.length() + 1)) {
607 return false;
608 }
609 MultiStringOption* mso =
610 js_new<MultiStringOption>(1, name, help, (const char*)nullptr);
611 if (!mso) {
612 return false;
613 }
614 arguments.infallibleAppend(mso);
615 return true;
616 }
617