UNPKG

22.6 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};
223
224class Option {
225 constructor(rawName, description, config) {
226 this.rawName = rawName;
227 this.description = description;
228 this.config = Object.assign({}, config);
229 // You may use cli.option('--env.* [value]', 'desc') to denote a dot-nested option
230 rawName = rawName.replace(/\.\*/g, '');
231 this.negated = false;
232 this.names = removeBrackets(rawName)
233 .split(',')
234 .map((v) => {
235 let name = v.trim().replace(/^-{1,2}/, '');
236 if (name.startsWith('no-')) {
237 this.negated = true;
238 name = name.replace(/^no-/, '');
239 }
240 return name;
241 })
242 .sort((a, b) => (a.length > b.length ? 1 : -1)); // Sort names
243 // Use the longese name (last one) as actual option name
244 this.name = this.names[this.names.length - 1];
245 if (this.negated) {
246 this.config.default = true;
247 }
248 if (rawName.includes('<')) {
249 this.required = true;
250 }
251 else if (rawName.includes('[')) {
252 this.required = false;
253 }
254 else {
255 // No arg needed, it's boolean flag
256 this.isBoolean = true;
257 }
258 }
259}
260
261const deno = typeof window !== 'undefined' && window.Deno;
262const exit = (code) => {
263 return deno ? Deno.exit(code) : process.exit(code);
264};
265const processArgs = deno ? ['deno'].concat(Deno.args) : process.argv;
266const platformInfo = deno
267 ? `${Deno.build.os}-${Deno.build.arch} deno-${Deno.version.deno}`
268 : `${process.platform}-${process.arch} node-${process.version}`;
269
270class Command {
271 constructor(rawName, description, config = {}, cli) {
272 this.rawName = rawName;
273 this.description = description;
274 this.config = config;
275 this.cli = cli;
276 this.options = [];
277 this.aliasNames = [];
278 this.name = removeBrackets(rawName);
279 this.args = findAllBrackets(rawName);
280 this.examples = [];
281 }
282 usage(text) {
283 this.usageText = text;
284 return this;
285 }
286 allowUnknownOptions() {
287 this.config.allowUnknownOptions = true;
288 return this;
289 }
290 ignoreOptionDefaultValue() {
291 this.config.ignoreOptionDefaultValue = true;
292 return this;
293 }
294 version(version, customFlags = '-v, --version') {
295 this.versionNumber = version;
296 this.option(customFlags, 'Display version number');
297 return this;
298 }
299 example(example) {
300 this.examples.push(example);
301 return this;
302 }
303 /**
304 * Add a option for this command
305 * @param rawName Raw option name(s)
306 * @param description Option description
307 * @param config Option config
308 */
309 option(rawName, description, config) {
310 const option = new Option(rawName, description, config);
311 this.options.push(option);
312 return this;
313 }
314 alias(name) {
315 this.aliasNames.push(name);
316 return this;
317 }
318 action(callback) {
319 this.commandAction = callback;
320 return this;
321 }
322 /**
323 * Check if a command name is matched by this command
324 * @param name Command name
325 */
326 isMatched(name) {
327 return this.name === name || this.aliasNames.includes(name);
328 }
329 get isDefaultCommand() {
330 return this.name === '' || this.aliasNames.includes('!');
331 }
332 get isGlobalCommand() {
333 return this instanceof GlobalCommand;
334 }
335 /**
336 * Check if an option is registered in this command
337 * @param name Option name
338 */
339 hasOption(name) {
340 name = name.split('.')[0];
341 return this.options.find(option => {
342 return option.names.includes(name);
343 });
344 }
345 outputHelp() {
346 const { name, commands } = this.cli;
347 const { versionNumber, options: globalOptions, helpCallback } = this.cli.globalCommand;
348 const sections = [
349 {
350 body: `${name}${versionNumber ? ` v${versionNumber}` : ''}`
351 }
352 ];
353 sections.push({
354 title: 'Usage',
355 body: ` $ ${name} ${this.usageText || this.rawName}`
356 });
357 const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
358 if (showCommands) {
359 const longestCommandName = findLongest(commands.map(command => command.rawName));
360 sections.push({
361 title: 'Commands',
362 body: commands
363 .map(command => {
364 return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
365 })
366 .join('\n')
367 });
368 sections.push({
369 title: `For more info, run any command with the \`--help\` flag`,
370 body: commands
371 .map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`)
372 .join('\n')
373 });
374 }
375 const options = this.isGlobalCommand
376 ? globalOptions
377 : [...this.options, ...(globalOptions || [])];
378 if (options.length > 0) {
379 const longestOptionName = findLongest(options.map(option => option.rawName));
380 sections.push({
381 title: 'Options',
382 body: options
383 .map(option => {
384 return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined
385 ? ''
386 : `(default: ${option.config.default})`}`;
387 })
388 .join('\n')
389 });
390 }
391 if (this.examples.length > 0) {
392 sections.push({
393 title: 'Examples',
394 body: this.examples
395 .map(example => {
396 if (typeof example === 'function') {
397 return example(name);
398 }
399 return example;
400 })
401 .join('\n')
402 });
403 }
404 if (helpCallback) {
405 helpCallback(sections);
406 }
407 console.log(sections
408 .map(section => {
409 return section.title
410 ? `${section.title}:\n${section.body}`
411 : section.body;
412 })
413 .join('\n\n'));
414 exit(0);
415 }
416 outputVersion() {
417 const { name } = this.cli;
418 const { versionNumber } = this.cli.globalCommand;
419 if (versionNumber) {
420 console.log(`${name}/${versionNumber} ${platformInfo}`);
421 }
422 exit(0);
423 }
424 checkRequiredArgs() {
425 const minimalArgsCount = this.args.filter(arg => arg.required).length;
426 if (this.cli.args.length < minimalArgsCount) {
427 console.error(`error: missing required args for command \`${this.rawName}\``);
428 exit(1);
429 }
430 }
431 /**
432 * Check if the parsed options contain any unknown options
433 *
434 * Exit and output error when true
435 */
436 checkUnknownOptions() {
437 const { rawOptions, globalCommand } = this.cli;
438 if (!this.config.allowUnknownOptions) {
439 for (const name of Object.keys(rawOptions)) {
440 if (name !== '--' &&
441 !this.hasOption(name) &&
442 !globalCommand.hasOption(name)) {
443 console.error(`error: Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
444 exit(1);
445 }
446 }
447 }
448 }
449 /**
450 * Check if the required string-type options exist
451 */
452 checkOptionValue() {
453 const { rawOptions, globalCommand } = this.cli;
454 const options = [...globalCommand.options, ...this.options];
455 for (const option of options) {
456 const value = rawOptions[option.name.split('.')[0]];
457 // Check required option value
458 if (option.required) {
459 const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
460 if (value === true || (value === false && !hasNegated)) {
461 console.error(`error: option \`${option.rawName}\` value is missing`);
462 exit(1);
463 }
464 }
465 }
466 }
467}
468class GlobalCommand extends Command {
469 constructor(cli) {
470 super('@@global@@', '', {}, cli);
471 }
472}
473
474class CAC extends events.EventEmitter {
475 /**
476 * @param name The program name to display in help and version message
477 */
478 constructor(name = '') {
479 super();
480 this.name = name;
481 this.commands = [];
482 this.globalCommand = new GlobalCommand(this);
483 this.globalCommand.usage('<command> [options]');
484 }
485 /**
486 * Add a global usage text.
487 *
488 * This is not used by sub-commands.
489 */
490 usage(text) {
491 this.globalCommand.usage(text);
492 return this;
493 }
494 /**
495 * Add a sub-command
496 */
497 command(rawName, description, config) {
498 const command = new Command(rawName, description || '', config, this);
499 command.globalCommand = this.globalCommand;
500 this.commands.push(command);
501 return command;
502 }
503 /**
504 * Add a global CLI option.
505 *
506 * Which is also applied to sub-commands.
507 */
508 option(rawName, description, config) {
509 this.globalCommand.option(rawName, description, config);
510 return this;
511 }
512 /**
513 * Show help message when `-h, --help` flags appear.
514 *
515 */
516 help(callback) {
517 this.globalCommand.option('-h, --help', 'Display this message');
518 this.globalCommand.helpCallback = callback;
519 this.showHelpOnExit = true;
520 return this;
521 }
522 /**
523 * Show version number when `-v, --version` flags appear.
524 *
525 */
526 version(version, customFlags = '-v, --version') {
527 this.globalCommand.version(version, customFlags);
528 this.showVersionOnExit = true;
529 return this;
530 }
531 /**
532 * Add a global example.
533 *
534 * This example added here will not be used by sub-commands.
535 */
536 example(example) {
537 this.globalCommand.example(example);
538 return this;
539 }
540 /**
541 * Output the corresponding help message
542 * When a sub-command is matched, output the help message for the command
543 * Otherwise output the global one.
544 *
545 * This will also call `process.exit(0)` to quit the process.
546 */
547 outputHelp() {
548 if (this.matchedCommand) {
549 this.matchedCommand.outputHelp();
550 }
551 else {
552 this.globalCommand.outputHelp();
553 }
554 }
555 /**
556 * Output the version number.
557 *
558 * This will also call `process.exit(0)` to quit the process.
559 */
560 outputVersion() {
561 this.globalCommand.outputVersion();
562 }
563 setParsedInfo({ args, options, rawOptions }, matchedCommand, matchedCommandName) {
564 this.args = args;
565 this.options = options;
566 this.rawOptions = rawOptions;
567 if (matchedCommand) {
568 this.matchedCommand = matchedCommand;
569 }
570 if (matchedCommandName) {
571 this.matchedCommandName = matchedCommandName;
572 }
573 return this;
574 }
575 /**
576 * Parse argv
577 */
578 parse(argv = processArgs, {
579 /** Whether to run the action for matched command */
580 run = true } = {}) {
581 this.rawArgs = argv;
582 if (!this.name) {
583 this.name = argv[1] ? getFileName(argv[1]) : 'cli';
584 }
585 let shouldParse = true;
586 // Search sub-commands
587 for (const command of this.commands) {
588 const mriResult = this.mri(argv.slice(2), command);
589 const commandName = mriResult.args[0];
590 if (command.isMatched(commandName)) {
591 shouldParse = false;
592 const parsedInfo = Object.assign({}, mriResult, { args: mriResult.args.slice(1) });
593 this.setParsedInfo(parsedInfo, command, commandName);
594 this.emit(`command:${commandName}`, command);
595 }
596 }
597 if (shouldParse) {
598 // Search the default command
599 for (const command of this.commands) {
600 if (command.name === '') {
601 shouldParse = false;
602 const mriResult = this.mri(argv.slice(2), command);
603 this.setParsedInfo(mriResult, command);
604 this.emit(`command:!`, command);
605 }
606 }
607 }
608 if (shouldParse) {
609 const mriResult = this.mri(argv.slice(2));
610 this.setParsedInfo(mriResult);
611 }
612 if (this.options.help && this.showHelpOnExit) {
613 this.outputHelp();
614 }
615 if (this.options.version && this.showVersionOnExit) {
616 this.outputVersion();
617 }
618 const parsedArgv = { args: this.args, options: this.options };
619 if (run) {
620 this.runMatchedCommand();
621 }
622 if (!this.matchedCommand && this.args[0]) {
623 this.emit('command:*');
624 }
625 return parsedArgv;
626 }
627 mri(argv,
628 /** Matched command */ command) {
629 // All added options
630 const cliOptions = [
631 ...this.globalCommand.options,
632 ...(command ? command.options : [])
633 ];
634 const mriOptions = getMriOptions(cliOptions);
635 // Extract everything after `--` since mri doesn't support it
636 let argsAfterDoubleDashes = [];
637 const doubleDashesIndex = argv.indexOf('--');
638 if (doubleDashesIndex > -1) {
639 argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
640 argv = argv.slice(0, doubleDashesIndex);
641 }
642 const parsed = lib(argv, mriOptions);
643 const args = parsed._;
644 delete parsed._;
645 const options = {
646 '--': argsAfterDoubleDashes
647 };
648 // Set option default value
649 const ignoreDefault = command && command.config.ignoreOptionDefaultValue
650 ? command.config.ignoreOptionDefaultValue
651 : this.globalCommand.config.ignoreOptionDefaultValue;
652 let transforms = Object.create(null);
653 for (const cliOption of cliOptions) {
654 if (!ignoreDefault && cliOption.config.default !== undefined) {
655 for (const name of cliOption.names) {
656 options[name] = cliOption.config.default;
657 }
658 }
659 // If options type is defined
660 if (Array.isArray(cliOption.config.type)) {
661 if (transforms[cliOption.name] === undefined) {
662 transforms[cliOption.name] = Object.create(null);
663 transforms[cliOption.name]['shouldTransform'] = true;
664 transforms[cliOption.name]['transformFunction'] =
665 cliOption.config.type[0];
666 }
667 }
668 }
669 // Camelcase option names and set dot nested option values
670 for (const key of Object.keys(parsed)) {
671 const keys = key.split('.').map((v, i) => {
672 return i === 0 ? camelcase(v) : v;
673 });
674 setDotProp(options, keys, parsed[key]);
675 setByType(options, transforms);
676 }
677 return {
678 args,
679 options,
680 rawOptions: parsed
681 };
682 }
683 runMatchedCommand() {
684 const { args, options, matchedCommand: command } = this;
685 if (!command || !command.commandAction)
686 return;
687 command.checkUnknownOptions();
688 command.checkOptionValue();
689 command.checkRequiredArgs();
690 const actionArgs = [];
691 command.args.forEach((arg, index) => {
692 if (arg.variadic) {
693 actionArgs.push(args.slice(index));
694 }
695 else {
696 actionArgs.push(args[index]);
697 }
698 });
699 actionArgs.push(options);
700 return command.commandAction.apply(this, actionArgs);
701 }
702}
703
704/**
705 * @param name The program name to display in help and version message
706 */
707const cac = (name = '') => new CAC(name);
708if (typeof module !== 'undefined') {
709 module.exports = cac;
710 module.exports.default = cac;
711 module.exports.cac = cac;
712}
713
714exports.cac = cac;
715exports.default = cac;