UNPKG

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