UNPKG

18.1 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
21function mri2 (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 for (i=opts.boolean.length; i-- > 0;) {
46 arr = opts.alias[opts.boolean[i]] || [];
47 for (j=arr.length; j-- > 0;) opts.boolean.push(arr[j]);
48 }
49
50 for (i=opts.string.length; i-- > 0;) {
51 arr = opts.alias[opts.string[i]] || [];
52 for (j=arr.length; j-- > 0;) opts.string.push(arr[j]);
53 }
54
55 if (defaults) {
56 for (k in opts.default) {
57 name = typeof opts.default[k];
58 arr = opts.alias[k] = opts.alias[k] || [];
59 if (opts[name] !== void 0) {
60 opts[name].push(k);
61 for (i=0; i < arr.length; i++) {
62 opts[name].push(arr[i]);
63 }
64 }
65 }
66 }
67
68 const keys = strict ? Object.keys(opts.alias) : [];
69
70 for (i=0; i < len; i++) {
71 arg = args[i];
72
73 if (arg === '--') {
74 out._ = out._.concat(args.slice(++i));
75 break;
76 }
77
78 for (j=0; j < arg.length; j++) {
79 if (arg.charCodeAt(j) !== 45) break; // "-"
80 }
81
82 if (j === 0) {
83 out._.push(arg);
84 } else if (arg.substring(j, j + 3) === 'no-') {
85 name = arg.substring(j + 3);
86 if (strict && !~keys.indexOf(name)) {
87 return opts.unknown(arg);
88 }
89 out[name] = false;
90 } else {
91 for (idx=j+1; idx < arg.length; idx++) {
92 if (arg.charCodeAt(idx) === 61) break; // "="
93 }
94
95 name = arg.substring(j, idx);
96 val = arg.substring(++idx) || (i+1 === len || (''+args[i+1]).charCodeAt(0) === 45 || args[++i]);
97 arr = (j === 2 ? [name] : name);
98
99 for (idx=0; idx < arr.length; idx++) {
100 name = arr[idx];
101 if (strict && !~keys.indexOf(name)) return opts.unknown('-'.repeat(j) + name);
102 toVal(out, name, (idx + 1 < arr.length) || val, opts);
103 }
104 }
105 }
106
107 if (defaults) {
108 for (k in opts.default) {
109 if (out[k] === void 0) {
110 out[k] = opts.default[k];
111 }
112 }
113 }
114
115 if (alibi) {
116 for (k in out) {
117 arr = opts.alias[k] || [];
118 while (arr.length > 0) {
119 out[arr.shift()] = out[k];
120 }
121 }
122 }
123
124 return out;
125}
126
127const removeBrackets = (v) => v.replace(/[<[].+/, "").trim();
128const findAllBrackets = (v) => {
129 const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
130 const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
131 const res = [];
132 const parse = (match) => {
133 let variadic = false;
134 let value = match[1];
135 if (value.startsWith("...")) {
136 value = value.slice(3);
137 variadic = true;
138 }
139 return {
140 required: match[0].startsWith("<"),
141 value,
142 variadic
143 };
144 };
145 let angledMatch;
146 while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {
147 res.push(parse(angledMatch));
148 }
149 let squareMatch;
150 while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {
151 res.push(parse(squareMatch));
152 }
153 return res;
154};
155const getMriOptions = (options) => {
156 const result = {alias: {}, boolean: []};
157 for (const [index, option] of options.entries()) {
158 if (option.names.length > 1) {
159 result.alias[option.names[0]] = option.names.slice(1);
160 }
161 if (option.isBoolean) {
162 if (option.negated) {
163 const hasStringTypeOption = options.some((o, i) => {
164 return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === "boolean";
165 });
166 if (!hasStringTypeOption) {
167 result.boolean.push(option.names[0]);
168 }
169 } else {
170 result.boolean.push(option.names[0]);
171 }
172 }
173 }
174 return result;
175};
176const findLongest = (arr) => {
177 return arr.sort((a, b) => {
178 return a.length > b.length ? -1 : 1;
179 })[0];
180};
181const padRight = (str, length) => {
182 return str.length >= length ? str : `${str}${" ".repeat(length - str.length)}`;
183};
184const camelcase = (input) => {
185 return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
186 return p1 + p2.toUpperCase();
187 });
188};
189const setDotProp = (obj, keys, val) => {
190 let i = 0;
191 let length = keys.length;
192 let t = obj;
193 let x;
194 for (; i < length; ++i) {
195 x = t[keys[i]];
196 t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(".") || !(+keys[i + 1] > -1) ? {} : [];
197 }
198};
199const setByType = (obj, transforms) => {
200 for (const key of Object.keys(transforms)) {
201 const transform = transforms[key];
202 if (transform.shouldTransform) {
203 obj[key] = Array.prototype.concat.call([], obj[key]);
204 if (typeof transform.transformFunction === "function") {
205 obj[key] = obj[key].map(transform.transformFunction);
206 }
207 }
208 }
209};
210const getFileName = (input) => {
211 const m = /([^\\\/]+)$/.exec(input);
212 return m ? m[1] : "";
213};
214const camelcaseOptionName = (name) => {
215 return name.split(".").map((v, i) => {
216 return i === 0 ? camelcase(v) : v;
217 }).join(".");
218};
219class CACError extends Error {
220 constructor(message) {
221 super(message);
222 this.name = this.constructor.name;
223 if (typeof Error.captureStackTrace === "function") {
224 Error.captureStackTrace(this, this.constructor);
225 } else {
226 this.stack = new Error(message).stack;
227 }
228 }
229}
230
231class Option {
232 constructor(rawName, description, config) {
233 this.rawName = rawName;
234 this.description = description;
235 this.config = Object.assign({}, config);
236 rawName = rawName.replace(/\.\*/g, "");
237 this.negated = false;
238 this.names = removeBrackets(rawName).split(",").map((v) => {
239 let name = v.trim().replace(/^-{1,2}/, "");
240 if (name.startsWith("no-")) {
241 this.negated = true;
242 name = name.replace(/^no-/, "");
243 }
244 return camelcaseOptionName(name);
245 }).sort((a, b) => a.length > b.length ? 1 : -1);
246 this.name = this.names[this.names.length - 1];
247 if (this.negated && this.config.default == null) {
248 this.config.default = true;
249 }
250 if (rawName.includes("<")) {
251 this.required = true;
252 } else if (rawName.includes("[")) {
253 this.required = false;
254 } else {
255 this.isBoolean = true;
256 }
257 }
258}
259
260const processArgs = process.argv;
261const platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
262
263class Command {
264 constructor(rawName, description, config = {}, cli) {
265 this.rawName = rawName;
266 this.description = description;
267 this.config = config;
268 this.cli = cli;
269 this.options = [];
270 this.aliasNames = [];
271 this.name = removeBrackets(rawName);
272 this.args = findAllBrackets(rawName);
273 this.examples = [];
274 }
275 usage(text) {
276 this.usageText = text;
277 return this;
278 }
279 allowUnknownOptions() {
280 this.config.allowUnknownOptions = true;
281 return this;
282 }
283 ignoreOptionDefaultValue() {
284 this.config.ignoreOptionDefaultValue = true;
285 return this;
286 }
287 version(version, customFlags = "-v, --version") {
288 this.versionNumber = version;
289 this.option(customFlags, "Display version number");
290 return this;
291 }
292 example(example) {
293 this.examples.push(example);
294 return this;
295 }
296 option(rawName, description, config) {
297 const option = new Option(rawName, description, config);
298 this.options.push(option);
299 return this;
300 }
301 alias(name) {
302 this.aliasNames.push(name);
303 return this;
304 }
305 action(callback) {
306 this.commandAction = callback;
307 return this;
308 }
309 isMatched(name) {
310 return this.name === name || this.aliasNames.includes(name);
311 }
312 get isDefaultCommand() {
313 return this.name === "" || this.aliasNames.includes("!");
314 }
315 get isGlobalCommand() {
316 return this instanceof GlobalCommand;
317 }
318 hasOption(name) {
319 name = name.split(".")[0];
320 return this.options.find((option) => {
321 return option.names.includes(name);
322 });
323 }
324 outputHelp() {
325 const {name, commands} = this.cli;
326 const {
327 versionNumber,
328 options: globalOptions,
329 helpCallback
330 } = this.cli.globalCommand;
331 let sections = [
332 {
333 body: `${name}${versionNumber ? `/${versionNumber}` : ""}`
334 }
335 ];
336 sections.push({
337 title: "Usage",
338 body: ` $ ${name} ${this.usageText || this.rawName}`
339 });
340 const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
341 if (showCommands) {
342 const longestCommandName = findLongest(commands.map((command) => command.rawName));
343 sections.push({
344 title: "Commands",
345 body: commands.map((command) => {
346 return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
347 }).join("\n")
348 });
349 sections.push({
350 title: `For more info, run any command with the \`--help\` flag`,
351 body: commands.map((command) => ` $ ${name}${command.name === "" ? "" : ` ${command.name}`} --help`).join("\n")
352 });
353 }
354 const options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
355 if (options.length > 0) {
356 const longestOptionName = findLongest(options.map((option) => option.rawName));
357 sections.push({
358 title: "Options",
359 body: options.map((option) => {
360 return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === void 0 ? "" : `(default: ${option.config.default})`}`;
361 }).join("\n")
362 });
363 }
364 if (this.examples.length > 0) {
365 sections.push({
366 title: "Examples",
367 body: this.examples.map((example) => {
368 if (typeof example === "function") {
369 return example(name);
370 }
371 return example;
372 }).join("\n")
373 });
374 }
375 if (helpCallback) {
376 sections = helpCallback(sections) || sections;
377 }
378 console.log(sections.map((section) => {
379 return section.title ? `${section.title}:
380${section.body}` : section.body;
381 }).join("\n\n"));
382 }
383 outputVersion() {
384 const {name} = this.cli;
385 const {versionNumber} = this.cli.globalCommand;
386 if (versionNumber) {
387 console.log(`${name}/${versionNumber} ${platformInfo}`);
388 }
389 }
390 checkRequiredArgs() {
391 const minimalArgsCount = this.args.filter((arg) => arg.required).length;
392 if (this.cli.args.length < minimalArgsCount) {
393 throw new CACError(`missing required args for command \`${this.rawName}\``);
394 }
395 }
396 checkUnknownOptions() {
397 const {options, globalCommand} = this.cli;
398 if (!this.config.allowUnknownOptions) {
399 for (const name of Object.keys(options)) {
400 if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
401 throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
402 }
403 }
404 }
405 }
406 checkOptionValue() {
407 const {options: parsedOptions, globalCommand} = this.cli;
408 const options = [...globalCommand.options, ...this.options];
409 for (const option of options) {
410 const value = parsedOptions[option.name.split(".")[0]];
411 if (option.required) {
412 const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
413 if (value === true || value === false && !hasNegated) {
414 throw new CACError(`option \`${option.rawName}\` value is missing`);
415 }
416 }
417 }
418 }
419}
420class GlobalCommand extends Command {
421 constructor(cli) {
422 super("@@global@@", "", {}, cli);
423 }
424}
425
426var __assign = Object.assign;
427class CAC extends events.EventEmitter {
428 constructor(name = "") {
429 super();
430 this.name = name;
431 this.commands = [];
432 this.rawArgs = [];
433 this.args = [];
434 this.options = {};
435 this.globalCommand = new GlobalCommand(this);
436 this.globalCommand.usage("<command> [options]");
437 }
438 usage(text) {
439 this.globalCommand.usage(text);
440 return this;
441 }
442 command(rawName, description, config) {
443 const command = new Command(rawName, description || "", config, this);
444 command.globalCommand = this.globalCommand;
445 this.commands.push(command);
446 return command;
447 }
448 option(rawName, description, config) {
449 this.globalCommand.option(rawName, description, config);
450 return this;
451 }
452 help(callback) {
453 this.globalCommand.option("-h, --help", "Display this message");
454 this.globalCommand.helpCallback = callback;
455 this.showHelpOnExit = true;
456 return this;
457 }
458 version(version, customFlags = "-v, --version") {
459 this.globalCommand.version(version, customFlags);
460 this.showVersionOnExit = true;
461 return this;
462 }
463 example(example) {
464 this.globalCommand.example(example);
465 return this;
466 }
467 outputHelp() {
468 if (this.matchedCommand) {
469 this.matchedCommand.outputHelp();
470 } else {
471 this.globalCommand.outputHelp();
472 }
473 }
474 outputVersion() {
475 this.globalCommand.outputVersion();
476 }
477 setParsedInfo({args, options}, matchedCommand, matchedCommandName) {
478 this.args = args;
479 this.options = options;
480 if (matchedCommand) {
481 this.matchedCommand = matchedCommand;
482 }
483 if (matchedCommandName) {
484 this.matchedCommandName = matchedCommandName;
485 }
486 return this;
487 }
488 unsetMatchedCommand() {
489 this.matchedCommand = void 0;
490 this.matchedCommandName = void 0;
491 }
492 parse(argv = processArgs, {
493 run = true
494 } = {}) {
495 this.rawArgs = argv;
496 if (!this.name) {
497 this.name = argv[1] ? getFileName(argv[1]) : "cli";
498 }
499 let shouldParse = true;
500 for (const command of this.commands) {
501 const parsed = this.mri(argv.slice(2), command);
502 const commandName = parsed.args[0];
503 if (command.isMatched(commandName)) {
504 shouldParse = false;
505 const parsedInfo = __assign(__assign({}, parsed), {
506 args: parsed.args.slice(1)
507 });
508 this.setParsedInfo(parsedInfo, command, commandName);
509 this.emit(`command:${commandName}`, command);
510 }
511 }
512 if (shouldParse) {
513 for (const command of this.commands) {
514 if (command.name === "") {
515 shouldParse = false;
516 const parsed = this.mri(argv.slice(2), command);
517 this.setParsedInfo(parsed, command);
518 this.emit(`command:!`, command);
519 }
520 }
521 }
522 if (shouldParse) {
523 const parsed = this.mri(argv.slice(2));
524 this.setParsedInfo(parsed);
525 }
526 if (this.options.help && this.showHelpOnExit) {
527 this.outputHelp();
528 run = false;
529 this.unsetMatchedCommand();
530 }
531 if (this.options.version && this.showVersionOnExit) {
532 this.outputVersion();
533 run = false;
534 this.unsetMatchedCommand();
535 }
536 const parsedArgv = {args: this.args, options: this.options};
537 if (run) {
538 this.runMatchedCommand();
539 }
540 if (!this.matchedCommand && this.args[0]) {
541 this.emit("command:*");
542 }
543 return parsedArgv;
544 }
545 mri(argv, command) {
546 const cliOptions = [
547 ...this.globalCommand.options,
548 ...command ? command.options : []
549 ];
550 const mriOptions = getMriOptions(cliOptions);
551 let argsAfterDoubleDashes = [];
552 const doubleDashesIndex = argv.indexOf("--");
553 if (doubleDashesIndex > -1) {
554 argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
555 argv = argv.slice(0, doubleDashesIndex);
556 }
557 let parsed = mri2(argv, mriOptions);
558 parsed = Object.keys(parsed).reduce((res, name) => {
559 return __assign(__assign({}, res), {
560 [camelcaseOptionName(name)]: parsed[name]
561 });
562 }, {_: []});
563 const args = parsed._;
564 const options = {
565 "--": argsAfterDoubleDashes
566 };
567 const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
568 let transforms = Object.create(null);
569 for (const cliOption of cliOptions) {
570 if (!ignoreDefault && cliOption.config.default !== void 0) {
571 for (const name of cliOption.names) {
572 options[name] = cliOption.config.default;
573 }
574 }
575 if (Array.isArray(cliOption.config.type)) {
576 if (transforms[cliOption.name] === void 0) {
577 transforms[cliOption.name] = Object.create(null);
578 transforms[cliOption.name]["shouldTransform"] = true;
579 transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
580 }
581 }
582 }
583 for (const key of Object.keys(parsed)) {
584 if (key !== "_") {
585 const keys = key.split(".");
586 setDotProp(options, keys, parsed[key]);
587 setByType(options, transforms);
588 }
589 }
590 return {
591 args,
592 options
593 };
594 }
595 runMatchedCommand() {
596 const {args, options, matchedCommand: command} = this;
597 if (!command || !command.commandAction)
598 return;
599 command.checkUnknownOptions();
600 command.checkOptionValue();
601 command.checkRequiredArgs();
602 const actionArgs = [];
603 command.args.forEach((arg, index) => {
604 if (arg.variadic) {
605 actionArgs.push(args.slice(index));
606 } else {
607 actionArgs.push(args[index]);
608 }
609 });
610 actionArgs.push(options);
611 return command.commandAction.apply(this, actionArgs);
612 }
613}
614
615const cac = (name = "") => new CAC(name);
616if (typeof module !== "undefined") {
617 module.exports = cac;
618 Object.assign(module.exports, {
619 default: cac,
620 cac,
621 CAC: CAC,
622 Command: Command
623 });
624}
625
626exports.CAC = CAC;
627exports.Command = Command;
628exports.cac = cac;
629exports.default = cac;