index.mjs 18 KB

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