help.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. const { humanReadableArgName } = require('./argument.js');
  2. /**
  3. * TypeScript import types for JSDoc, used by Visual Studio Code IntelliSense and `npm run typescript-checkJS`
  4. * https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types
  5. * @typedef { import("./argument.js").Argument } Argument
  6. * @typedef { import("./command.js").Command } Command
  7. * @typedef { import("./option.js").Option } Option
  8. */
  9. // Although this is a class, methods are static in style to allow override using subclass or just functions.
  10. class Help {
  11. constructor() {
  12. this.helpWidth = undefined;
  13. this.minWidthToWrap = 40;
  14. this.sortSubcommands = false;
  15. this.sortOptions = false;
  16. this.showGlobalOptions = false;
  17. }
  18. /**
  19. * prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
  20. * and just before calling `formatHelp()`.
  21. *
  22. * Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses.
  23. *
  24. * @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions
  25. */
  26. prepareContext(contextOptions) {
  27. this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
  28. }
  29. /**
  30. * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
  31. *
  32. * @param {Command} cmd
  33. * @returns {Command[]}
  34. */
  35. visibleCommands(cmd) {
  36. const visibleCommands = cmd.commands.filter((cmd) => !cmd._hidden);
  37. const helpCommand = cmd._getHelpCommand();
  38. if (helpCommand && !helpCommand._hidden) {
  39. visibleCommands.push(helpCommand);
  40. }
  41. if (this.sortSubcommands) {
  42. visibleCommands.sort((a, b) => {
  43. // @ts-ignore: because overloaded return type
  44. return a.name().localeCompare(b.name());
  45. });
  46. }
  47. return visibleCommands;
  48. }
  49. /**
  50. * Compare options for sort.
  51. *
  52. * @param {Option} a
  53. * @param {Option} b
  54. * @returns {number}
  55. */
  56. compareOptions(a, b) {
  57. const getSortKey = (option) => {
  58. // WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.
  59. return option.short
  60. ? option.short.replace(/^-/, '')
  61. : option.long.replace(/^--/, '');
  62. };
  63. return getSortKey(a).localeCompare(getSortKey(b));
  64. }
  65. /**
  66. * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.
  67. *
  68. * @param {Command} cmd
  69. * @returns {Option[]}
  70. */
  71. visibleOptions(cmd) {
  72. const visibleOptions = cmd.options.filter((option) => !option.hidden);
  73. // Built-in help option.
  74. const helpOption = cmd._getHelpOption();
  75. if (helpOption && !helpOption.hidden) {
  76. // Automatically hide conflicting flags. Bit dubious but a historical behaviour that is convenient for single-command programs.
  77. const removeShort = helpOption.short && cmd._findOption(helpOption.short);
  78. const removeLong = helpOption.long && cmd._findOption(helpOption.long);
  79. if (!removeShort && !removeLong) {
  80. visibleOptions.push(helpOption); // no changes needed
  81. } else if (helpOption.long && !removeLong) {
  82. visibleOptions.push(
  83. cmd.createOption(helpOption.long, helpOption.description),
  84. );
  85. } else if (helpOption.short && !removeShort) {
  86. visibleOptions.push(
  87. cmd.createOption(helpOption.short, helpOption.description),
  88. );
  89. }
  90. }
  91. if (this.sortOptions) {
  92. visibleOptions.sort(this.compareOptions);
  93. }
  94. return visibleOptions;
  95. }
  96. /**
  97. * Get an array of the visible global options. (Not including help.)
  98. *
  99. * @param {Command} cmd
  100. * @returns {Option[]}
  101. */
  102. visibleGlobalOptions(cmd) {
  103. if (!this.showGlobalOptions) return [];
  104. const globalOptions = [];
  105. for (
  106. let ancestorCmd = cmd.parent;
  107. ancestorCmd;
  108. ancestorCmd = ancestorCmd.parent
  109. ) {
  110. const visibleOptions = ancestorCmd.options.filter(
  111. (option) => !option.hidden,
  112. );
  113. globalOptions.push(...visibleOptions);
  114. }
  115. if (this.sortOptions) {
  116. globalOptions.sort(this.compareOptions);
  117. }
  118. return globalOptions;
  119. }
  120. /**
  121. * Get an array of the arguments if any have a description.
  122. *
  123. * @param {Command} cmd
  124. * @returns {Argument[]}
  125. */
  126. visibleArguments(cmd) {
  127. // Side effect! Apply the legacy descriptions before the arguments are displayed.
  128. if (cmd._argsDescription) {
  129. cmd.registeredArguments.forEach((argument) => {
  130. argument.description =
  131. argument.description || cmd._argsDescription[argument.name()] || '';
  132. });
  133. }
  134. // If there are any arguments with a description then return all the arguments.
  135. if (cmd.registeredArguments.find((argument) => argument.description)) {
  136. return cmd.registeredArguments;
  137. }
  138. return [];
  139. }
  140. /**
  141. * Get the command term to show in the list of subcommands.
  142. *
  143. * @param {Command} cmd
  144. * @returns {string}
  145. */
  146. subcommandTerm(cmd) {
  147. // Legacy. Ignores custom usage string, and nested commands.
  148. const args = cmd.registeredArguments
  149. .map((arg) => humanReadableArgName(arg))
  150. .join(' ');
  151. return (
  152. cmd._name +
  153. (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
  154. (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
  155. (args ? ' ' + args : '')
  156. );
  157. }
  158. /**
  159. * Get the option term to show in the list of options.
  160. *
  161. * @param {Option} option
  162. * @returns {string}
  163. */
  164. optionTerm(option) {
  165. return option.flags;
  166. }
  167. /**
  168. * Get the argument term to show in the list of arguments.
  169. *
  170. * @param {Argument} argument
  171. * @returns {string}
  172. */
  173. argumentTerm(argument) {
  174. return argument.name();
  175. }
  176. /**
  177. * Get the longest command term length.
  178. *
  179. * @param {Command} cmd
  180. * @param {Help} helper
  181. * @returns {number}
  182. */
  183. longestSubcommandTermLength(cmd, helper) {
  184. return helper.visibleCommands(cmd).reduce((max, command) => {
  185. return Math.max(
  186. max,
  187. this.displayWidth(
  188. helper.styleSubcommandTerm(helper.subcommandTerm(command)),
  189. ),
  190. );
  191. }, 0);
  192. }
  193. /**
  194. * Get the longest option term length.
  195. *
  196. * @param {Command} cmd
  197. * @param {Help} helper
  198. * @returns {number}
  199. */
  200. longestOptionTermLength(cmd, helper) {
  201. return helper.visibleOptions(cmd).reduce((max, option) => {
  202. return Math.max(
  203. max,
  204. this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
  205. );
  206. }, 0);
  207. }
  208. /**
  209. * Get the longest global option term length.
  210. *
  211. * @param {Command} cmd
  212. * @param {Help} helper
  213. * @returns {number}
  214. */
  215. longestGlobalOptionTermLength(cmd, helper) {
  216. return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
  217. return Math.max(
  218. max,
  219. this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
  220. );
  221. }, 0);
  222. }
  223. /**
  224. * Get the longest argument term length.
  225. *
  226. * @param {Command} cmd
  227. * @param {Help} helper
  228. * @returns {number}
  229. */
  230. longestArgumentTermLength(cmd, helper) {
  231. return helper.visibleArguments(cmd).reduce((max, argument) => {
  232. return Math.max(
  233. max,
  234. this.displayWidth(
  235. helper.styleArgumentTerm(helper.argumentTerm(argument)),
  236. ),
  237. );
  238. }, 0);
  239. }
  240. /**
  241. * Get the command usage to be displayed at the top of the built-in help.
  242. *
  243. * @param {Command} cmd
  244. * @returns {string}
  245. */
  246. commandUsage(cmd) {
  247. // Usage
  248. let cmdName = cmd._name;
  249. if (cmd._aliases[0]) {
  250. cmdName = cmdName + '|' + cmd._aliases[0];
  251. }
  252. let ancestorCmdNames = '';
  253. for (
  254. let ancestorCmd = cmd.parent;
  255. ancestorCmd;
  256. ancestorCmd = ancestorCmd.parent
  257. ) {
  258. ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames;
  259. }
  260. return ancestorCmdNames + cmdName + ' ' + cmd.usage();
  261. }
  262. /**
  263. * Get the description for the command.
  264. *
  265. * @param {Command} cmd
  266. * @returns {string}
  267. */
  268. commandDescription(cmd) {
  269. // @ts-ignore: because overloaded return type
  270. return cmd.description();
  271. }
  272. /**
  273. * Get the subcommand summary to show in the list of subcommands.
  274. * (Fallback to description for backwards compatibility.)
  275. *
  276. * @param {Command} cmd
  277. * @returns {string}
  278. */
  279. subcommandDescription(cmd) {
  280. // @ts-ignore: because overloaded return type
  281. return cmd.summary() || cmd.description();
  282. }
  283. /**
  284. * Get the option description to show in the list of options.
  285. *
  286. * @param {Option} option
  287. * @return {string}
  288. */
  289. optionDescription(option) {
  290. const extraInfo = [];
  291. if (option.argChoices) {
  292. extraInfo.push(
  293. // use stringify to match the display of the default value
  294. `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
  295. );
  296. }
  297. if (option.defaultValue !== undefined) {
  298. // default for boolean and negated more for programmer than end user,
  299. // but show true/false for boolean option as may be for hand-rolled env or config processing.
  300. const showDefault =
  301. option.required ||
  302. option.optional ||
  303. (option.isBoolean() && typeof option.defaultValue === 'boolean');
  304. if (showDefault) {
  305. extraInfo.push(
  306. `default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,
  307. );
  308. }
  309. }
  310. // preset for boolean and negated are more for programmer than end user
  311. if (option.presetArg !== undefined && option.optional) {
  312. extraInfo.push(`preset: ${JSON.stringify(option.presetArg)}`);
  313. }
  314. if (option.envVar !== undefined) {
  315. extraInfo.push(`env: ${option.envVar}`);
  316. }
  317. if (extraInfo.length > 0) {
  318. const extraDescription = `(${extraInfo.join(', ')})`;
  319. if (option.description) {
  320. return `${option.description} ${extraDescription}`;
  321. }
  322. return extraDescription;
  323. }
  324. return option.description;
  325. }
  326. /**
  327. * Get the argument description to show in the list of arguments.
  328. *
  329. * @param {Argument} argument
  330. * @return {string}
  331. */
  332. argumentDescription(argument) {
  333. const extraInfo = [];
  334. if (argument.argChoices) {
  335. extraInfo.push(
  336. // use stringify to match the display of the default value
  337. `choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
  338. );
  339. }
  340. if (argument.defaultValue !== undefined) {
  341. extraInfo.push(
  342. `default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`,
  343. );
  344. }
  345. if (extraInfo.length > 0) {
  346. const extraDescription = `(${extraInfo.join(', ')})`;
  347. if (argument.description) {
  348. return `${argument.description} ${extraDescription}`;
  349. }
  350. return extraDescription;
  351. }
  352. return argument.description;
  353. }
  354. /**
  355. * Format a list of items, given a heading and an array of formatted items.
  356. *
  357. * @param {string} heading
  358. * @param {string[]} items
  359. * @param {Help} helper
  360. * @returns string[]
  361. */
  362. formatItemList(heading, items, helper) {
  363. if (items.length === 0) return [];
  364. return [helper.styleTitle(heading), ...items, ''];
  365. }
  366. /**
  367. * Group items by their help group heading.
  368. *
  369. * @param {Command[] | Option[]} unsortedItems
  370. * @param {Command[] | Option[]} visibleItems
  371. * @param {Function} getGroup
  372. * @returns {Map<string, Command[] | Option[]>}
  373. */
  374. groupItems(unsortedItems, visibleItems, getGroup) {
  375. const result = new Map();
  376. // Add groups in order of appearance in unsortedItems.
  377. unsortedItems.forEach((item) => {
  378. const group = getGroup(item);
  379. if (!result.has(group)) result.set(group, []);
  380. });
  381. // Add items in order of appearance in visibleItems.
  382. visibleItems.forEach((item) => {
  383. const group = getGroup(item);
  384. if (!result.has(group)) {
  385. result.set(group, []);
  386. }
  387. result.get(group).push(item);
  388. });
  389. return result;
  390. }
  391. /**
  392. * Generate the built-in help text.
  393. *
  394. * @param {Command} cmd
  395. * @param {Help} helper
  396. * @returns {string}
  397. */
  398. formatHelp(cmd, helper) {
  399. const termWidth = helper.padWidth(cmd, helper);
  400. const helpWidth = helper.helpWidth ?? 80; // in case prepareContext() was not called
  401. function callFormatItem(term, description) {
  402. return helper.formatItem(term, termWidth, description, helper);
  403. }
  404. // Usage
  405. let output = [
  406. `${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
  407. '',
  408. ];
  409. // Description
  410. const commandDescription = helper.commandDescription(cmd);
  411. if (commandDescription.length > 0) {
  412. output = output.concat([
  413. helper.boxWrap(
  414. helper.styleCommandDescription(commandDescription),
  415. helpWidth,
  416. ),
  417. '',
  418. ]);
  419. }
  420. // Arguments
  421. const argumentList = helper.visibleArguments(cmd).map((argument) => {
  422. return callFormatItem(
  423. helper.styleArgumentTerm(helper.argumentTerm(argument)),
  424. helper.styleArgumentDescription(helper.argumentDescription(argument)),
  425. );
  426. });
  427. output = output.concat(
  428. this.formatItemList('Arguments:', argumentList, helper),
  429. );
  430. // Options
  431. const optionGroups = this.groupItems(
  432. cmd.options,
  433. helper.visibleOptions(cmd),
  434. (option) => option.helpGroupHeading ?? 'Options:',
  435. );
  436. optionGroups.forEach((options, group) => {
  437. const optionList = options.map((option) => {
  438. return callFormatItem(
  439. helper.styleOptionTerm(helper.optionTerm(option)),
  440. helper.styleOptionDescription(helper.optionDescription(option)),
  441. );
  442. });
  443. output = output.concat(this.formatItemList(group, optionList, helper));
  444. });
  445. if (helper.showGlobalOptions) {
  446. const globalOptionList = helper
  447. .visibleGlobalOptions(cmd)
  448. .map((option) => {
  449. return callFormatItem(
  450. helper.styleOptionTerm(helper.optionTerm(option)),
  451. helper.styleOptionDescription(helper.optionDescription(option)),
  452. );
  453. });
  454. output = output.concat(
  455. this.formatItemList('Global Options:', globalOptionList, helper),
  456. );
  457. }
  458. // Commands
  459. const commandGroups = this.groupItems(
  460. cmd.commands,
  461. helper.visibleCommands(cmd),
  462. (sub) => sub.helpGroup() || 'Commands:',
  463. );
  464. commandGroups.forEach((commands, group) => {
  465. const commandList = commands.map((sub) => {
  466. return callFormatItem(
  467. helper.styleSubcommandTerm(helper.subcommandTerm(sub)),
  468. helper.styleSubcommandDescription(helper.subcommandDescription(sub)),
  469. );
  470. });
  471. output = output.concat(this.formatItemList(group, commandList, helper));
  472. });
  473. return output.join('\n');
  474. }
  475. /**
  476. * Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
  477. *
  478. * @param {string} str
  479. * @returns {number}
  480. */
  481. displayWidth(str) {
  482. return stripColor(str).length;
  483. }
  484. /**
  485. * Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
  486. *
  487. * @param {string} str
  488. * @returns {string}
  489. */
  490. styleTitle(str) {
  491. return str;
  492. }
  493. styleUsage(str) {
  494. // Usage has lots of parts the user might like to color separately! Assume default usage string which is formed like:
  495. // command subcommand [options] [command] <foo> [bar]
  496. return str
  497. .split(' ')
  498. .map((word) => {
  499. if (word === '[options]') return this.styleOptionText(word);
  500. if (word === '[command]') return this.styleSubcommandText(word);
  501. if (word[0] === '[' || word[0] === '<')
  502. return this.styleArgumentText(word);
  503. return this.styleCommandText(word); // Restrict to initial words?
  504. })
  505. .join(' ');
  506. }
  507. styleCommandDescription(str) {
  508. return this.styleDescriptionText(str);
  509. }
  510. styleOptionDescription(str) {
  511. return this.styleDescriptionText(str);
  512. }
  513. styleSubcommandDescription(str) {
  514. return this.styleDescriptionText(str);
  515. }
  516. styleArgumentDescription(str) {
  517. return this.styleDescriptionText(str);
  518. }
  519. styleDescriptionText(str) {
  520. return str;
  521. }
  522. styleOptionTerm(str) {
  523. return this.styleOptionText(str);
  524. }
  525. styleSubcommandTerm(str) {
  526. // This is very like usage with lots of parts! Assume default string which is formed like:
  527. // subcommand [options] <foo> [bar]
  528. return str
  529. .split(' ')
  530. .map((word) => {
  531. if (word === '[options]') return this.styleOptionText(word);
  532. if (word[0] === '[' || word[0] === '<')
  533. return this.styleArgumentText(word);
  534. return this.styleSubcommandText(word); // Restrict to initial words?
  535. })
  536. .join(' ');
  537. }
  538. styleArgumentTerm(str) {
  539. return this.styleArgumentText(str);
  540. }
  541. styleOptionText(str) {
  542. return str;
  543. }
  544. styleArgumentText(str) {
  545. return str;
  546. }
  547. styleSubcommandText(str) {
  548. return str;
  549. }
  550. styleCommandText(str) {
  551. return str;
  552. }
  553. /**
  554. * Calculate the pad width from the maximum term length.
  555. *
  556. * @param {Command} cmd
  557. * @param {Help} helper
  558. * @returns {number}
  559. */
  560. padWidth(cmd, helper) {
  561. return Math.max(
  562. helper.longestOptionTermLength(cmd, helper),
  563. helper.longestGlobalOptionTermLength(cmd, helper),
  564. helper.longestSubcommandTermLength(cmd, helper),
  565. helper.longestArgumentTermLength(cmd, helper),
  566. );
  567. }
  568. /**
  569. * Detect manually wrapped and indented strings by checking for line break followed by whitespace.
  570. *
  571. * @param {string} str
  572. * @returns {boolean}
  573. */
  574. preformatted(str) {
  575. return /\n[^\S\r\n]/.test(str);
  576. }
  577. /**
  578. * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
  579. *
  580. * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
  581. * TTT DDD DDDD
  582. * DD DDD
  583. *
  584. * @param {string} term
  585. * @param {number} termWidth
  586. * @param {string} description
  587. * @param {Help} helper
  588. * @returns {string}
  589. */
  590. formatItem(term, termWidth, description, helper) {
  591. const itemIndent = 2;
  592. const itemIndentStr = ' '.repeat(itemIndent);
  593. if (!description) return itemIndentStr + term;
  594. // Pad the term out to a consistent width, so descriptions are aligned.
  595. const paddedTerm = term.padEnd(
  596. termWidth + term.length - helper.displayWidth(term),
  597. );
  598. // Format the description.
  599. const spacerWidth = 2; // between term and description
  600. const helpWidth = this.helpWidth ?? 80; // in case prepareContext() was not called
  601. const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
  602. let formattedDescription;
  603. if (
  604. remainingWidth < this.minWidthToWrap ||
  605. helper.preformatted(description)
  606. ) {
  607. formattedDescription = description;
  608. } else {
  609. const wrappedDescription = helper.boxWrap(description, remainingWidth);
  610. formattedDescription = wrappedDescription.replace(
  611. /\n/g,
  612. '\n' + ' '.repeat(termWidth + spacerWidth),
  613. );
  614. }
  615. // Construct and overall indent.
  616. return (
  617. itemIndentStr +
  618. paddedTerm +
  619. ' '.repeat(spacerWidth) +
  620. formattedDescription.replace(/\n/g, `\n${itemIndentStr}`)
  621. );
  622. }
  623. /**
  624. * Wrap a string at whitespace, preserving existing line breaks.
  625. * Wrapping is skipped if the width is less than `minWidthToWrap`.
  626. *
  627. * @param {string} str
  628. * @param {number} width
  629. * @returns {string}
  630. */
  631. boxWrap(str, width) {
  632. if (width < this.minWidthToWrap) return str;
  633. const rawLines = str.split(/\r\n|\n/);
  634. // split up text by whitespace
  635. const chunkPattern = /[\s]*[^\s]+/g;
  636. const wrappedLines = [];
  637. rawLines.forEach((line) => {
  638. const chunks = line.match(chunkPattern);
  639. if (chunks === null) {
  640. wrappedLines.push('');
  641. return;
  642. }
  643. let sumChunks = [chunks.shift()];
  644. let sumWidth = this.displayWidth(sumChunks[0]);
  645. chunks.forEach((chunk) => {
  646. const visibleWidth = this.displayWidth(chunk);
  647. // Accumulate chunks while they fit into width.
  648. if (sumWidth + visibleWidth <= width) {
  649. sumChunks.push(chunk);
  650. sumWidth += visibleWidth;
  651. return;
  652. }
  653. wrappedLines.push(sumChunks.join(''));
  654. const nextChunk = chunk.trimStart(); // trim space at line break
  655. sumChunks = [nextChunk];
  656. sumWidth = this.displayWidth(nextChunk);
  657. });
  658. wrappedLines.push(sumChunks.join(''));
  659. });
  660. return wrappedLines.join('\n');
  661. }
  662. }
  663. /**
  664. * Strip style ANSI escape sequences from the string. In particular, SGR (Select Graphic Rendition) codes.
  665. *
  666. * @param {string} str
  667. * @returns {string}
  668. * @package
  669. */
  670. function stripColor(str) {
  671. // eslint-disable-next-line no-control-regex
  672. const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
  673. return str.replace(sgrPattern, '');
  674. }
  675. exports.Help = Help;
  676. exports.stripColor = stripColor;