usage.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.usage = void 0;
  4. // this file handles outputting usage instructions,
  5. // failures, etc. keeps logging in one place.
  6. const common_types_1 = require("./common-types");
  7. const obj_filter_1 = require("./obj-filter");
  8. const path = require("path");
  9. const yerror_1 = require("./yerror");
  10. const decamelize = require("decamelize");
  11. const setBlocking = require("set-blocking");
  12. const stringWidth = require("string-width");
  13. function usage(yargs, y18n) {
  14. const __ = y18n.__;
  15. const self = {};
  16. // methods for ouputting/building failure message.
  17. const fails = [];
  18. self.failFn = function failFn(f) {
  19. fails.push(f);
  20. };
  21. let failMessage = null;
  22. let showHelpOnFail = true;
  23. self.showHelpOnFail = function showHelpOnFailFn(arg1 = true, arg2) {
  24. function parseFunctionArgs() {
  25. return typeof arg1 === 'string' ? [true, arg1] : [arg1, arg2];
  26. }
  27. const [enabled, message] = parseFunctionArgs();
  28. failMessage = message;
  29. showHelpOnFail = enabled;
  30. return self;
  31. };
  32. let failureOutput = false;
  33. self.fail = function fail(msg, err) {
  34. const logger = yargs._getLoggerInstance();
  35. if (fails.length) {
  36. for (let i = fails.length - 1; i >= 0; --i) {
  37. fails[i](msg, err, self);
  38. }
  39. }
  40. else {
  41. if (yargs.getExitProcess())
  42. setBlocking(true);
  43. // don't output failure message more than once
  44. if (!failureOutput) {
  45. failureOutput = true;
  46. if (showHelpOnFail) {
  47. yargs.showHelp('error');
  48. logger.error();
  49. }
  50. if (msg || err)
  51. logger.error(msg || err);
  52. if (failMessage) {
  53. if (msg || err)
  54. logger.error('');
  55. logger.error(failMessage);
  56. }
  57. }
  58. err = err || new yerror_1.YError(msg);
  59. if (yargs.getExitProcess()) {
  60. return yargs.exit(1);
  61. }
  62. else if (yargs._hasParseCallback()) {
  63. return yargs.exit(1, err);
  64. }
  65. else {
  66. throw err;
  67. }
  68. }
  69. };
  70. // methods for ouputting/building help (usage) message.
  71. let usages = [];
  72. let usageDisabled = false;
  73. self.usage = (msg, description) => {
  74. if (msg === null) {
  75. usageDisabled = true;
  76. usages = [];
  77. return self;
  78. }
  79. usageDisabled = false;
  80. usages.push([msg, description || '']);
  81. return self;
  82. };
  83. self.getUsage = () => {
  84. return usages;
  85. };
  86. self.getUsageDisabled = () => {
  87. return usageDisabled;
  88. };
  89. self.getPositionalGroupName = () => {
  90. return __('Positionals:');
  91. };
  92. let examples = [];
  93. self.example = (cmd, description) => {
  94. examples.push([cmd, description || '']);
  95. };
  96. let commands = [];
  97. self.command = function command(cmd, description, isDefault, aliases, deprecated = false) {
  98. // the last default wins, so cancel out any previously set default
  99. if (isDefault) {
  100. commands = commands.map((cmdArray) => {
  101. cmdArray[2] = false;
  102. return cmdArray;
  103. });
  104. }
  105. commands.push([cmd, description || '', isDefault, aliases, deprecated]);
  106. };
  107. self.getCommands = () => commands;
  108. let descriptions = {};
  109. self.describe = function describe(keyOrKeys, desc) {
  110. if (Array.isArray(keyOrKeys)) {
  111. keyOrKeys.forEach((k) => {
  112. self.describe(k, desc);
  113. });
  114. }
  115. else if (typeof keyOrKeys === 'object') {
  116. Object.keys(keyOrKeys).forEach((k) => {
  117. self.describe(k, keyOrKeys[k]);
  118. });
  119. }
  120. else {
  121. descriptions[keyOrKeys] = desc;
  122. }
  123. };
  124. self.getDescriptions = () => descriptions;
  125. let epilogs = [];
  126. self.epilog = (msg) => {
  127. epilogs.push(msg);
  128. };
  129. let wrapSet = false;
  130. let wrap;
  131. self.wrap = (cols) => {
  132. wrapSet = true;
  133. wrap = cols;
  134. };
  135. function getWrap() {
  136. if (!wrapSet) {
  137. wrap = windowWidth();
  138. wrapSet = true;
  139. }
  140. return wrap;
  141. }
  142. const deferY18nLookupPrefix = '__yargsString__:';
  143. self.deferY18nLookup = str => deferY18nLookupPrefix + str;
  144. self.help = function help() {
  145. if (cachedHelpMessage)
  146. return cachedHelpMessage;
  147. normalizeAliases();
  148. // handle old demanded API
  149. const base$0 = yargs.customScriptName ? yargs.$0 : path.basename(yargs.$0);
  150. const demandedOptions = yargs.getDemandedOptions();
  151. const demandedCommands = yargs.getDemandedCommands();
  152. const deprecatedOptions = yargs.getDeprecatedOptions();
  153. const groups = yargs.getGroups();
  154. const options = yargs.getOptions();
  155. let keys = [];
  156. keys = keys.concat(Object.keys(descriptions));
  157. keys = keys.concat(Object.keys(demandedOptions));
  158. keys = keys.concat(Object.keys(demandedCommands));
  159. keys = keys.concat(Object.keys(options.default));
  160. keys = keys.filter(filterHiddenOptions);
  161. keys = Object.keys(keys.reduce((acc, key) => {
  162. if (key !== '_')
  163. acc[key] = true;
  164. return acc;
  165. }, {}));
  166. const theWrap = getWrap();
  167. const ui = require('cliui')({
  168. width: theWrap,
  169. wrap: !!theWrap
  170. });
  171. // the usage string.
  172. if (!usageDisabled) {
  173. if (usages.length) {
  174. // user-defined usage.
  175. usages.forEach((usage) => {
  176. ui.div(`${usage[0].replace(/\$0/g, base$0)}`);
  177. if (usage[1]) {
  178. ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] });
  179. }
  180. });
  181. ui.div();
  182. }
  183. else if (commands.length) {
  184. let u = null;
  185. // demonstrate how commands are used.
  186. if (demandedCommands._) {
  187. u = `${base$0} <${__('command')}>\n`;
  188. }
  189. else {
  190. u = `${base$0} [${__('command')}]\n`;
  191. }
  192. ui.div(`${u}`);
  193. }
  194. }
  195. // your application's commands, i.e., non-option
  196. // arguments populated in '_'.
  197. if (commands.length) {
  198. ui.div(__('Commands:'));
  199. const context = yargs.getContext();
  200. const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : '';
  201. if (yargs.getParserConfiguration()['sort-commands'] === true) {
  202. commands = commands.sort((a, b) => a[0].localeCompare(b[0]));
  203. }
  204. commands.forEach((command) => {
  205. const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}`; // drop $0 from default commands.
  206. ui.span({
  207. text: commandString,
  208. padding: [0, 2, 0, 2],
  209. width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
  210. }, { text: command[1] });
  211. const hints = [];
  212. if (command[2])
  213. hints.push(`[${__('default')}]`);
  214. if (command[3] && command[3].length) {
  215. hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`);
  216. }
  217. if (command[4]) {
  218. if (typeof command[4] === 'string') {
  219. hints.push(`[${__('deprecated: %s', command[4])}]`);
  220. }
  221. else {
  222. hints.push(`[${__('deprecated')}]`);
  223. }
  224. }
  225. if (hints.length) {
  226. ui.div({ text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right' });
  227. }
  228. else {
  229. ui.div();
  230. }
  231. });
  232. ui.div();
  233. }
  234. // perform some cleanup on the keys array, making it
  235. // only include top-level keys not their aliases.
  236. const aliasKeys = (Object.keys(options.alias) || [])
  237. .concat(Object.keys(yargs.parsed.newAliases) || []);
  238. keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1));
  239. // populate 'Options:' group with any keys that have not
  240. // explicitly had a group set.
  241. const defaultGroup = __('Options:');
  242. if (!groups[defaultGroup])
  243. groups[defaultGroup] = [];
  244. addUngroupedKeys(keys, options.alias, groups, defaultGroup);
  245. // display 'Options:' table along with any custom tables:
  246. Object.keys(groups).forEach((groupName) => {
  247. if (!groups[groupName].length)
  248. return;
  249. // if we've grouped the key 'f', but 'f' aliases 'foobar',
  250. // normalizedKeys should contain only 'foobar'.
  251. const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => {
  252. if (~aliasKeys.indexOf(key))
  253. return key;
  254. for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
  255. if (~(options.alias[aliasKey] || []).indexOf(key))
  256. return aliasKey;
  257. }
  258. return key;
  259. });
  260. if (normalizedKeys.length < 1)
  261. return;
  262. ui.div(groupName);
  263. // actually generate the switches string --foo, -f, --bar.
  264. const switches = normalizedKeys.reduce((acc, key) => {
  265. acc[key] = [key].concat(options.alias[key] || [])
  266. .map(sw => {
  267. // for the special positional group don't
  268. // add '--' or '-' prefix.
  269. if (groupName === self.getPositionalGroupName())
  270. return sw;
  271. else {
  272. return (
  273. // matches yargs-parser logic in which single-digits
  274. // aliases declared with a boolean type are now valid
  275. /^[0-9]$/.test(sw)
  276. ? ~options.boolean.indexOf(key) ? '-' : '--'
  277. : sw.length > 1 ? '--' : '-') + sw;
  278. }
  279. })
  280. .join(', ');
  281. return acc;
  282. }, {});
  283. normalizedKeys.forEach((key) => {
  284. const kswitch = switches[key];
  285. let desc = descriptions[key] || '';
  286. let type = null;
  287. if (~desc.lastIndexOf(deferY18nLookupPrefix))
  288. desc = __(desc.substring(deferY18nLookupPrefix.length));
  289. if (~options.boolean.indexOf(key))
  290. type = `[${__('boolean')}]`;
  291. if (~options.count.indexOf(key))
  292. type = `[${__('count')}]`;
  293. if (~options.string.indexOf(key))
  294. type = `[${__('string')}]`;
  295. if (~options.normalize.indexOf(key))
  296. type = `[${__('string')}]`;
  297. if (~options.array.indexOf(key))
  298. type = `[${__('array')}]`;
  299. if (~options.number.indexOf(key))
  300. type = `[${__('number')}]`;
  301. const deprecatedExtra = (deprecated) => typeof deprecated === 'string'
  302. ? `[${__('deprecated: %s', deprecated)}]`
  303. : `[${__('deprecated')}]`;
  304. const extra = [
  305. (key in deprecatedOptions) ? deprecatedExtra(deprecatedOptions[key]) : null,
  306. type,
  307. (key in demandedOptions) ? `[${__('required')}]` : null,
  308. options.choices && options.choices[key] ? `[${__('choices:')} ${self.stringifiedValues(options.choices[key])}]` : null,
  309. defaultString(options.default[key], options.defaultDescription[key])
  310. ].filter(Boolean).join(' ');
  311. ui.span({ text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4 }, desc);
  312. if (extra)
  313. ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' });
  314. else
  315. ui.div();
  316. });
  317. ui.div();
  318. });
  319. // describe some common use-cases for your application.
  320. if (examples.length) {
  321. ui.div(__('Examples:'));
  322. examples.forEach((example) => {
  323. example[0] = example[0].replace(/\$0/g, base$0);
  324. });
  325. examples.forEach((example) => {
  326. if (example[1] === '') {
  327. ui.div({
  328. text: example[0],
  329. padding: [0, 2, 0, 2]
  330. });
  331. }
  332. else {
  333. ui.div({
  334. text: example[0],
  335. padding: [0, 2, 0, 2],
  336. width: maxWidth(examples, theWrap) + 4
  337. }, {
  338. text: example[1]
  339. });
  340. }
  341. });
  342. ui.div();
  343. }
  344. // the usage string.
  345. if (epilogs.length > 0) {
  346. const e = epilogs.map(epilog => epilog.replace(/\$0/g, base$0)).join('\n');
  347. ui.div(`${e}\n`);
  348. }
  349. // Remove the trailing white spaces
  350. return ui.toString().replace(/\s*$/, '');
  351. };
  352. // return the maximum width of a string
  353. // in the left-hand column of a table.
  354. function maxWidth(table, theWrap, modifier) {
  355. let width = 0;
  356. // table might be of the form [leftColumn],
  357. // or {key: leftColumn}
  358. if (!Array.isArray(table)) {
  359. table = Object.values(table).map(v => [v]);
  360. }
  361. table.forEach((v) => {
  362. width = Math.max(stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]), width);
  363. });
  364. // if we've enabled 'wrap' we should limit
  365. // the max-width of the left-column.
  366. if (theWrap)
  367. width = Math.min(width, parseInt((theWrap * 0.5).toString(), 10));
  368. return width;
  369. }
  370. // make sure any options set for aliases,
  371. // are copied to the keys being aliased.
  372. function normalizeAliases() {
  373. // handle old demanded API
  374. const demandedOptions = yargs.getDemandedOptions();
  375. const options = yargs.getOptions();
  376. (Object.keys(options.alias) || []).forEach((key) => {
  377. options.alias[key].forEach((alias) => {
  378. // copy descriptions.
  379. if (descriptions[alias])
  380. self.describe(key, descriptions[alias]);
  381. // copy demanded.
  382. if (alias in demandedOptions)
  383. yargs.demandOption(key, demandedOptions[alias]);
  384. // type messages.
  385. if (~options.boolean.indexOf(alias))
  386. yargs.boolean(key);
  387. if (~options.count.indexOf(alias))
  388. yargs.count(key);
  389. if (~options.string.indexOf(alias))
  390. yargs.string(key);
  391. if (~options.normalize.indexOf(alias))
  392. yargs.normalize(key);
  393. if (~options.array.indexOf(alias))
  394. yargs.array(key);
  395. if (~options.number.indexOf(alias))
  396. yargs.number(key);
  397. });
  398. });
  399. }
  400. // if yargs is executing an async handler, we take a snapshot of the
  401. // help message to display on failure:
  402. let cachedHelpMessage;
  403. self.cacheHelpMessage = function () {
  404. cachedHelpMessage = this.help();
  405. };
  406. // however this snapshot must be cleared afterwards
  407. // not to be be used by next calls to parse
  408. self.clearCachedHelpMessage = function () {
  409. cachedHelpMessage = undefined;
  410. };
  411. // given a set of keys, place any keys that are
  412. // ungrouped under the 'Options:' grouping.
  413. function addUngroupedKeys(keys, aliases, groups, defaultGroup) {
  414. let groupedKeys = [];
  415. let toCheck = null;
  416. Object.keys(groups).forEach((group) => {
  417. groupedKeys = groupedKeys.concat(groups[group]);
  418. });
  419. keys.forEach((key) => {
  420. toCheck = [key].concat(aliases[key]);
  421. if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
  422. groups[defaultGroup].push(key);
  423. }
  424. });
  425. return groupedKeys;
  426. }
  427. function filterHiddenOptions(key) {
  428. return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt];
  429. }
  430. self.showHelp = (level) => {
  431. const logger = yargs._getLoggerInstance();
  432. if (!level)
  433. level = 'error';
  434. const emit = typeof level === 'function' ? level : logger[level];
  435. emit(self.help());
  436. };
  437. self.functionDescription = (fn) => {
  438. const description = fn.name ? decamelize(fn.name, '-') : __('generated-value');
  439. return ['(', description, ')'].join('');
  440. };
  441. self.stringifiedValues = function stringifiedValues(values, separator) {
  442. let string = '';
  443. const sep = separator || ', ';
  444. const array = [].concat(values);
  445. if (!values || !array.length)
  446. return string;
  447. array.forEach((value) => {
  448. if (string.length)
  449. string += sep;
  450. string += JSON.stringify(value);
  451. });
  452. return string;
  453. };
  454. // format the default-value-string displayed in
  455. // the right-hand column.
  456. function defaultString(value, defaultDescription) {
  457. let string = `[${__('default:')} `;
  458. if (value === undefined && !defaultDescription)
  459. return null;
  460. if (defaultDescription) {
  461. string += defaultDescription;
  462. }
  463. else {
  464. switch (typeof value) {
  465. case 'string':
  466. string += `"${value}"`;
  467. break;
  468. case 'object':
  469. string += JSON.stringify(value);
  470. break;
  471. default:
  472. string += value;
  473. }
  474. }
  475. return `${string}]`;
  476. }
  477. // guess the width of the console window, max-width 80.
  478. function windowWidth() {
  479. const maxWidth = 80;
  480. // CI is not a TTY
  481. /* c8 ignore next 2 */
  482. if (typeof process === 'object' && process.stdout && process.stdout.columns) {
  483. return Math.min(maxWidth, process.stdout.columns);
  484. }
  485. else {
  486. return maxWidth;
  487. }
  488. }
  489. // logic for displaying application version.
  490. let version = null;
  491. self.version = (ver) => {
  492. version = ver;
  493. };
  494. self.showVersion = () => {
  495. const logger = yargs._getLoggerInstance();
  496. logger.log(version);
  497. };
  498. self.reset = function reset(localLookup) {
  499. // do not reset wrap here
  500. // do not reset fails here
  501. failMessage = null;
  502. failureOutput = false;
  503. usages = [];
  504. usageDisabled = false;
  505. epilogs = [];
  506. examples = [];
  507. commands = [];
  508. descriptions = obj_filter_1.objFilter(descriptions, k => !localLookup[k]);
  509. return self;
  510. };
  511. const frozens = [];
  512. self.freeze = function freeze() {
  513. frozens.push({
  514. failMessage,
  515. failureOutput,
  516. usages,
  517. usageDisabled,
  518. epilogs,
  519. examples,
  520. commands,
  521. descriptions
  522. });
  523. };
  524. self.unfreeze = function unfreeze() {
  525. const frozen = frozens.pop();
  526. common_types_1.assertNotStrictEqual(frozen, undefined);
  527. ({
  528. failMessage,
  529. failureOutput,
  530. usages,
  531. usageDisabled,
  532. epilogs,
  533. examples,
  534. commands,
  535. descriptions
  536. } = frozen);
  537. };
  538. return self;
  539. }
  540. exports.usage = usage;