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