usage.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. import { objFilter } from './utils/obj-filter.js';
  2. import { YError } from './yerror.js';
  3. import setBlocking from './utils/set-blocking.js';
  4. function isBoolean(fail) {
  5. return typeof fail === 'boolean';
  6. }
  7. export function usage(yargs, shim) {
  8. const __ = shim.y18n.__;
  9. const self = {};
  10. const fails = [];
  11. self.failFn = function failFn(f) {
  12. fails.push(f);
  13. };
  14. let failMessage = null;
  15. let showHelpOnFail = true;
  16. self.showHelpOnFail = function showHelpOnFailFn(arg1 = true, arg2) {
  17. function parseFunctionArgs() {
  18. return typeof arg1 === 'string' ? [true, arg1] : [arg1, arg2];
  19. }
  20. const [enabled, message] = parseFunctionArgs();
  21. failMessage = message;
  22. showHelpOnFail = enabled;
  23. return self;
  24. };
  25. let failureOutput = false;
  26. self.fail = function fail(msg, err) {
  27. const logger = yargs.getInternalMethods().getLoggerInstance();
  28. if (fails.length) {
  29. for (let i = fails.length - 1; i >= 0; --i) {
  30. const fail = fails[i];
  31. if (isBoolean(fail)) {
  32. if (err)
  33. throw err;
  34. else if (msg)
  35. throw Error(msg);
  36. }
  37. else {
  38. fail(msg, err, self);
  39. }
  40. }
  41. }
  42. else {
  43. if (yargs.getExitProcess())
  44. setBlocking(true);
  45. if (!failureOutput) {
  46. failureOutput = true;
  47. if (showHelpOnFail) {
  48. yargs.showHelp('error');
  49. logger.error();
  50. }
  51. if (msg || err)
  52. logger.error(msg || err);
  53. if (failMessage) {
  54. if (msg || err)
  55. logger.error('');
  56. logger.error(failMessage);
  57. }
  58. }
  59. err = err || new YError(msg);
  60. if (yargs.getExitProcess()) {
  61. return yargs.exit(1);
  62. }
  63. else if (yargs.getInternalMethods().hasParseCallback()) {
  64. return yargs.exit(1, err);
  65. }
  66. else {
  67. throw err;
  68. }
  69. }
  70. };
  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. if (isDefault) {
  99. commands = commands.map(cmdArray => {
  100. cmdArray[2] = false;
  101. return cmdArray;
  102. });
  103. }
  104. commands.push([cmd, description || '', isDefault, aliases, deprecated]);
  105. };
  106. self.getCommands = () => commands;
  107. let descriptions = {};
  108. self.describe = function describe(keyOrKeys, desc) {
  109. if (Array.isArray(keyOrKeys)) {
  110. keyOrKeys.forEach(k => {
  111. self.describe(k, desc);
  112. });
  113. }
  114. else if (typeof keyOrKeys === 'object') {
  115. Object.keys(keyOrKeys).forEach(k => {
  116. self.describe(k, keyOrKeys[k]);
  117. });
  118. }
  119. else {
  120. descriptions[keyOrKeys] = desc;
  121. }
  122. };
  123. self.getDescriptions = () => descriptions;
  124. let epilogs = [];
  125. self.epilog = msg => {
  126. epilogs.push(msg);
  127. };
  128. let wrapSet = false;
  129. let wrap;
  130. self.wrap = cols => {
  131. wrapSet = true;
  132. wrap = cols;
  133. };
  134. function getWrap() {
  135. if (!wrapSet) {
  136. wrap = windowWidth();
  137. wrapSet = true;
  138. }
  139. return wrap;
  140. }
  141. const deferY18nLookupPrefix = '__yargsString__:';
  142. self.deferY18nLookup = str => deferY18nLookupPrefix + str;
  143. self.help = function help() {
  144. if (cachedHelpMessage)
  145. return cachedHelpMessage;
  146. normalizeAliases();
  147. const base$0 = yargs.customScriptName
  148. ? yargs.$0
  149. : shim.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 = shim.cliui({
  168. width: theWrap,
  169. wrap: !!theWrap,
  170. });
  171. if (!usageDisabled) {
  172. if (usages.length) {
  173. usages.forEach(usage => {
  174. ui.div({ text: `${usage[0].replace(/\$0/g, base$0)}` });
  175. if (usage[1]) {
  176. ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] });
  177. }
  178. });
  179. ui.div();
  180. }
  181. else if (commands.length) {
  182. let u = null;
  183. if (demandedCommands._) {
  184. u = `${base$0} <${__('command')}>\n`;
  185. }
  186. else {
  187. u = `${base$0} [${__('command')}]\n`;
  188. }
  189. ui.div(`${u}`);
  190. }
  191. }
  192. if (commands.length > 1 || (commands.length === 1 && !commands[0][2])) {
  193. ui.div(__('Commands:'));
  194. const context = yargs.getInternalMethods().getContext();
  195. const parentCommands = context.commands.length
  196. ? `${context.commands.join(' ')} `
  197. : '';
  198. if (yargs.getInternalMethods().getParserConfiguration()['sort-commands'] ===
  199. true) {
  200. commands = commands.sort((a, b) => a[0].localeCompare(b[0]));
  201. }
  202. commands.forEach(command => {
  203. const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}`;
  204. ui.span({
  205. text: commandString,
  206. padding: [0, 2, 0, 2],
  207. width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4,
  208. }, { text: command[1] });
  209. const hints = [];
  210. if (command[2])
  211. hints.push(`[${__('default')}]`);
  212. if (command[3] && command[3].length) {
  213. hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`);
  214. }
  215. if (command[4]) {
  216. if (typeof command[4] === 'string') {
  217. hints.push(`[${__('deprecated: %s', command[4])}]`);
  218. }
  219. else {
  220. hints.push(`[${__('deprecated')}]`);
  221. }
  222. }
  223. if (hints.length) {
  224. ui.div({
  225. text: hints.join(' '),
  226. padding: [0, 0, 0, 2],
  227. align: 'right',
  228. });
  229. }
  230. else {
  231. ui.div();
  232. }
  233. });
  234. ui.div();
  235. }
  236. const aliasKeys = (Object.keys(options.alias) || []).concat(Object.keys(yargs.parsed.newAliases) || []);
  237. keys = keys.filter(key => !yargs.parsed.newAliases[key] &&
  238. aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1));
  239. const defaultGroup = __('Options:');
  240. if (!groups[defaultGroup])
  241. groups[defaultGroup] = [];
  242. addUngroupedKeys(keys, options.alias, groups, defaultGroup);
  243. const isLongSwitch = (sw) => /^--/.test(getText(sw));
  244. const displayedGroups = Object.keys(groups)
  245. .filter(groupName => groups[groupName].length > 0)
  246. .map(groupName => {
  247. const normalizedKeys = groups[groupName]
  248. .filter(filterHiddenOptions)
  249. .map(key => {
  250. if (aliasKeys.includes(key))
  251. return key;
  252. for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
  253. if ((options.alias[aliasKey] || []).includes(key))
  254. return aliasKey;
  255. }
  256. return key;
  257. });
  258. return { groupName, normalizedKeys };
  259. })
  260. .filter(({ normalizedKeys }) => normalizedKeys.length > 0)
  261. .map(({ groupName, normalizedKeys }) => {
  262. const switches = normalizedKeys.reduce((acc, key) => {
  263. acc[key] = [key]
  264. .concat(options.alias[key] || [])
  265. .map(sw => {
  266. if (groupName === self.getPositionalGroupName())
  267. return sw;
  268. else {
  269. return ((/^[0-9]$/.test(sw)
  270. ? options.boolean.includes(key)
  271. ? '-'
  272. : '--'
  273. : sw.length > 1
  274. ? '--'
  275. : '-') + sw);
  276. }
  277. })
  278. .sort((sw1, sw2) => isLongSwitch(sw1) === isLongSwitch(sw2)
  279. ? 0
  280. : isLongSwitch(sw1)
  281. ? 1
  282. : -1)
  283. .join(', ');
  284. return acc;
  285. }, {});
  286. return { groupName, normalizedKeys, switches };
  287. });
  288. const shortSwitchesUsed = displayedGroups
  289. .filter(({ groupName }) => groupName !== self.getPositionalGroupName())
  290. .some(({ normalizedKeys, switches }) => !normalizedKeys.every(key => isLongSwitch(switches[key])));
  291. if (shortSwitchesUsed) {
  292. displayedGroups
  293. .filter(({ groupName }) => groupName !== self.getPositionalGroupName())
  294. .forEach(({ normalizedKeys, switches }) => {
  295. normalizedKeys.forEach(key => {
  296. if (isLongSwitch(switches[key])) {
  297. switches[key] = addIndentation(switches[key], '-x, '.length);
  298. }
  299. });
  300. });
  301. }
  302. displayedGroups.forEach(({ groupName, normalizedKeys, switches }) => {
  303. ui.div(groupName);
  304. normalizedKeys.forEach(key => {
  305. const kswitch = switches[key];
  306. let desc = descriptions[key] || '';
  307. let type = null;
  308. if (desc.includes(deferY18nLookupPrefix))
  309. desc = __(desc.substring(deferY18nLookupPrefix.length));
  310. if (options.boolean.includes(key))
  311. type = `[${__('boolean')}]`;
  312. if (options.count.includes(key))
  313. type = `[${__('count')}]`;
  314. if (options.string.includes(key))
  315. type = `[${__('string')}]`;
  316. if (options.normalize.includes(key))
  317. type = `[${__('string')}]`;
  318. if (options.array.includes(key))
  319. type = `[${__('array')}]`;
  320. if (options.number.includes(key))
  321. type = `[${__('number')}]`;
  322. const deprecatedExtra = (deprecated) => typeof deprecated === 'string'
  323. ? `[${__('deprecated: %s', deprecated)}]`
  324. : `[${__('deprecated')}]`;
  325. const extra = [
  326. key in deprecatedOptions
  327. ? deprecatedExtra(deprecatedOptions[key])
  328. : null,
  329. type,
  330. key in demandedOptions ? `[${__('required')}]` : null,
  331. options.choices && options.choices[key]
  332. ? `[${__('choices:')} ${self.stringifiedValues(options.choices[key])}]`
  333. : null,
  334. defaultString(options.default[key], options.defaultDescription[key]),
  335. ]
  336. .filter(Boolean)
  337. .join(' ');
  338. ui.span({
  339. text: getText(kswitch),
  340. padding: [0, 2, 0, 2 + getIndentation(kswitch)],
  341. width: maxWidth(switches, theWrap) + 4,
  342. }, desc);
  343. if (extra)
  344. ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' });
  345. else
  346. ui.div();
  347. });
  348. ui.div();
  349. });
  350. if (examples.length) {
  351. ui.div(__('Examples:'));
  352. examples.forEach(example => {
  353. example[0] = example[0].replace(/\$0/g, base$0);
  354. });
  355. examples.forEach(example => {
  356. if (example[1] === '') {
  357. ui.div({
  358. text: example[0],
  359. padding: [0, 2, 0, 2],
  360. });
  361. }
  362. else {
  363. ui.div({
  364. text: example[0],
  365. padding: [0, 2, 0, 2],
  366. width: maxWidth(examples, theWrap) + 4,
  367. }, {
  368. text: example[1],
  369. });
  370. }
  371. });
  372. ui.div();
  373. }
  374. if (epilogs.length > 0) {
  375. const e = epilogs
  376. .map(epilog => epilog.replace(/\$0/g, base$0))
  377. .join('\n');
  378. ui.div(`${e}\n`);
  379. }
  380. return ui.toString().replace(/\s*$/, '');
  381. };
  382. function maxWidth(table, theWrap, modifier) {
  383. let width = 0;
  384. if (!Array.isArray(table)) {
  385. table = Object.values(table).map(v => [v]);
  386. }
  387. table.forEach(v => {
  388. width = Math.max(shim.stringWidth(modifier ? `${modifier} ${getText(v[0])}` : getText(v[0])) + getIndentation(v[0]), width);
  389. });
  390. if (theWrap)
  391. width = Math.min(width, parseInt((theWrap * 0.5).toString(), 10));
  392. return width;
  393. }
  394. function normalizeAliases() {
  395. const demandedOptions = yargs.getDemandedOptions();
  396. const options = yargs.getOptions();
  397. (Object.keys(options.alias) || []).forEach(key => {
  398. options.alias[key].forEach(alias => {
  399. if (descriptions[alias])
  400. self.describe(key, descriptions[alias]);
  401. if (alias in demandedOptions)
  402. yargs.demandOption(key, demandedOptions[alias]);
  403. if (options.boolean.includes(alias))
  404. yargs.boolean(key);
  405. if (options.count.includes(alias))
  406. yargs.count(key);
  407. if (options.string.includes(alias))
  408. yargs.string(key);
  409. if (options.normalize.includes(alias))
  410. yargs.normalize(key);
  411. if (options.array.includes(alias))
  412. yargs.array(key);
  413. if (options.number.includes(alias))
  414. yargs.number(key);
  415. });
  416. });
  417. }
  418. let cachedHelpMessage;
  419. self.cacheHelpMessage = function () {
  420. cachedHelpMessage = this.help();
  421. };
  422. self.clearCachedHelpMessage = function () {
  423. cachedHelpMessage = undefined;
  424. };
  425. self.hasCachedHelpMessage = function () {
  426. return !!cachedHelpMessage;
  427. };
  428. function addUngroupedKeys(keys, aliases, groups, defaultGroup) {
  429. let groupedKeys = [];
  430. let toCheck = null;
  431. Object.keys(groups).forEach(group => {
  432. groupedKeys = groupedKeys.concat(groups[group]);
  433. });
  434. keys.forEach(key => {
  435. toCheck = [key].concat(aliases[key]);
  436. if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
  437. groups[defaultGroup].push(key);
  438. }
  439. });
  440. return groupedKeys;
  441. }
  442. function filterHiddenOptions(key) {
  443. return (yargs.getOptions().hiddenOptions.indexOf(key) < 0 ||
  444. yargs.parsed.argv[yargs.getOptions().showHiddenOpt]);
  445. }
  446. self.showHelp = (level) => {
  447. const logger = yargs.getInternalMethods().getLoggerInstance();
  448. if (!level)
  449. level = 'error';
  450. const emit = typeof level === 'function' ? level : logger[level];
  451. emit(self.help());
  452. };
  453. self.functionDescription = fn => {
  454. const description = fn.name
  455. ? shim.Parser.decamelize(fn.name, '-')
  456. : __('generated-value');
  457. return ['(', description, ')'].join('');
  458. };
  459. self.stringifiedValues = function stringifiedValues(values, separator) {
  460. let string = '';
  461. const sep = separator || ', ';
  462. const array = [].concat(values);
  463. if (!values || !array.length)
  464. return string;
  465. array.forEach(value => {
  466. if (string.length)
  467. string += sep;
  468. string += JSON.stringify(value);
  469. });
  470. return string;
  471. };
  472. function defaultString(value, defaultDescription) {
  473. let string = `[${__('default:')} `;
  474. if (value === undefined && !defaultDescription)
  475. return null;
  476. if (defaultDescription) {
  477. string += defaultDescription;
  478. }
  479. else {
  480. switch (typeof value) {
  481. case 'string':
  482. string += `"${value}"`;
  483. break;
  484. case 'object':
  485. string += JSON.stringify(value);
  486. break;
  487. default:
  488. string += value;
  489. }
  490. }
  491. return `${string}]`;
  492. }
  493. function windowWidth() {
  494. const maxWidth = 80;
  495. if (shim.process.stdColumns) {
  496. return Math.min(maxWidth, shim.process.stdColumns);
  497. }
  498. else {
  499. return maxWidth;
  500. }
  501. }
  502. let version = null;
  503. self.version = ver => {
  504. version = ver;
  505. };
  506. self.showVersion = level => {
  507. const logger = yargs.getInternalMethods().getLoggerInstance();
  508. if (!level)
  509. level = 'error';
  510. const emit = typeof level === 'function' ? level : logger[level];
  511. emit(version);
  512. };
  513. self.reset = function reset(localLookup) {
  514. failMessage = null;
  515. failureOutput = false;
  516. usages = [];
  517. usageDisabled = false;
  518. epilogs = [];
  519. examples = [];
  520. commands = [];
  521. descriptions = objFilter(descriptions, k => !localLookup[k]);
  522. return self;
  523. };
  524. const frozens = [];
  525. self.freeze = function freeze() {
  526. frozens.push({
  527. failMessage,
  528. failureOutput,
  529. usages,
  530. usageDisabled,
  531. epilogs,
  532. examples,
  533. commands,
  534. descriptions,
  535. });
  536. };
  537. self.unfreeze = function unfreeze() {
  538. const frozen = frozens.pop();
  539. if (!frozen)
  540. return;
  541. ({
  542. failMessage,
  543. failureOutput,
  544. usages,
  545. usageDisabled,
  546. epilogs,
  547. examples,
  548. commands,
  549. descriptions,
  550. } = frozen);
  551. };
  552. return self;
  553. }
  554. function isIndentedText(text) {
  555. return typeof text === 'object';
  556. }
  557. function addIndentation(text, indent) {
  558. return isIndentedText(text)
  559. ? { text: text.text, indentation: text.indentation + indent }
  560. : { text, indentation: indent };
  561. }
  562. function getIndentation(text) {
  563. return isIndentedText(text) ? text.indentation : 0;
  564. }
  565. function getText(text) {
  566. return isIndentedText(text) ? text.text : text;
  567. }