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