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