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 let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
355 if (!this.isGlobalCommand && !this.isDefaultCommand) {
356 options = options.filter((option) => option.name !== "version");
357 }
358 if (options.length > 0) {
359 const longestOptionName = findLongest(options.map((option) => option.rawName));
360 sections.push({
361 title: "Options",
362 body: options.map((option) => {
363 return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === void 0 ? "" : `(default: ${option.config.default})`}`;
364 }).join("\n")
365 });
366 }
367 if (this.examples.length > 0) {
368 sections.push({
369 title: "Examples",
370 body: this.examples.map((example) => {
371 if (typeof example === "function") {
372 return example(name);
373 }
374 return example;
375 }).join("\n")
376 });
377 }
378 if (helpCallback) {
379 sections = helpCallback(sections) || sections;
380 }
381 console.log(sections.map((section) => {
382 return section.title ? `${section.title}:
383${section.body}` : section.body;
384 }).join("\n\n"));
385 }
386 outputVersion() {
387 const {name} = this.cli;
388 const {versionNumber} = this.cli.globalCommand;
389 if (versionNumber) {
390 console.log(`${name}/${versionNumber} ${platformInfo}`);
391 }
392 }
393 checkRequiredArgs() {
394 const minimalArgsCount = this.args.filter((arg) => arg.required).length;
395 if (this.cli.args.length < minimalArgsCount) {
396 throw new CACError(`missing required args for command \`${this.rawName}\``);
397 }
398 }
399 checkUnknownOptions() {
400 const {options, globalCommand} = this.cli;
401 if (!this.config.allowUnknownOptions) {
402 for (const name of Object.keys(options)) {
403 if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
404 throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
405 }
406 }
407 }
408 }
409 checkOptionValue() {
410 const {options: parsedOptions, globalCommand} = this.cli;
411 const options = [...globalCommand.options, ...this.options];
412 for (const option of options) {
413 const value = parsedOptions[option.name.split(".")[0]];
414 if (option.required) {
415 const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
416 if (value === true || value === false && !hasNegated) {
417 throw new CACError(`option \`${option.rawName}\` value is missing`);
418 }
419 }
420 }
421 }
422}
423class GlobalCommand extends Command {
424 constructor(cli) {
425 super("@@global@@", "", {}, cli);
426 }
427}
428
429var __assign = Object.assign;
430class CAC extends events.EventEmitter {
431 constructor(name = "") {
432 super();
433 this.name = name;
434 this.commands = [];
435 this.rawArgs = [];
436 this.args = [];
437 this.options = {};
438 this.globalCommand = new GlobalCommand(this);
439 this.globalCommand.usage("<command> [options]");
440 }
441 usage(text) {
442 this.globalCommand.usage(text);
443 return this;
444 }
445 command(rawName, description, config) {
446 const command = new Command(rawName, description || "", config, this);
447 command.globalCommand = this.globalCommand;
448 this.commands.push(command);
449 return command;
450 }
451 option(rawName, description, config) {
452 this.globalCommand.option(rawName, description, config);
453 return this;
454 }
455 help(callback) {
456 this.globalCommand.option("-h, --help", "Display this message");
457 this.globalCommand.helpCallback = callback;
458 this.showHelpOnExit = true;
459 return this;
460 }
461 version(version, customFlags = "-v, --version") {
462 this.globalCommand.version(version, customFlags);
463 this.showVersionOnExit = true;
464 return this;
465 }
466 example(example) {
467 this.globalCommand.example(example);
468 return this;
469 }
470 outputHelp() {
471 if (this.matchedCommand) {
472 this.matchedCommand.outputHelp();
473 } else {
474 this.globalCommand.outputHelp();
475 }
476 }
477 outputVersion() {
478 this.globalCommand.outputVersion();
479 }
480 setParsedInfo({args, options}, matchedCommand, matchedCommandName) {
481 this.args = args;
482 this.options = options;
483 if (matchedCommand) {
484 this.matchedCommand = matchedCommand;
485 }
486 if (matchedCommandName) {
487 this.matchedCommandName = matchedCommandName;
488 }
489 return this;
490 }
491 unsetMatchedCommand() {
492 this.matchedCommand = void 0;
493 this.matchedCommandName = void 0;
494 }
495 parse(argv = processArgs, {
496 run = true
497 } = {}) {
498 this.rawArgs = argv;
499 if (!this.name) {
500 this.name = argv[1] ? getFileName(argv[1]) : "cli";
501 }
502 let shouldParse = true;
503 for (const command of this.commands) {
504 const parsed = this.mri(argv.slice(2), command);
505 const commandName = parsed.args[0];
506 if (command.isMatched(commandName)) {
507 shouldParse = false;
508 const parsedInfo = __assign(__assign({}, parsed), {
509 args: parsed.args.slice(1)
510 });
511 this.setParsedInfo(parsedInfo, command, commandName);
512 this.emit(`command:${commandName}`, command);
513 }
514 }
515 if (shouldParse) {
516 for (const command of this.commands) {
517 if (command.name === "") {
518 shouldParse = false;
519 const parsed = this.mri(argv.slice(2), command);
520 this.setParsedInfo(parsed, command);
521 this.emit(`command:!`, command);
522 }
523 }
524 }
525 if (shouldParse) {
526 const parsed = this.mri(argv.slice(2));
527 this.setParsedInfo(parsed);
528 }
529 if (this.options.help && this.showHelpOnExit) {
530 this.outputHelp();
531 run = false;
532 this.unsetMatchedCommand();
533 }
534 if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
535 this.outputVersion();
536 run = false;
537 this.unsetMatchedCommand();
538 }
539 const parsedArgv = {args: this.args, options: this.options};
540 if (run) {
541 this.runMatchedCommand();
542 }
543 if (!this.matchedCommand && this.args[0]) {
544 this.emit("command:*");
545 }
546 return parsedArgv;
547 }
548 mri(argv, command) {
549 const cliOptions = [
550 ...this.globalCommand.options,
551 ...command ? command.options : []
552 ];
553 const mriOptions = getMriOptions(cliOptions);
554 let argsAfterDoubleDashes = [];
555 const doubleDashesIndex = argv.indexOf("--");
556 if (doubleDashesIndex > -1) {
557 argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
558 argv = argv.slice(0, doubleDashesIndex);
559 }
560 let parsed = mri2(argv, mriOptions);
561 parsed = Object.keys(parsed).reduce((res, name) => {
562 return __assign(__assign({}, res), {
563 [camelcaseOptionName(name)]: parsed[name]
564 });
565 }, {_: []});
566 const args = parsed._;
567 const options = {
568 "--": argsAfterDoubleDashes
569 };
570 const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
571 let transforms = Object.create(null);
572 for (const cliOption of cliOptions) {
573 if (!ignoreDefault && cliOption.config.default !== void 0) {
574 for (const name of cliOption.names) {
575 options[name] = cliOption.config.default;
576 }
577 }
578 if (Array.isArray(cliOption.config.type)) {
579 if (transforms[cliOption.name] === void 0) {
580 transforms[cliOption.name] = Object.create(null);
581 transforms[cliOption.name]["shouldTransform"] = true;
582 transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
583 }
584 }
585 }
586 for (const key of Object.keys(parsed)) {
587 if (key !== "_") {
588 const keys = key.split(".");
589 setDotProp(options, keys, parsed[key]);
590 setByType(options, transforms);
591 }
592 }
593 return {
594 args,
595 options
596 };
597 }
598 runMatchedCommand() {
599 const {args, options, matchedCommand: command} = this;
600 if (!command || !command.commandAction)
601 return;
602 command.checkUnknownOptions();
603 command.checkOptionValue();
604 command.checkRequiredArgs();
605 const actionArgs = [];
606 command.args.forEach((arg, index) => {
607 if (arg.variadic) {
608 actionArgs.push(args.slice(index));
609 } else {
610 actionArgs.push(args[index]);
611 }
612 });
613 actionArgs.push(options);
614 return command.commandAction.apply(this, actionArgs);
615 }
616}
617
618const cac = (name = "") => new CAC(name);
619
620exports.CAC = CAC;
621exports.Command = Command;
622exports.cac = cac;
623exports.default = cac;