123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- const { humanReadableArgName } = require('./argument.js');
- /**
- * TypeScript import types for JSDoc, used by Visual Studio Code IntelliSense and `npm run typescript-checkJS`
- * https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types
- * @typedef { import("./argument.js").Argument } Argument
- * @typedef { import("./command.js").Command } Command
- * @typedef { import("./option.js").Option } Option
- */
- // @ts-check
- // Although this is a class, methods are static in style to allow override using subclass or just functions.
- class Help {
- constructor() {
- this.helpWidth = undefined;
- this.sortSubcommands = false;
- this.sortOptions = false;
- }
- /**
- * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
- *
- * @param {Command} cmd
- * @returns {Command[]}
- */
- visibleCommands(cmd) {
- const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden);
- if (cmd._hasImplicitHelpCommand()) {
- // Create a command matching the implicit help command.
- const [, helpName, helpArgs] = cmd._helpCommandnameAndArgs.match(/([^ ]+) *(.*)/);
- const helpCommand = cmd.createCommand(helpName)
- .helpOption(false);
- helpCommand.description(cmd._helpCommandDescription);
- if (helpArgs) helpCommand.arguments(helpArgs);
- visibleCommands.push(helpCommand);
- }
- if (this.sortSubcommands) {
- visibleCommands.sort((a, b) => {
- // @ts-ignore: overloaded return type
- return a.name().localeCompare(b.name());
- });
- }
- return visibleCommands;
- }
- /**
- * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.
- *
- * @param {Command} cmd
- * @returns {Option[]}
- */
- visibleOptions(cmd) {
- const visibleOptions = cmd.options.filter((option) => !option.hidden);
- // Implicit help
- const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag);
- const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag);
- if (showShortHelpFlag || showLongHelpFlag) {
- let helpOption;
- if (!showShortHelpFlag) {
- helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription);
- } else if (!showLongHelpFlag) {
- helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription);
- } else {
- helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription);
- }
- visibleOptions.push(helpOption);
- }
- if (this.sortOptions) {
- const getSortKey = (option) => {
- // WYSIWYG for order displayed in help with short before long, no special handling for negated.
- return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, '');
- };
- visibleOptions.sort((a, b) => {
- return getSortKey(a).localeCompare(getSortKey(b));
- });
- }
- return visibleOptions;
- }
- /**
- * Get an array of the arguments if any have a description.
- *
- * @param {Command} cmd
- * @returns {Argument[]}
- */
- visibleArguments(cmd) {
- // Side effect! Apply the legacy descriptions before the arguments are displayed.
- if (cmd._argsDescription) {
- cmd._args.forEach(argument => {
- argument.description = argument.description || cmd._argsDescription[argument.name()] || '';
- });
- }
- // If there are any arguments with a description then return all the arguments.
- if (cmd._args.find(argument => argument.description)) {
- return cmd._args;
- };
- return [];
- }
- /**
- * Get the command term to show in the list of subcommands.
- *
- * @param {Command} cmd
- * @returns {string}
- */
- subcommandTerm(cmd) {
- // Legacy. Ignores custom usage string, and nested commands.
- const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' ');
- return cmd._name +
- (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
- (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
- (args ? ' ' + args : '');
- }
- /**
- * Get the option term to show in the list of options.
- *
- * @param {Option} option
- * @returns {string}
- */
- optionTerm(option) {
- return option.flags;
- }
- /**
- * Get the argument term to show in the list of arguments.
- *
- * @param {Argument} argument
- * @returns {string}
- */
- argumentTerm(argument) {
- return argument.name();
- }
- /**
- * Get the longest command term length.
- *
- * @param {Command} cmd
- * @param {Help} helper
- * @returns {number}
- */
- longestSubcommandTermLength(cmd, helper) {
- return helper.visibleCommands(cmd).reduce((max, command) => {
- return Math.max(max, helper.subcommandTerm(command).length);
- }, 0);
- };
- /**
- * Get the longest option term length.
- *
- * @param {Command} cmd
- * @param {Help} helper
- * @returns {number}
- */
- longestOptionTermLength(cmd, helper) {
- return helper.visibleOptions(cmd).reduce((max, option) => {
- return Math.max(max, helper.optionTerm(option).length);
- }, 0);
- };
- /**
- * Get the longest argument term length.
- *
- * @param {Command} cmd
- * @param {Help} helper
- * @returns {number}
- */
- longestArgumentTermLength(cmd, helper) {
- return helper.visibleArguments(cmd).reduce((max, argument) => {
- return Math.max(max, helper.argumentTerm(argument).length);
- }, 0);
- };
- /**
- * Get the command usage to be displayed at the top of the built-in help.
- *
- * @param {Command} cmd
- * @returns {string}
- */
- commandUsage(cmd) {
- // Usage
- let cmdName = cmd._name;
- if (cmd._aliases[0]) {
- cmdName = cmdName + '|' + cmd._aliases[0];
- }
- let parentCmdNames = '';
- for (let parentCmd = cmd.parent; parentCmd; parentCmd = parentCmd.parent) {
- parentCmdNames = parentCmd.name() + ' ' + parentCmdNames;
- }
- return parentCmdNames + cmdName + ' ' + cmd.usage();
- }
- /**
- * Get the description for the command.
- *
- * @param {Command} cmd
- * @returns {string}
- */
- commandDescription(cmd) {
- // @ts-ignore: overloaded return type
- return cmd.description();
- }
- /**
- * Get the command description to show in the list of subcommands.
- *
- * @param {Command} cmd
- * @returns {string}
- */
- subcommandDescription(cmd) {
- // @ts-ignore: overloaded return type
- return cmd.description();
- }
- /**
- * Get the option description to show in the list of options.
- *
- * @param {Option} option
- * @return {string}
- */
- optionDescription(option) {
- const extraInfo = [];
- // Some of these do not make sense for negated boolean and suppress for backwards compatibility.
- if (option.argChoices && !option.negate) {
- extraInfo.push(
- // use stringify to match the display of the default value
- `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
- }
- if (option.defaultValue !== undefined && !option.negate) {
- extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
- }
- if (option.envVar !== undefined) {
- extraInfo.push(`env: ${option.envVar}`);
- }
- if (extraInfo.length > 0) {
- return `${option.description} (${extraInfo.join(', ')})`;
- }
- return option.description;
- };
- /**
- * Get the argument description to show in the list of arguments.
- *
- * @param {Argument} argument
- * @return {string}
- */
- argumentDescription(argument) {
- const extraInfo = [];
- if (argument.argChoices) {
- extraInfo.push(
- // use stringify to match the display of the default value
- `choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
- }
- if (argument.defaultValue !== undefined) {
- extraInfo.push(`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`);
- }
- if (extraInfo.length > 0) {
- const extraDescripton = `(${extraInfo.join(', ')})`;
- if (argument.description) {
- return `${argument.description} ${extraDescripton}`;
- }
- return extraDescripton;
- }
- return argument.description;
- }
- /**
- * Generate the built-in help text.
- *
- * @param {Command} cmd
- * @param {Help} helper
- * @returns {string}
- */
- formatHelp(cmd, helper) {
- const termWidth = helper.padWidth(cmd, helper);
- const helpWidth = helper.helpWidth || 80;
- const itemIndentWidth = 2;
- const itemSeparatorWidth = 2; // between term and description
- function formatItem(term, description) {
- if (description) {
- const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
- return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
- }
- return term;
- };
- function formatList(textArray) {
- return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
- }
- // Usage
- let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];
- // Description
- const commandDescription = helper.commandDescription(cmd);
- if (commandDescription.length > 0) {
- output = output.concat([commandDescription, '']);
- }
- // Arguments
- const argumentList = helper.visibleArguments(cmd).map((argument) => {
- return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument));
- });
- if (argumentList.length > 0) {
- output = output.concat(['Arguments:', formatList(argumentList), '']);
- }
- // Options
- const optionList = helper.visibleOptions(cmd).map((option) => {
- return formatItem(helper.optionTerm(option), helper.optionDescription(option));
- });
- if (optionList.length > 0) {
- output = output.concat(['Options:', formatList(optionList), '']);
- }
- // Commands
- const commandList = helper.visibleCommands(cmd).map((cmd) => {
- return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
- });
- if (commandList.length > 0) {
- output = output.concat(['Commands:', formatList(commandList), '']);
- }
- return output.join('\n');
- }
- /**
- * Calculate the pad width from the maximum term length.
- *
- * @param {Command} cmd
- * @param {Help} helper
- * @returns {number}
- */
- padWidth(cmd, helper) {
- return Math.max(
- helper.longestOptionTermLength(cmd, helper),
- helper.longestSubcommandTermLength(cmd, helper),
- helper.longestArgumentTermLength(cmd, helper)
- );
- };
- /**
- * Wrap the given string to width characters per line, with lines after the first indented.
- * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted.
- *
- * @param {string} str
- * @param {number} width
- * @param {number} indent
- * @param {number} [minColumnWidth=40]
- * @return {string}
- *
- */
- wrap(str, width, indent, minColumnWidth = 40) {
- // Detect manually wrapped and indented strings by searching for line breaks
- // followed by multiple spaces/tabs.
- if (str.match(/[\n]\s+/)) return str;
- // Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line).
- const columnWidth = width - indent;
- if (columnWidth < minColumnWidth) return str;
- const leadingStr = str.substr(0, indent);
- const columnText = str.substr(indent);
- const indentString = ' '.repeat(indent);
- const regex = new RegExp('.{1,' + (columnWidth - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g');
- const lines = columnText.match(regex) || [];
- return leadingStr + lines.map((line, i) => {
- if (line.slice(-1) === '\n') {
- line = line.slice(0, line.length - 1);
- }
- return ((i > 0) ? indentString : '') + line.trimRight();
- }).join('\n');
- }
- }
- exports.Help = Help;
|