UNPKG

23.2 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var events = require('events');
6
7function toArr(any) {
8 return any == null ? [] : Array.isArray(any) ? any : [any];
9}
10
11function toVal(out, key, val, opts) {
12 var x, old=out[key], nxt=(
13 !!~opts.string.indexOf(key) ? (val == null || val === true ? '' : String(val))
14 : typeof val === 'boolean' ? val
15 : !!~opts.boolean.indexOf(key) ? (val === 'false' ? false : val === 'true' || (out._.push((x = +val,x * 0 === 0) ? x : val),!!val))
16 : (x = +val,x * 0 === 0) ? x : val
17 );
18 out[key] = old == null ? nxt : (Array.isArray(old) ? old.concat(nxt) : [old, nxt]);
19}
20
21var lib = function (args, opts) {
22 args = args || [];
23 opts = opts || {};
24
25 var k, arr, arg, name, val, out={ _:[] };
26 var i=0, j=0, idx=0, len=args.length;
27
28 const alibi = opts.alias !== void 0;
29 const strict = opts.unknown !== void 0;
30 const defaults = opts.default !== void 0;
31
32 opts.alias = opts.alias || {};
33 opts.string = toArr(opts.string);
34 opts.boolean = toArr(opts.boolean);
35
36 if (alibi) {
37 for (k in opts.alias) {
38 arr = opts.alias[k] = toArr(opts.alias[k]);
39 for (i=0; i < arr.length; i++) {
40 (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
41 }
42 }
43 }
44
45 opts.boolean.forEach(key => {
46 opts.boolean = opts.boolean.concat(opts.alias[key] = opts.alias[key] || []);
47 });
48
49 opts.string.forEach(key => {
50 opts.string = opts.string.concat(opts.alias[key] = opts.alias[key] || []);
51 });
52
53 if (defaults) {
54 for (k in opts.default) {
55 opts.alias[k] = opts.alias[k] || [];
56 (opts[typeof opts.default[k]] || []).push(k);
57 }
58 }
59
60 const keys = strict ? Object.keys(opts.alias) : [];
61
62 for (i=0; i < len; i++) {
63 arg = args[i];
64
65 if (arg === '--') {
66 out._ = out._.concat(args.slice(++i));
67 break;
68 }
69
70 for (j=0; j < arg.length; j++) {
71 if (arg.charCodeAt(j) !== 45) break; // "-"
72 }
73
74 if (j === 0) {
75 out._.push(arg);
76 } else if (arg.substring(j, j + 3) === 'no-') {
77 name = arg.substring(j + 3);
78 if (strict && !~keys.indexOf(name)) {
79 return opts.unknown(arg);
80 }
81 out[name] = false;
82 } else {
83 for (idx=j+1; idx < arg.length; idx++) {
84 if (arg.charCodeAt(idx) === 61) break; // "="
85 }
86
87 name = arg.substring(j, idx);
88 val = arg.substring(++idx) || (i+1 === len || (''+args[i+1]).charCodeAt(0) === 45 || args[++i]);
89 arr = (j === 2 ? [name] : name);
90
91 for (idx=0; idx < arr.length; idx++) {
92 name = arr[idx];
93 if (strict && !~keys.indexOf(name)) return opts.unknown('-'.repeat(j) + name);
94 toVal(out, name, (idx + 1 < arr.length) || val, opts);
95 }
96 }
97 }
98
99 if (defaults) {
100 for (k in opts.default) {
101 if (out[k] === void 0) {
102 out[k] = opts.default[k];
103 }
104 }
105 }
106
107 if (alibi) {
108 for (k in out) {
109 arr = opts.alias[k] || [];
110 while (arr.length > 0) {
111 out[arr.shift()] = out[k];
112 }
113 }
114 }
115
116 return out;
117};
118
119const removeBrackets = (v) => v.replace(/[<[].+/, '').trim();
120const findAllBrackets = (v) => {
121 const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
122 const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
123 const res = [];
124 const parse = (match) => {
125 let variadic = false;
126 let value = match[1];
127 if (value.startsWith('...')) {
128 value = value.slice(3);
129 variadic = true;
130 }
131 return {
132 required: match[0].startsWith('<'),
133 value,
134 variadic
135 };
136 };
137 let angledMatch;
138 while ((angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v))) {
139 res.push(parse(angledMatch));
140 }
141 let squareMatch;
142 while ((squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v))) {
143 res.push(parse(squareMatch));
144 }
145 return res;
146};
147const getMriOptions = (options) => {
148 const result = { alias: {}, boolean: [] };
149 for (const [index, option] of options.entries()) {
150 // We do not set default values in mri options
151 // Since its type (typeof) will be used to cast parsed arguments.
152 // Which mean `--foo foo` will be parsed as `{foo: true}` if we have `{default:{foo: true}}`
153 // Set alias
154 if (option.names.length > 1) {
155 result.alias[option.names[0]] = option.names.slice(1);
156 }
157 // Set boolean
158 if (option.isBoolean) {
159 if (option.negated) {
160 // For negated option
161 // We only set it to `boolean` type when there's no string-type option with the same name
162 const hasStringTypeOption = options.some((o, i) => {
163 return (i !== index &&
164 o.names.some(name => option.names.includes(name)) &&
165 typeof o.required === 'boolean');
166 });
167 if (!hasStringTypeOption) {
168 result.boolean.push(option.names[0]);
169 }
170 }
171 else {
172 result.boolean.push(option.names[0]);
173 }
174 }
175 }
176 return result;
177};
178const findLongest = (arr) => {
179 return arr.sort((a, b) => {
180 return a.length > b.length ? -1 : 1;
181 })[0];
182};
183const padRight = (str, length) => {
184 return str.length >= length ? str : `${str}${' '.repeat(length - str.length)}`;
185};
186const camelcase = (input) => {
187 return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
188 return p1 + p2.toUpperCase();
189 });
190};
191const setDotProp = (obj, keys, val) => {
192 let i = 0;
193 let length = keys.length;
194 let t = obj;
195 let x;
196 for (; i < length; ++i) {
197 x = t[keys[i]];
198 t = t[keys[i]] =
199 i === length - 1
200 ? val
201 : x != null
202 ? x
203 : !!~keys[i + 1].indexOf('.') || !(+keys[i + 1] > -1)
204 ? {}
205 : [];
206 }
207};
208const setByType = (obj, transforms) => {
209 for (const key of Object.keys(transforms)) {
210 const transform = transforms[key];
211 if (transform.shouldTransform) {
212 obj[key] = Array.prototype.concat.call([], obj[key]);
213 if (typeof transform.transformFunction === 'function') {
214 obj[key] = obj[key].map(transform.transformFunction);
215 }
216 }
217 }
218};
219const getFileName = (input) => {
220 const m = /([^\\\/]+)$/.exec(input);
221 return m ? m[1] : '';
222};
223const camelcaseOptionName = (name) => {
224 // Camelcase the option name
225 // Don't camelcase anything after the dot `.`
226 return name
227 .split('.')
228 .map((v, i) => {
229 return i === 0 ? camelcase(v) : v;
230 })
231 .join('.');
232};
233class CACError extends Error {
234 constructor(message) {
235 super(message);
236 this.name = this.constructor.name;
237 if (typeof Error.captureStackTrace === 'function') {
238 Error.captureStackTrace(this, this.constructor);
239 }
240 else {
241 this.stack = new Error(message).stack;
242 }
243 }
244}
245
246class Option {
247 constructor(rawName, description, config) {
248 this.rawName = rawName;
249 this.description = description;
250 this.config = Object.assign({}, config);
251 // You may use cli.option('--env.* [value]', 'desc') to denote a dot-nested option
252 rawName = rawName.replace(/\.\*/g, '');
253 this.negated = false;
254 this.names = removeBrackets(rawName)
255 .split(',')
256 .map((v) => {
257 let name = v.trim().replace(/^-{1,2}/, '');
258 if (name.startsWith('no-')) {
259 this.negated = true;
260 name = name.replace(/^no-/, '');
261 }
262 return camelcaseOptionName(name);
263 })
264 .sort((a, b) => (a.length > b.length ? 1 : -1)); // Sort names
265 // Use the longest name (last one) as actual option name
266 this.name = this.names[this.names.length - 1];
267 if (this.negated) {
268 this.config.default = true;
269 }
270 if (rawName.includes('<')) {
271 this.required = true;
272 }
273 else if (rawName.includes('[')) {
274 this.required = false;
275 }
276 else {
277 // No arg needed, it's boolean flag
278 this.isBoolean = true;
279 }
280 }
281}
282
283const deno = typeof window !== 'undefined' && window.Deno;
284const denoScriptPath = deno && typeof window !== 'undefined' && window.location.pathname;
285// Adds deno executable and script path to processArgs as "compatibility" layer for node
286// See https://github.com/cacjs/cac/issues/69
287const processArgs = deno
288 ? ['deno', denoScriptPath].concat(Deno.args)
289 : process.argv;
290const platformInfo = deno
291 ? `${Deno.build.os}-${Deno.build.arch} deno-${Deno.version.deno}`
292 : `${process.platform}-${process.arch} node-${process.version}`;
293
294class Command {
295 constructor(rawName, description, config = {}, cli) {
296 this.rawName = rawName;
297 this.description = description;
298 this.config = config;
299 this.cli = cli;
300 this.options = [];
301 this.aliasNames = [];
302 this.name = removeBrackets(rawName);
303 this.args = findAllBrackets(rawName);
304 this.examples = [];
305 }
306 usage(text) {
307 this.usageText = text;
308 return this;
309 }
310 allowUnknownOptions() {
311 this.config.allowUnknownOptions = true;
312 return this;
313 }
314 ignoreOptionDefaultValue() {
315 this.config.ignoreOptionDefaultValue = true;
316 return this;
317 }
318 version(version, customFlags = '-v, --version') {
319 this.versionNumber = version;
320 this.option(customFlags, 'Display version number');
321 return this;
322 }
323 example(example) {
324 this.examples.push(example);
325 return this;
326 }
327 /**
328 * Add a option for this command
329 * @param rawName Raw option name(s)
330 * @param description Option description
331 * @param config Option config
332 */
333 option(rawName, description, config) {
334 const option = new Option(rawName, description, config);
335 this.options.push(option);
336 return this;
337 }
338 alias(name) {
339 this.aliasNames.push(name);
340 return this;
341 }
342 action(callback) {
343 this.commandAction = callback;
344 return this;
345 }
346 /**
347 * Check if a command name is matched by this command
348 * @param name Command name
349 */
350 isMatched(name) {
351 return this.name === name || this.aliasNames.includes(name);
352 }
353 get isDefaultCommand() {
354 return this.name === '' || this.aliasNames.includes('!');
355 }
356 get isGlobalCommand() {
357 return this instanceof GlobalCommand;
358 }
359 /**
360 * Check if an option is registered in this command
361 * @param name Option name
362 */
363 hasOption(name) {
364 name = name.split('.')[0];
365 return this.options.find(option => {
366 return option.names.includes(name);
367 });
368 }
369 outputHelp() {
370 const { name, commands } = this.cli;
371 const { versionNumber, options: globalOptions, helpCallback } = this.cli.globalCommand;
372 const sections = [
373 {
374 body: `${name}${versionNumber ? ` v${versionNumber}` : ''}`
375 }
376 ];
377 sections.push({
378 title: 'Usage',
379 body: ` $ ${name} ${this.usageText || this.rawName}`
380 });
381 const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
382 if (showCommands) {
383 const longestCommandName = findLongest(commands.map(command => command.rawName));
384 sections.push({
385 title: 'Commands',
386 body: commands
387 .map(command => {
388 return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
389 })
390 .join('\n')
391 });
392 sections.push({
393 title: `For more info, run any command with the \`--help\` flag`,
394 body: commands
395 .map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`)
396 .join('\n')
397 });
398 }
399 const options = this.isGlobalCommand
400 ? globalOptions
401 : [...this.options, ...(globalOptions || [])];
402 if (options.length > 0) {
403 const longestOptionName = findLongest(options.map(option => option.rawName));
404 sections.push({
405 title: 'Options',
406 body: options
407 .map(option => {
408 return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined
409 ? ''
410 : `(default: ${option.config.default})`}`;
411 })
412 .join('\n')
413 });
414 }
415 if (this.examples.length > 0) {
416 sections.push({
417 title: 'Examples',
418 body: this.examples
419 .map(example => {
420 if (typeof example === 'function') {
421 return example(name);
422 }
423 return example;
424 })
425 .join('\n')
426 });
427 }
428 if (helpCallback) {
429 helpCallback(sections);
430 }
431 console.log(sections
432 .map(section => {
433 return section.title
434 ? `${section.title}:\n${section.body}`
435 : section.body;
436 })
437 .join('\n\n'));
438 }
439 outputVersion() {
440 const { name } = this.cli;
441 const { versionNumber } = this.cli.globalCommand;
442 if (versionNumber) {
443 console.log(`${name}/${versionNumber} ${platformInfo}`);
444 }
445 }
446 checkRequiredArgs() {
447 const minimalArgsCount = this.args.filter(arg => arg.required).length;
448 if (this.cli.args.length < minimalArgsCount) {
449 throw new CACError(`missing required args for command \`${this.rawName}\``);
450 }
451 }
452 /**
453 * Check if the parsed options contain any unknown options
454 *
455 * Exit and output error when true
456 */
457 checkUnknownOptions() {
458 const { options, globalCommand } = this.cli;
459 if (!this.config.allowUnknownOptions) {
460 for (const name of Object.keys(options)) {
461 if (name !== '--' &&
462 !this.hasOption(name) &&
463 !globalCommand.hasOption(name)) {
464 throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
465 }
466 }
467 }
468 }
469 /**
470 * Check if the required string-type options exist
471 */
472 checkOptionValue() {
473 const { options: parsedOptions, globalCommand } = this.cli;
474 const options = [...globalCommand.options, ...this.options];
475 for (const option of options) {
476 const value = parsedOptions[option.name.split('.')[0]];
477 // Check required option value
478 if (option.required) {
479 const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
480 if (value === true || (value === false && !hasNegated)) {
481 throw new CACError(`option \`${option.rawName}\` value is missing`);
482 }
483 }
484 }
485 }
486}
487class GlobalCommand extends Command {
488 constructor(cli) {
489 super('@@global@@', '', {}, cli);
490 }
491}
492
493class CAC extends events.EventEmitter {
494 /**
495 * @param name The program name to display in help and version message
496 */
497 constructor(name = '') {
498 super();
499 this.name = name;
500 this.commands = [];
501 this.globalCommand = new GlobalCommand(this);
502 this.globalCommand.usage('<command> [options]');
503 }
504 /**
505 * Add a global usage text.
506 *
507 * This is not used by sub-commands.
508 */
509 usage(text) {
510 this.globalCommand.usage(text);
511 return this;
512 }
513 /**
514 * Add a sub-command
515 */
516 command(rawName, description, config) {
517 const command = new Command(rawName, description || '', config, this);
518 command.globalCommand = this.globalCommand;
519 this.commands.push(command);
520 return command;
521 }
522 /**
523 * Add a global CLI option.
524 *
525 * Which is also applied to sub-commands.
526 */
527 option(rawName, description, config) {
528 this.globalCommand.option(rawName, description, config);
529 return this;
530 }
531 /**
532 * Show help message when `-h, --help` flags appear.
533 *
534 */
535 help(callback) {
536 this.globalCommand.option('-h, --help', 'Display this message');
537 this.globalCommand.helpCallback = callback;
538 this.showHelpOnExit = true;
539 return this;
540 }
541 /**
542 * Show version number when `-v, --version` flags appear.
543 *
544 */
545 version(version, customFlags = '-v, --version') {
546 this.globalCommand.version(version, customFlags);
547 this.showVersionOnExit = true;
548 return this;
549 }
550 /**
551 * Add a global example.
552 *
553 * This example added here will not be used by sub-commands.
554 */
555 example(example) {
556 this.globalCommand.example(example);
557 return this;
558 }
559 /**
560 * Output the corresponding help message
561 * When a sub-command is matched, output the help message for the command
562 * Otherwise output the global one.
563 *
564 */
565 outputHelp() {
566 if (this.matchedCommand) {
567 this.matchedCommand.outputHelp();
568 }
569 else {
570 this.globalCommand.outputHelp();
571 }
572 }
573 /**
574 * Output the version number.
575 *
576 */
577 outputVersion() {
578 this.globalCommand.outputVersion();
579 }
580 setParsedInfo({ args, options }, matchedCommand, matchedCommandName) {
581 this.args = args;
582 this.options = options;
583 if (matchedCommand) {
584 this.matchedCommand = matchedCommand;
585 }
586 if (matchedCommandName) {
587 this.matchedCommandName = matchedCommandName;
588 }
589 return this;
590 }
591 /**
592 * Parse argv
593 */
594 parse(argv = processArgs, {
595 /** Whether to run the action for matched command */
596 run = true } = {}) {
597 this.rawArgs = argv;
598 if (!this.name) {
599 this.name = argv[1] ? getFileName(argv[1]) : 'cli';
600 }
601 let shouldParse = true;
602 // Search sub-commands
603 for (const command of this.commands) {
604 const parsed = this.mri(argv.slice(2), command);
605 const commandName = parsed.args[0];
606 if (command.isMatched(commandName)) {
607 shouldParse = false;
608 const parsedInfo = Object.assign({}, parsed, { args: parsed.args.slice(1) });
609 this.setParsedInfo(parsedInfo, command, commandName);
610 this.emit(`command:${commandName}`, command);
611 }
612 }
613 if (shouldParse) {
614 // Search the default command
615 for (const command of this.commands) {
616 if (command.name === '') {
617 shouldParse = false;
618 const parsed = this.mri(argv.slice(2), command);
619 this.setParsedInfo(parsed, command);
620 this.emit(`command:!`, command);
621 }
622 }
623 }
624 if (shouldParse) {
625 const parsed = this.mri(argv.slice(2));
626 this.setParsedInfo(parsed);
627 }
628 if (this.options.help && this.showHelpOnExit) {
629 this.outputHelp();
630 run = false;
631 }
632 if (this.options.version && this.showVersionOnExit) {
633 this.outputVersion();
634 run = false;
635 }
636 const parsedArgv = { args: this.args, options: this.options };
637 if (run) {
638 this.runMatchedCommand();
639 }
640 if (!this.matchedCommand && this.args[0]) {
641 this.emit('command:*');
642 }
643 return parsedArgv;
644 }
645 mri(argv,
646 /** Matched command */ command) {
647 // All added options
648 const cliOptions = [
649 ...this.globalCommand.options,
650 ...(command ? command.options : [])
651 ];
652 const mriOptions = getMriOptions(cliOptions);
653 // Extract everything after `--` since mri doesn't support it
654 let argsAfterDoubleDashes = [];
655 const doubleDashesIndex = argv.indexOf('--');
656 if (doubleDashesIndex > -1) {
657 argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
658 argv = argv.slice(0, doubleDashesIndex);
659 }
660 let parsed = lib(argv, mriOptions);
661 parsed = Object.keys(parsed).reduce((res, name) => {
662 return Object.assign({}, res, { [camelcaseOptionName(name)]: parsed[name] });
663 }, { _: [] });
664 const args = parsed._;
665 delete parsed._;
666 const options = {
667 '--': argsAfterDoubleDashes
668 };
669 // Set option default value
670 const ignoreDefault = command && command.config.ignoreOptionDefaultValue
671 ? command.config.ignoreOptionDefaultValue
672 : this.globalCommand.config.ignoreOptionDefaultValue;
673 let transforms = Object.create(null);
674 for (const cliOption of cliOptions) {
675 if (!ignoreDefault && cliOption.config.default !== undefined) {
676 for (const name of cliOption.names) {
677 options[name] = cliOption.config.default;
678 }
679 }
680 // If options type is defined
681 if (Array.isArray(cliOption.config.type)) {
682 if (transforms[cliOption.name] === undefined) {
683 transforms[cliOption.name] = Object.create(null);
684 transforms[cliOption.name]['shouldTransform'] = true;
685 transforms[cliOption.name]['transformFunction'] =
686 cliOption.config.type[0];
687 }
688 }
689 }
690 // Set dot nested option values
691 for (const key of Object.keys(parsed)) {
692 const keys = key.split('.');
693 setDotProp(options, keys, parsed[key]);
694 setByType(options, transforms);
695 }
696 return {
697 args,
698 options
699 };
700 }
701 runMatchedCommand() {
702 const { args, options, matchedCommand: command } = this;
703 if (!command || !command.commandAction)
704 return;
705 command.checkUnknownOptions();
706 command.checkOptionValue();
707 command.checkRequiredArgs();
708 const actionArgs = [];
709 command.args.forEach((arg, index) => {
710 if (arg.variadic) {
711 actionArgs.push(args.slice(index));
712 }
713 else {
714 actionArgs.push(args[index]);
715 }
716 });
717 actionArgs.push(options);
718 return command.commandAction.apply(this, actionArgs);
719 }
720}
721
722/**
723 * @param name The program name to display in help and version message
724 */
725const cac = (name = '') => new CAC(name);
726if (typeof module !== 'undefined') {
727 module.exports = cac;
728 module.exports.default = cac;
729 module.exports.cac = cac;
730}
731
732exports.cac = cac;
733exports.default = cac;