UNPKG

36.9 kBJavaScriptView Raw
1/**
2 * dashdash - A light, featureful and explicit option parsing library for
3 * node.js.
4 */
5// vim: set ts=4 sts=4 sw=4 et:
6
7var assert = require('assert-plus');
8var format = require('util').format;
9var fs = require('fs');
10var path = require('path');
11
12// ---- internal support stuff
13
14// Replace {{variable}} in `s` with the template data in `d`.
15function renderTemplate(s, d) {
16 return s.replace(/{{([a-zA-Z]+)}}/g, function onMatch(match, key) {
17 return Object.prototype.hasOwnProperty.call(d, key) ? d[key] : match;
18 });
19}
20
21/**
22 * Return a shallow copy of the given object;
23 */
24function shallowCopy(obj) {
25 if (!obj) {
26 return obj;
27 }
28 var copy = {};
29 Object.keys(obj).forEach(function onK(k) {
30 copy[k] = obj[k];
31 });
32 return copy;
33}
34
35function space(n) {
36 var s = '';
37 for (var i = 0; i < n; i++) {
38 s += ' ';
39 }
40 return s;
41}
42
43function makeIndent(arg, deflen, name) {
44 if (arg === null || arg === undefined) {
45 return space(deflen);
46 } else if (typeof arg === 'number') {
47 return space(arg);
48 } else if (typeof arg === 'string') {
49 return arg;
50 } else {
51 assert.fail('invalid "' + name + '": not a string or number: ' + arg);
52 }
53}
54
55/**
56 * Return an array of lines wrapping the given text to the given width.
57 * This splits on whitespace. Single tokens longer than `width` are not
58 * broken up.
59 */
60function textwrap(s, width) {
61 var words = s.trim().split(/\s+/);
62 var lines = [];
63 var line = '';
64 words.forEach(function onWord(w) {
65 var newLength = line.length + w.length;
66 if (line.length > 0) {
67 newLength += 1;
68 }
69 if (newLength > width) {
70 lines.push(line);
71 line = '';
72 }
73 if (line.length > 0) {
74 line += ' ';
75 }
76 line += w;
77 });
78 lines.push(line);
79 return lines;
80}
81
82/**
83 * Transform an option name to a "key" that is used as the field
84 * on the `opts` object returned from `<parser>.parse()`.
85 *
86 * Transformations:
87 * - '-' -> '_': This allow one to use hyphen in option names (common)
88 * but not have to do silly things like `opt["dry-run"]` to access the
89 * parsed results.
90 */
91function optionKeyFromName(name) {
92 return name.replace(/-/g, '_');
93}
94
95// ---- Option types
96
97function parseBool(option, optstr, arg) {
98 return Boolean(arg);
99}
100
101function parseString(option, optstr, arg) {
102 assert.string(arg, 'arg');
103 return arg;
104}
105
106function parseNumber(option, optstr, arg) {
107 assert.string(arg, 'arg');
108 var num = Number(arg);
109 if (isNaN(num)) {
110 throw new Error(
111 format('arg for "%s" is not a number: "%s"', optstr, arg)
112 );
113 }
114 return num;
115}
116
117function parseInteger(option, optstr, arg) {
118 assert.string(arg, 'arg');
119 var num = Number(arg);
120 if (!/^[0-9-]+$/.test(arg) || isNaN(num)) {
121 throw new Error(
122 format('arg for "%s" is not an integer: "%s"', optstr, arg)
123 );
124 }
125 return num;
126}
127
128function parsePositiveInteger(option, optstr, arg) {
129 assert.string(arg, 'arg');
130 var num = Number(arg);
131 if (!/^[0-9]+$/.test(arg) || isNaN(num) || num === 0) {
132 throw new Error(
133 format('arg for "%s" is not a positive integer: "%s"', optstr, arg)
134 );
135 }
136 return num;
137}
138
139/**
140 * Supported date args:
141 * - epoch second times (e.g. 1396031701)
142 * - ISO 8601 format: YYYY-MM-DD[THH:MM:SS[.sss][Z]]
143 * 2014-03-28T18:35:01.489Z
144 * 2014-03-28T18:35:01.489
145 * 2014-03-28T18:35:01Z
146 * 2014-03-28T18:35:01
147 * 2014-03-28
148 */
149function parseDate(option, optstr, arg) {
150 assert.string(arg, 'arg');
151 var date;
152 if (/^\d+$/.test(arg)) {
153 // epoch seconds
154 date = new Date(Number(arg) * 1000);
155 } else if (
156 /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?Z?)?$/i.test(arg)
157 ) {
158 // ISO 8601 format
159 date = new Date(arg);
160 } else {
161 throw new Error(
162 format('arg for "%s" is not a valid date format: "%s"', optstr, arg)
163 );
164 }
165 if (date.toString() === 'Invalid Date') {
166 throw new Error(
167 format('arg for "%s" is an invalid date: "%s"', optstr, arg)
168 );
169 }
170 return date;
171}
172
173var optionTypes = {
174 bool: {
175 takesArg: false,
176 parseArg: parseBool
177 },
178 string: {
179 takesArg: true,
180 helpArg: 'ARG',
181 parseArg: parseString
182 },
183 number: {
184 takesArg: true,
185 helpArg: 'NUM',
186 parseArg: parseNumber
187 },
188 integer: {
189 takesArg: true,
190 helpArg: 'INT',
191 parseArg: parseInteger
192 },
193 positiveInteger: {
194 takesArg: true,
195 helpArg: 'INT',
196 parseArg: parsePositiveInteger
197 },
198 date: {
199 takesArg: true,
200 helpArg: 'DATE',
201 parseArg: parseDate
202 },
203 arrayOfBool: {
204 takesArg: false,
205 array: true,
206 parseArg: parseBool
207 },
208 arrayOfString: {
209 takesArg: true,
210 helpArg: 'ARG',
211 array: true,
212 parseArg: parseString
213 },
214 arrayOfNumber: {
215 takesArg: true,
216 helpArg: 'NUM',
217 array: true,
218 parseArg: parseNumber
219 },
220 arrayOfInteger: {
221 takesArg: true,
222 helpArg: 'INT',
223 array: true,
224 parseArg: parseInteger
225 },
226 arrayOfPositiveInteger: {
227 takesArg: true,
228 helpArg: 'INT',
229 array: true,
230 parseArg: parsePositiveInteger
231 },
232 arrayOfDate: {
233 takesArg: true,
234 helpArg: 'INT',
235 array: true,
236 parseArg: parseDate
237 }
238};
239
240// ---- Parser
241
242/**
243 * Parser constructor.
244 *
245 * @param config {Object} The parser configuration
246 * - options {Array} Array of option specs. See the README for how to
247 * specify each option spec.
248 * - allowUnknown {Boolean} Default false. Whether to throw on unknown
249 * options. If false, then unknown args are included in the _args array.
250 * - interspersed {Boolean} Default true. Whether to allow interspersed
251 * arguments (non-options) and options. E.g.:
252 * node tool.js arg1 arg2 -v
253 * '-v' is after some args here. If `interspersed: false` then '-v'
254 * would not be parsed out. Note that regardless of `interspersed`
255 * the presence of '--' will stop option parsing, as all good
256 * option parsers should.
257 */
258function Parser(config) {
259 assert.object(config, 'config');
260 assert.arrayOfObject(config.options, 'config.options');
261 assert.optionalBool(config.interspersed, 'config.interspersed');
262 var self = this;
263
264 // Allow interspersed arguments (true by default).
265 this.interspersed =
266 config.interspersed !== undefined ? config.interspersed : true;
267
268 // Don't allow unknown flags (true by default).
269 this.allowUnknown =
270 config.allowUnknown !== undefined ? config.allowUnknown : false;
271
272 this.options = config.options.map(function onOpt(o) {
273 return shallowCopy(o);
274 });
275 this.optionFromName = {};
276 this.optionFromEnv = {};
277 for (var i = 0; i < this.options.length; i++) {
278 var o = this.options[i];
279 if (o.group !== undefined && o.group !== null) {
280 assert.optionalString(
281 o.group,
282 format('config.options.%d.group', i)
283 );
284 continue;
285 }
286 assert.ok(
287 optionTypes[o.type],
288 format('invalid config.options.%d.type: "%s" in %j', i, o.type, o)
289 );
290 assert.optionalString(o.name, format('config.options.%d.name', i));
291 assert.optionalArrayOfString(
292 o.names,
293 format('config.options.%d.names', i)
294 );
295 assert.ok(
296 (o.name || o.names) && !(o.name && o.names),
297 format('exactly one of "name" or "names" required: %j', o)
298 );
299 assert.optionalString(o.help, format('config.options.%d.help', i));
300 var env = o.env || [];
301 if (typeof env === 'string') {
302 env = [env];
303 }
304 assert.optionalArrayOfString(env, format('config.options.%d.env', i));
305 assert.optionalString(
306 o.helpGroup,
307 format('config.options.%d.helpGroup', i)
308 );
309 assert.optionalBool(
310 o.helpWrap,
311 format('config.options.%d.helpWrap', i)
312 );
313 assert.optionalBool(o.hidden, format('config.options.%d.hidden', i));
314
315 if (o.name) {
316 o.names = [o.name];
317 } else {
318 assert.string(
319 o.names[0],
320 format('config.options.%d.names is empty', i)
321 );
322 }
323 o.key = optionKeyFromName(o.names[0]);
324 o.names.forEach(function onName(n) {
325 if (self.optionFromName[n]) {
326 throw new Error(
327 format(
328 'option name collision: "%s" used in %j and %j',
329 n,
330 self.optionFromName[n],
331 o
332 )
333 );
334 }
335 self.optionFromName[n] = o;
336 });
337 env.forEach(function onName(n) {
338 if (self.optionFromEnv[n]) {
339 throw new Error(
340 format(
341 'option env collision: "%s" used in %j and %j',
342 n,
343 self.optionFromEnv[n],
344 o
345 )
346 );
347 }
348 self.optionFromEnv[n] = o;
349 });
350 }
351}
352
353Parser.prototype.optionTakesArg = function optionTakesArg(option) {
354 return optionTypes[option.type].takesArg;
355};
356
357/**
358 * Parse options from the given argv.
359 *
360 * @param inputs {Object} Optional.
361 * - argv {Array} Optional. The argv to parse. Defaults to
362 * `process.argv`.
363 * - slice {Number} The index into argv at which options/args begin.
364 * Default is 2, as appropriate for `process.argv`.
365 * - env {Object} Optional. The env to use for 'env' entries in the
366 * option specs. Defaults to `process.env`.
367 * @returns {Object} Parsed `opts`. It has special keys `_args` (the
368 * remaining args from `argv`) and `_order` (gives the order that
369 * options were specified).
370 */
371Parser.prototype.parse = function parse(inputs) {
372 var self = this;
373
374 // Old API was `parse([argv, [slice]])`
375 if (Array.isArray(arguments[0])) {
376 inputs = {argv: arguments[0], slice: arguments[1]};
377 }
378
379 assert.optionalObject(inputs, 'inputs');
380 if (!inputs) {
381 inputs = {};
382 }
383 assert.optionalArrayOfString(inputs.argv, 'inputs.argv');
384 //assert.optionalNumber(slice, 'slice');
385 var argv = inputs.argv || process.argv;
386 var slice = inputs.slice !== undefined ? inputs.slice : 2;
387 var args = argv.slice(slice);
388 var env = inputs.env || process.env;
389 var opts = {};
390 var _order = [];
391
392 function addOpt(option, optstr, key, val, from) {
393 var type = optionTypes[option.type];
394 var parsedVal = type.parseArg(option, optstr, val);
395 if (type.array) {
396 if (!opts[key]) {
397 opts[key] = [];
398 }
399 if (type.arrayFlatten && Array.isArray(parsedVal)) {
400 for (var i = 0; i < parsedVal.length; i++) {
401 opts[key].push(parsedVal[i]);
402 }
403 } else {
404 opts[key].push(parsedVal);
405 }
406 } else {
407 opts[key] = parsedVal;
408 }
409 var item = {key: key, value: parsedVal, from: from};
410 _order.push(item);
411 }
412
413 // Parse args.
414 var _args = [];
415 var arg;
416 var i = 0;
417 var idx;
418 var name;
419 var option;
420 var takesArg;
421 var val;
422 outer: while (i < args.length) {
423 arg = args[i];
424
425 // End of options marker.
426 if (arg === '--') {
427 i++;
428 break;
429
430 // Long option
431 } else if (arg.slice(0, 2) === '--') {
432 name = arg.slice(2);
433 val = null;
434 idx = name.indexOf('=');
435 if (idx !== -1) {
436 val = name.slice(idx + 1);
437 name = name.slice(0, idx);
438 }
439 option = this.optionFromName[name];
440 if (!option) {
441 if (!this.allowUnknown) {
442 throw new Error(format('unknown option: "--%s"', name));
443 } else if (this.interspersed) {
444 _args.push(arg);
445 } else {
446 break outer;
447 }
448 } else {
449 takesArg = this.optionTakesArg(option);
450 if (val !== null && !takesArg) {
451 throw new Error(
452 format(
453 'argument given to "--%s" option ' +
454 'that does not take one: "%s"',
455 name,
456 arg
457 )
458 );
459 }
460 if (!takesArg) {
461 addOpt(option, '--' + name, option.key, true, 'argv');
462 } else if (val !== null) {
463 addOpt(option, '--' + name, option.key, val, 'argv');
464 } else if (i + 1 >= args.length) {
465 throw new Error(
466 format(
467 'do not have enough args for "--%s" ' + 'option',
468 name
469 )
470 );
471 } else {
472 addOpt(
473 option,
474 '--' + name,
475 option.key,
476 args[i + 1],
477 'argv'
478 );
479 i++;
480 }
481 }
482
483 // Short option
484 } else if (arg[0] === '-' && arg.length > 1) {
485 var j = 1;
486 var allFound = true;
487 while (j < arg.length) {
488 name = arg[j];
489 option = this.optionFromName[name];
490 if (!option) {
491 allFound = false;
492 if (this.allowUnknown) {
493 if (this.interspersed) {
494 _args.push(arg);
495 break;
496 } else {
497 break outer;
498 }
499 } else if (arg.length > 2) {
500 throw new Error(
501 format(
502 'unknown option: "-%s" in "%s" group',
503 name,
504 arg
505 )
506 );
507 } else {
508 throw new Error(format('unknown option: "-%s"', name));
509 }
510 } else if (this.optionTakesArg(option)) {
511 break;
512 }
513 j++;
514 }
515
516 j = 1;
517 while (allFound && j < arg.length) {
518 name = arg[j];
519 val = arg.slice(j + 1); // option val if it takes an arg
520 option = this.optionFromName[name];
521 takesArg = this.optionTakesArg(option);
522 if (!takesArg) {
523 addOpt(option, '-' + name, option.key, true, 'argv');
524 } else if (val) {
525 addOpt(option, '-' + name, option.key, val, 'argv');
526 break;
527 } else {
528 if (i + 1 >= args.length) {
529 throw new Error(
530 format(
531 'do not have enough args ' + 'for "-%s" option',
532 name
533 )
534 );
535 }
536 addOpt(option, '-' + name, option.key, args[i + 1], 'argv');
537 i++;
538 break;
539 }
540 j++;
541 }
542
543 // An interspersed arg
544 } else if (this.interspersed) {
545 _args.push(arg);
546
547 // An arg and interspersed args are not allowed, so done options.
548 } else {
549 break outer;
550 }
551 i++;
552 }
553 _args = _args.concat(args.slice(i));
554
555 // Parse environment.
556 Object.keys(this.optionFromEnv).forEach(function onEnvname(envname) {
557 var val = env[envname];
558 if (val === undefined) {
559 return;
560 }
561 var option = self.optionFromEnv[envname];
562 if (opts[option.key] !== undefined) {
563 return;
564 }
565 var takesArg = self.optionTakesArg(option);
566 if (takesArg) {
567 addOpt(option, envname, option.key, val, 'env');
568 } else if (val !== '') {
569 // Boolean envvar handling:
570 // - VAR=<empty-string> not set (as if the VAR was not set)
571 // - VAR=0 false
572 // - anything else true
573 addOpt(option, envname, option.key, val !== '0', 'env');
574 }
575 });
576
577 // Apply default values.
578 this.options.forEach(function onOpt(o) {
579 if (opts[o.key] === undefined) {
580 if (o.default !== undefined) {
581 opts[o.key] = o.default;
582 } else if (o.type && optionTypes[o.type].default !== undefined) {
583 opts[o.key] = optionTypes[o.type].default;
584 }
585 }
586 });
587
588 opts._order = _order;
589 opts._args = _args;
590 return opts;
591};
592
593/**
594 * Return help output for the current options.
595 *
596 * E.g.: if the current options are:
597 * [{names: ['help', 'h'], type: 'bool', help: 'Show help and exit.'}]
598 * then this would return:
599 * ' -h, --help Show help and exit.\n'
600 *
601 * @param config {Object} Config for controlling the option help output.
602 * - indent {Number|String} Default 4. An indent/prefix to use for
603 * each option line.
604 * - nameSort {String} Default is 'length'. By default the names are
605 * sorted to put the short opts first (i.e. '-h, --help' preferred
606 * to '--help, -h'). Set to 'none' to not do this sorting.
607 * - maxCol {Number} Default 80. Note that long tokens in a help string
608 * can go past this.
609 * - helpCol {Number} Set to specify a specific column at which
610 * option help will be aligned. By default this is determined
611 * automatically.
612 * - minHelpCol {Number} Default 20.
613 * - maxHelpCol {Number} Default 40.
614 * - includeEnv {Boolean} Default false. If true, a note stating the `env`
615 * envvar (if specified for this option) will be appended to the help
616 * output.
617 * - includeDefault {Boolean} Default false. If true, a note stating
618 * the `default` for this option, if any, will be appended to the help
619 * output.
620 * - helpWrap {Boolean} Default true. Wrap help text in helpCol..maxCol
621 * bounds.
622 * @returns {String}
623 */
624Parser.prototype.help = function help(config) {
625 config = config || {};
626 assert.object(config, 'config');
627
628 var indent = makeIndent(config.indent, 4, 'config.indent');
629 var headingIndent = makeIndent(
630 config.headingIndent,
631 Math.round(indent.length / 2),
632 'config.headingIndent'
633 );
634
635 assert.optionalString(config.nameSort, 'config.nameSort');
636 var nameSort = config.nameSort || 'length';
637 assert.ok(
638 ~['length', 'none'].indexOf(nameSort),
639 'invalid "config.nameSort"'
640 );
641 assert.optionalNumber(config.maxCol, 'config.maxCol');
642 assert.optionalNumber(config.maxHelpCol, 'config.maxHelpCol');
643 assert.optionalNumber(config.minHelpCol, 'config.minHelpCol');
644 assert.optionalNumber(config.helpCol, 'config.helpCol');
645 assert.optionalBool(config.includeEnv, 'config.includeEnv');
646 assert.optionalBool(config.includeDefault, 'config.includeDefault');
647 assert.optionalBool(config.helpWrap, 'config.helpWrap');
648 var maxCol = config.maxCol || 80;
649 var minHelpCol = config.minHelpCol || 20;
650 var maxHelpCol = config.maxHelpCol || 40;
651
652 var lines = [];
653 var maxWidth = 0;
654 this.options.forEach(function onOpt(o) {
655 if (o.hidden) {
656 return;
657 }
658 if (o.group !== undefined && o.group !== null) {
659 // We deal with groups in the next pass
660 lines.push(null);
661 return;
662 }
663 var type = optionTypes[o.type];
664 var arg = o.helpArg || type.helpArg || 'ARG';
665 var line = '';
666 var names = o.names.slice();
667 if (nameSort === 'length') {
668 names.sort(function onCmp(a, b) {
669 if (a.length < b.length) {
670 return -1;
671 } else if (b.length < a.length) {
672 return 1;
673 } else {
674 return 0;
675 }
676 });
677 }
678 names.forEach(function onName(name, i) {
679 if (i > 0) {
680 line += ', ';
681 }
682 if (name.length === 1) {
683 line += '-' + name;
684 if (type.takesArg) {
685 line += ' ' + arg;
686 }
687 } else {
688 line += '--' + name;
689 if (type.takesArg) {
690 line += '=' + arg;
691 }
692 }
693 });
694 maxWidth = Math.max(maxWidth, line.length);
695 lines.push(line);
696 });
697
698 // Add help strings.
699 var helpCol = config.helpCol;
700 if (!helpCol) {
701 helpCol = maxWidth + indent.length + 2;
702 helpCol = Math.min(Math.max(helpCol, minHelpCol), maxHelpCol);
703 }
704 var i = -1;
705 this.options.forEach(function onOpt(o) {
706 if (o.hidden) {
707 return;
708 }
709 i++;
710
711 if (o.group !== undefined && o.group !== null) {
712 if (o.group === '') {
713 // Support a empty string "group" to have a blank line between
714 // sets of options.
715 lines[i] = '';
716 } else {
717 // Render the group heading with the heading-specific indent.
718 lines[i] =
719 (i === 0 ? '' : '\n') + headingIndent + o.group + ':';
720 }
721 return;
722 }
723
724 var helpDefault;
725 if (config.includeDefault) {
726 if (o.default !== undefined) {
727 helpDefault = format('Default: %j', o.default);
728 } else if (o.type && optionTypes[o.type].default !== undefined) {
729 helpDefault = format(
730 'Default: %j',
731 optionTypes[o.type].default
732 );
733 }
734 }
735
736 var line = (lines[i] = indent + lines[i]);
737 if (!o.help && !(config.includeEnv && o.env) && !helpDefault) {
738 return;
739 }
740 var n = helpCol - line.length;
741 if (n >= 0) {
742 line += space(n);
743 } else {
744 line += '\n' + space(helpCol);
745 }
746
747 var helpEnv = '';
748 if (o.env && o.env.length && config.includeEnv) {
749 helpEnv += 'Environment: ';
750 var type = optionTypes[o.type];
751 var arg = o.helpArg || type.helpArg || 'ARG';
752 var envs = (Array.isArray(o.env) ? o.env : [o.env]).map(
753 function onE(e) {
754 if (type.takesArg) {
755 return e + '=' + arg;
756 } else {
757 return e + '=1';
758 }
759 }
760 );
761 helpEnv += envs.join(', ');
762 }
763 var help = (o.help || '').trim();
764 if (o.helpWrap !== false && config.helpWrap !== false) {
765 // Wrap help description normally.
766 if (help.length && !~'.!?"\''.indexOf(help.slice(-1))) {
767 help += '.';
768 }
769 if (help.length) {
770 help += ' ';
771 }
772 help += helpEnv;
773 if (helpDefault) {
774 if (helpEnv) {
775 help += '. ';
776 }
777 help += helpDefault;
778 }
779 line += textwrap(help, maxCol - helpCol).join(
780 '\n' + space(helpCol)
781 );
782 } else {
783 // Do not wrap help description, but indent newlines appropriately.
784 var helpLines = help.split('\n').filter(function onLine(ln) {
785 return ln.length;
786 });
787 if (helpEnv !== '') {
788 helpLines.push(helpEnv);
789 }
790 if (helpDefault) {
791 helpLines.push(helpDefault);
792 }
793 line += helpLines.join('\n' + space(helpCol));
794 }
795
796 lines[i] = line;
797 });
798
799 var rv = '';
800 if (lines.length > 0) {
801 rv = lines.join('\n') + '\n';
802 }
803 return rv;
804};
805
806/**
807 * Return a string suitable for a Bash completion file for this tool.
808 *
809 * @param args.name {String} The tool name.
810 * @param args.specExtra {String} Optional. Extra Bash code content to add
811 * to the end of the "spec". Typically this is used to append Bash
812 * "complete_TYPE" functions for custom option types. See
813 * "examples/ddcompletion.js" for an example.
814 * @param args.argtypes {Array} Optional. Array of completion types for
815 * positional args (i.e. non-options). E.g.
816 * argtypes = ['fruit', 'veggie', 'file']
817 * will result in completion of fruits for the first arg, veggies for the
818 * second, and filenames for the third and subsequent positional args.
819 * If not given, positional args will use Bash's 'default' completion.
820 * See `specExtra` for providing Bash `complete_TYPE` functions, e.g.
821 * `complete_fruit` and `complete_veggie` in this example.
822 */
823Parser.prototype.bashCompletion = function bashCompletion(args) {
824 assert.object(args, 'args');
825 assert.string(args.name, 'args.name');
826 assert.optionalString(args.specExtra, 'args.specExtra');
827 assert.optionalArrayOfString(args.argtypes, 'args.argtypes');
828
829 return bashCompletionFromOptions({
830 name: args.name,
831 specExtra: args.specExtra,
832 argtypes: args.argtypes,
833 options: this.options
834 });
835};
836
837// ---- Bash completion
838
839const BASH_COMPLETION_TEMPLATE_PATH = path.join(
840 __dirname,
841 '../etc/dashdash.bash_completion.in'
842);
843
844/**
845 * Return the Bash completion "spec" (the string value for the "{{spec}}"
846 * var in the "dashdash.bash_completion.in" template) for this tool.
847 *
848 * The "spec" is Bash code that defines the CLI options and subcmds for
849 * the template's completion code. It looks something like this:
850 *
851 * local cmd_shortopts="-J ..."
852 * local cmd_longopts="--help ..."
853 * local cmd_optargs="-p=tritonprofile ..."
854 *
855 * @param args.options {Array} The array of dashdash option specs.
856 * @param args.context {String} Optional. A context string for the "local cmd*"
857 * vars in the spec. By default it is the empty string. When used to
858 * scope for completion on a *sub-command* (e.g. for "git log" on a "git"
859 * tool), then it would have a value (e.g. "__log"). See
860 * <http://github.com/trentm/node-cmdln> Bash completion for details.
861 * @param opts.includeHidden {Boolean} Optional. Default false. By default
862 * hidden options and subcmds are "excluded". Here excluded means they
863 * won't be offered as a completion, but if used, their argument type
864 * will be completed. "Hidden" options and subcmds are ones with the
865 * `hidden: true` attribute to exclude them from default help output.
866 * @param args.argtypes {Array} Optional. Array of completion types for
867 * positional args (i.e. non-options). E.g.
868 * argtypes = ['fruit', 'veggie', 'file']
869 * will result in completion of fruits for the first arg, veggies for the
870 * second, and filenames for the third and subsequent positional args.
871 * If not given, positional args will use Bash's 'default' completion.
872 * See `specExtra` for providing Bash `complete_TYPE` functions, e.g.
873 * `complete_fruit` and `complete_veggie` in this example.
874 */
875function bashCompletionSpecFromOptions(args) {
876 assert.object(args, 'args');
877 assert.object(args.options, 'args.options');
878 assert.optionalString(args.context, 'args.context');
879 assert.optionalBool(args.includeHidden, 'args.includeHidden');
880 assert.optionalArrayOfString(args.argtypes, 'args.argtypes');
881
882 var context = args.context || '';
883 var includeHidden =
884 args.includeHidden === undefined ? false : args.includeHidden;
885
886 var spec = [];
887 var shortopts = [];
888 var longopts = [];
889 var optargs = [];
890 (args.options || []).forEach(function onOpt(o) {
891 if (o.group !== undefined && o.group !== null) {
892 // Skip group headers.
893 return;
894 }
895
896 var optNames = o.names || [o.name];
897 var optType = getOptionType(o.type);
898 if (optType.takesArg) {
899 var completionType =
900 o.completionType || optType.completionType || o.type;
901 optNames.forEach(function onOptName(optName) {
902 if (optName.length === 1) {
903 if (includeHidden || !o.hidden) {
904 shortopts.push('-' + optName);
905 }
906 // Include even hidden options in `optargs` so that bash
907 // completion of its arg still works.
908 optargs.push('-' + optName + '=' + completionType);
909 } else {
910 if (includeHidden || !o.hidden) {
911 longopts.push('--' + optName);
912 }
913 optargs.push('--' + optName + '=' + completionType);
914 }
915 });
916 } else {
917 optNames.forEach(function onOptName(optName) {
918 if (includeHidden || !o.hidden) {
919 if (optName.length === 1) {
920 shortopts.push('-' + optName);
921 } else {
922 longopts.push('--' + optName);
923 }
924 }
925 });
926 }
927 });
928
929 spec.push(
930 format(
931 'local cmd%s_shortopts="%s"',
932 context,
933 shortopts.sort().join(' ')
934 )
935 );
936 spec.push(
937 format('local cmd%s_longopts="%s"', context, longopts.sort().join(' '))
938 );
939 spec.push(
940 format('local cmd%s_optargs="%s"', context, optargs.sort().join(' '))
941 );
942 if (args.argtypes) {
943 spec.push(
944 format(
945 'local cmd%s_argtypes="%s"',
946 context,
947 args.argtypes.join(' ')
948 )
949 );
950 }
951 return spec.join('\n');
952}
953
954/**
955 * Return a string suitable for a Bash completion file for this tool.
956 *
957 * @param args.name {String} The tool name.
958 * @param args.options {Array} The array of dashdash option specs.
959 * @param args.specExtra {String} Optional. Extra Bash code content to add
960 * to the end of the "spec". Typically this is used to append Bash
961 * "complete_TYPE" functions for custom option types. See
962 * "examples/ddcompletion.js" for an example.
963 * @param args.argtypes {Array} Optional. Array of completion types for
964 * positional args (i.e. non-options). E.g.
965 * argtypes = ['fruit', 'veggie', 'file']
966 * will result in completion of fruits for the first arg, veggies for the
967 * second, and filenames for the third and subsequent positional args.
968 * If not given, positional args will use Bash's 'default' completion.
969 * See `specExtra` for providing Bash `complete_TYPE` functions, e.g.
970 * `complete_fruit` and `complete_veggie` in this example.
971 */
972function bashCompletionFromOptions(args) {
973 assert.object(args, 'args');
974 assert.object(args.options, 'args.options');
975 assert.string(args.name, 'args.name');
976 assert.optionalString(args.specExtra, 'args.specExtra');
977 assert.optionalArrayOfString(args.argtypes, 'args.argtypes');
978
979 // Gather template data.
980 var data = {
981 name: args.name,
982 date: new Date(),
983 spec: bashCompletionSpecFromOptions({
984 options: args.options,
985 argtypes: args.argtypes
986 })
987 };
988 if (args.specExtra) {
989 data.spec += '\n\n' + args.specExtra;
990 }
991
992 // Render template.
993 var template = fs.readFileSync(BASH_COMPLETION_TEMPLATE_PATH, 'utf8');
994 return renderTemplate(template, data);
995}
996
997// ---- exports
998
999function createParser(config) {
1000 return new Parser(config);
1001}
1002
1003/**
1004 * Parse argv with the given options.
1005 *
1006 * @param config {Object} A merge of all the available fields from
1007 * `dashdash.Parser` and `dashdash.Parser.parse`: options, interspersed,
1008 * argv, env, slice.
1009 */
1010function parse(config_) {
1011 assert.object(config_, 'config');
1012 assert.optionalArrayOfString(config_.argv, 'config.argv');
1013 assert.optionalObject(config_.env, 'config.env');
1014
1015 var config = shallowCopy(config_);
1016 var argv = config.argv;
1017 delete config.argv;
1018 var env = config.env;
1019 delete config.env;
1020
1021 var parser = new Parser(config);
1022 return parser.parse({argv: argv, env: env});
1023}
1024
1025/**
1026 * Add a new option type.
1027 *
1028 * @params optionType {Object}:
1029 * - name {String} Required.
1030 * - takesArg {Boolean} Required. Whether this type of option takes an
1031 * argument on process.argv. Typically this is true for all but the
1032 * "bool" type.
1033 * - helpArg {String} Required iff `takesArg === true`. The string to
1034 * show in generated help for options of this type.
1035 * - parseArg {Function} Require. `function (option, optstr, arg)` parser
1036 * that takes a string argument and returns an instance of the
1037 * appropriate type, or throws an error if the arg is invalid.
1038 * - array {Boolean} Optional. Set to true if this is an 'arrayOf' type
1039 * that collects multiple usages of the option in process.argv and
1040 * puts results in an array.
1041 * - arrayFlatten {Boolean} Optional. XXX
1042 * - default Optional. Default value for options of this type, if no
1043 * default is specified in the option type usage.
1044 */
1045function addOptionType(optionType) {
1046 assert.object(optionType, 'optionType');
1047 assert.string(optionType.name, 'optionType.name');
1048 assert.bool(optionType.takesArg, 'optionType.takesArg');
1049 if (optionType.takesArg) {
1050 assert.string(optionType.helpArg, 'optionType.helpArg');
1051 }
1052 assert.func(optionType.parseArg, 'optionType.parseArg');
1053 assert.optionalBool(optionType.array, 'optionType.array');
1054 assert.optionalBool(optionType.arrayFlatten, 'optionType.arrayFlatten');
1055
1056 optionTypes[optionType.name] = {
1057 takesArg: optionType.takesArg,
1058 helpArg: optionType.helpArg,
1059 parseArg: optionType.parseArg,
1060 array: optionType.array,
1061 arrayFlatten: optionType.arrayFlatten,
1062 default: optionType.default
1063 };
1064}
1065
1066function getOptionType(name) {
1067 assert.string(name, 'name');
1068 return optionTypes[name];
1069}
1070
1071/**
1072 * Return a synopsis string for the given option spec.
1073 *
1074 * Examples:
1075 * > synopsisFromOpt({names: ['help', 'h'], type: 'bool'});
1076 * '[ --help | -h ]'
1077 * > synopsisFromOpt({name: 'file', type: 'string', helpArg: 'FILE'});
1078 * '[ --file=FILE ]'
1079 */
1080function synopsisFromOpt(o) {
1081 assert.object(o, 'o');
1082
1083 if (Object.prototype.hasOwnProperty.call(o, 'group')) {
1084 return null;
1085 }
1086 var names = o.names || [o.name];
1087 // `type` here could be undefined if, for example, the command has a
1088 // dashdash option spec with a bogus 'type'.
1089 var type = getOptionType(o.type);
1090 var helpArg = o.helpArg || (type && type.helpArg) || 'ARG';
1091 var parts = [];
1092 names.forEach(function onName(name) {
1093 var part = (name.length === 1 ? '-' : '--') + name;
1094 if (type && type.takesArg) {
1095 part += name.length === 1 ? ' ' + helpArg : '=' + helpArg;
1096 }
1097 parts.push(part);
1098 });
1099 return '[ ' + parts.join(' | ') + ' ]';
1100}
1101
1102module.exports = {
1103 createParser: createParser,
1104 Parser: Parser,
1105 parse: parse,
1106 addOptionType: addOptionType,
1107 getOptionType: getOptionType,
1108 synopsisFromOpt: synopsisFromOpt,
1109
1110 // Bash completion-related exports
1111 BASH_COMPLETION_TEMPLATE_PATH: BASH_COMPLETION_TEMPLATE_PATH,
1112 bashCompletionFromOptions: bashCompletionFromOptions,
1113 bashCompletionSpecFromOptions: bashCompletionSpecFromOptions,
1114
1115 // Export the parseFoo parsers because they might be useful as primitives
1116 // for custom option types.
1117 parseBool: parseBool,
1118 parseString: parseString,
1119 parseNumber: parseNumber,
1120 parseInteger: parseInteger,
1121 parsePositiveInteger: parsePositiveInteger,
1122 parseDate: parseDate
1123};