UNPKG

@zowe/imperative

Version:
515 lines 23 kB
"use strict"; /* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html * * SPDX-License-Identifier: EPL-2.0 * * Copyright Contributors to the Zowe Project. * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DefaultHelpGenerator = void 0; const util_1 = require("util"); const AbstractHelpGenerator_1 = require("./abstract/AbstractHelpGenerator"); const utilities_1 = require("../../../utilities"); const constants_1 = require("../../../constants"); const CommandUtils_1 = require("../utils/CommandUtils"); const error_1 = require("../../../error"); const ICommandDefinition_1 = require("../doc/ICommandDefinition"); const stripAnsi = require("strip-ansi"); const CliUtils_1 = require("../../../utilities/src/CliUtils"); /** * Imperative default help generator. Accepts the command definitions and constructs * the full help text for the command node. * * TODO - Consider removing word wrap on a fixed with and apply dynamically based on terminal sizes * @export * @class DefaultHelpGenerator * @extends {AbstractHelpGenerator} */ class DefaultHelpGenerator extends AbstractHelpGenerator_1.AbstractHelpGenerator { /** * Creates an instance of DefaultHelpGenerator. * @param {IHelpGeneratorFactoryParms} defaultParms - Imperative config parameters for help generation - See interface for details * @param {IHelpGeneratorParms} commandParms - The command definitions for generating help - See interface for details * @memberof DefaultHelpGenerator */ constructor(defaultParms, commandParms) { var _a; super(defaultParms, commandParms); /** * Indicates that the help generator should skip introducing breaks based on terminal width * @type {boolean} * @memberof IHelpGeneratorParms */ this.skipTextWrap = false; this.skipTextWrap = (_a = commandParms.skipTextWrap) !== null && _a !== void 0 ? _a : false; this.buildOptionMaps(); } /** * Construct the full help text for display. * @returns {string} - The full help text * @memberof DefaultHelpGenerator */ buildHelp() { let helpText = ""; switch (this.mCommandDefinition.type) { case "group": helpText = this.buildFullGroupHelpText(); break; case "command": helpText = this.buildFullCommandHelpText(); break; default: throw new error_1.ImperativeError({ msg: `${DefaultHelpGenerator.ERROR_TAG} Unknown command definition type specified: "${this.mCommandDefinition.type}"` }); } return helpText; } /** * Build the help text for a "group" - a group has a set of children The help text contains the standard * description/usage/etc., but unlike a command only displays the next set of "commands" or "groups" that can * be issued after the current node. * @returns {string} - the full group help text * @memberof DefaultHelpGenerator */ buildFullGroupHelpText() { let helpText = "\n"; // Description and usage helpText += this.buildDescriptionSection(); helpText += this.buildUsageSection(); // markdown is not requested, build the children summary tables and // The global options if (!this.mProduceMarkdown) { helpText += this.buildChildrenSummaryTables(); } // Append any options helpText += this.buildCommandOptionsSection(); // Append any global options if (!this.mProduceMarkdown) { helpText += this.buildGlobalOptionsSection(); } // Get any example text helpText += this.buildExamplesSection(); return helpText; } /** * Returns the help text for the command definition - the help text contains the standard items such as * description/usage/etc. and also contains the full option descriptions for the command. * @param {boolean} [includeGlobalOptions=true] - Include the global options in the help text * @returns {string} - The help text for --help or markdown. */ buildFullCommandHelpText(includeGlobalOptions = true) { let helpText = ""; // Construct the command name section. if (!this.mProduceMarkdown && this.mCommandDefinition.name != null && this.mCommandDefinition.name.length > 0) { helpText += "\n" + this.buildHeader("COMMAND NAME"); helpText += DefaultHelpGenerator.HELP_INDENT + this.mCommandDefinition.name; if (this.mCommandDefinition.aliases != null && this.mCommandDefinition.aliases.length > 0) { helpText += " | " + this.mCommandDefinition.aliases.join(" | "); } if (this.mCommandDefinition.experimental) { helpText += this.grey(DefaultHelpGenerator.HELP_INDENT + "(experimental command)\n\n"); } else { helpText += "\n\n"; } } // Only include global options by request and we're not producing markdown includeGlobalOptions = includeGlobalOptions && !this.mProduceMarkdown; // Print standard areas like description and usage helpText += this.buildDescriptionSection(); helpText += this.buildUsageSection(); // Add positional arguments to the help text if (this.mCommandDefinition.positionals != null && this.mCommandDefinition.positionals.length > 0) { helpText += this.buildPositionalArgumentsSection(); } // Add options to the help text helpText += this.buildCommandOptionsSection(); if (includeGlobalOptions) { helpText += this.buildGlobalOptionsSection(); } // Build experimental description section and examples helpText += this.getExperimentalCommandSection(); helpText += this.buildExamplesSection(); return helpText; } /** * Build a string containing the command name and aliases separated by the vertical bar: * command | c * @param {ICommandDefinition} commandDefinition - The definition for the command * @returns {string} - Contains the command name followed by the aliases (e.g. command | c) * @memberof DefaultHelpGenerator */ buildCommandAndAliases(commandDefinition) { let names = commandDefinition.name; if (commandDefinition.aliases != null) { if (commandDefinition.aliases.join("").trim().length !== 0) { names += " | "; names += commandDefinition.aliases.join(" | "); } } return names; } /** * Builds a table of commands/groups for display in the help: * * GROUPS * ------ * group1 hello this is group1 * group2 hello this is group2 * * @return {string}: Returns the table for display. */ buildChildrenSummaryTables() { // Construct a map of all the types and definitions - we may produce multiple tables // if the children of the current command are not all the same type const childrenDefinitions = this.mCommandDefinition.children.sort(ICommandDefinition_1.compareCommands); const typeMap = new Map(); for (const def of childrenDefinitions) { const children = typeMap.get(def.type); if (children == null) { typeMap.set(def.type, [def]); } else { typeMap.set(def.type, children.concat(def)); } } // Iterate through the map and children, creating a table with heading for each type let fullTableText = ""; typeMap.forEach((definitions, type) => { // Construct the table const table = []; let maximumLeftHandSide = 0; for (const command of definitions) { let summaryText = ""; summaryText += command.summary || command.description; if (command.deprecatedReplacement != null) { // Mark with the deprecated tag summaryText += this.grey(" (deprecated)"); } else if (command.experimental) { // Mark with the experimental tag summaryText += this.grey(" (experimental) "); } const printString = DefaultHelpGenerator.HELP_INDENT + this.buildCommandAndAliases(command); if (printString.length > maximumLeftHandSide) { maximumLeftHandSide = printString.length; } table.push({ name: printString, summary: summaryText }); } let maxColumnWidth; // if all the items in the left hand side are less than half of the screen width, // set the maximum column length for the action/object/(child command)/etc. table // to be based on that so that we don't wrap unnecessarily if (maximumLeftHandSide < utilities_1.TextUtils.getRecommendedWidth() / 2) { maxColumnWidth = utilities_1.TextUtils.getRecommendedWidth() - maximumLeftHandSide; } let tableText = utilities_1.TextUtils.getTable(table, this.mPrimaryHighlightColor, maxColumnWidth, false); tableText = tableText.split(/\n/g).map((line) => { return DefaultHelpGenerator.HELP_INDENT + line; // indent the table }).join("\n"); const properCaseType = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase(); fullTableText += this.renderHelp(this.buildHeader(properCaseType + "s") + tableText + "\n\n"); }); // Return all the table tests return fullTableText; } /** * Build the usage diagram for the command. * TODO - very simple at the moment, should be enhanced with a "better" diagram * @returns {string} * @memberof DefaultHelpGenerator */ buildUsageDiagram() { let usage = /* binary name */ this.mRootCommandName + " " + CommandUtils_1.CommandUtils.getFullCommandName(this.mCommandDefinition, this.mDefinitionTree); // For a command, build the usage diagram with positional and options. if (this.mCommandDefinition.type === "command") { // Place the positional parameters. if (this.mCommandDefinition.positionals != null) { for (const positional of this.mCommandDefinition.positionals) { usage += " " + (positional.required ? "<" + positional.name + ">" : "[" + positional.name + "]"); } } // Append the options segment usage += " " + constants_1.Constants.OPTIONS_SEGMENT; } else if (this.mCommandDefinition.type === "group") { // Determine what command section we are currently at and append the correct usages. usage = usage.trim(); if (this.mCommandDefinition.children != null && this.mCommandDefinition.children.length > 0) { // Get all the possible command types. (E.G <group>, <command>, <command|group>, ETC) let nextType = "<"; // usage += " <"; const types = []; for (const definition of this.mCommandDefinition.children) { if (!types.includes(definition.type)) { types.push(definition.type); } } nextType += types.join("|") + ">"; usage += ` ${nextType}\n\n${DefaultHelpGenerator.HELP_INDENT}Where ${nextType} is one of the following:`; } else { usage += " " + constants_1.Constants.OPTIONS_SEGMENT; } } else { throw new error_1.ImperativeError({ msg: `${DefaultHelpGenerator.ERROR_TAG} Unknown or unsupported command type ` + `"${this.mCommandDefinition.type}" used in command definition.` }); } return usage; } /** * Build the usage section of the help text: * * USAGE * ----- * command blah [options] * * @returns {string} - The usage help text section * @memberof DefaultHelpGenerator */ buildUsageSection() { return this.renderHelp(this.buildHeader("Usage") + DefaultHelpGenerator.HELP_INDENT + this.buildUsageDiagram()) + "\n\n"; } /** * Build the global options section of the command help text. * * GLOBAL OPTIONS * -------------- * ... * * @returns {string} - The global options help text section * @memberof DefaultHelpGenerator */ buildGlobalOptionsSection() { let result = this.buildHeader(constants_1.Constants.GLOBAL_GROUP); if (this.groupToOption[constants_1.Constants.GLOBAL_GROUP] != null) { for (const globalOption of this.groupToOption[constants_1.Constants.GLOBAL_GROUP]) { result += this.buildOptionText(globalOption, this.optionToDescription[globalOption]); } } return this.renderHelp(result); } /** * Build the command description section of the help text: * * DESCRIPTION * ----------- * This command is great. * * @returns {string} - The command description text * @memberof DefaultHelpGenerator */ buildDescriptionSection() { let descriptionForHelp = ""; if (!this.mProduceMarkdown) { descriptionForHelp += this.buildHeader("DESCRIPTION"); } let description = this.mCommandDefinition.description || this.mCommandDefinition.summary; // Use consolidated deprecated message logic description += this.grey(CliUtils_1.CliUtils.generateDeprecatedMessage(this.mCommandDefinition, true)); if (this.mProduceMarkdown) { description = this.escapeMarkdown(description); } if (this.skipTextWrap) { descriptionForHelp += utilities_1.TextUtils.indentLines(description, this.mProduceMarkdown ? "" : DefaultHelpGenerator.HELP_INDENT); } else { descriptionForHelp += utilities_1.TextUtils.wordWrap(description, undefined, this.mProduceMarkdown ? "" : DefaultHelpGenerator.HELP_INDENT); } return this.renderHelp(descriptionForHelp + "\n\n"); } /** * Return the help text format for positional parameters - includes the parameter itself, the optional regex, * and the full description. * @returns {string} - The help text for each positional parameter. * @memberof DefaultHelpGenerator */ buildPositionalArgumentsSection() { if (this.mCommandDefinition.positionals != null && this.mCommandDefinition.positionals.length > 0) { let positionalsHelpText = this.buildHeader("Positional Arguments"); for (const positional of this.mCommandDefinition.positionals) { const positionalString = "{{codeBegin}}" + positional.name + "{{codeEnd}}\t\t " + this.dimGrey("{{italic}}(" + this.explainType(positional.type) + "){{italic}}"); let fullDescription = positional.description; if (positional.regex) { fullDescription += DefaultHelpGenerator.HELP_INDENT + DefaultHelpGenerator.HELP_INDENT + "Must match regular expression: {{codeBegin}}" + positional.regex + "{{codeEnd}}\n\n"; } positionalsHelpText += this.buildOptionText(positionalString, fullDescription); } return this.renderHelp(positionalsHelpText); } else { throw new error_1.ImperativeError({ msg: `${DefaultHelpGenerator.ERROR_TAG} Unable to print positional arguments: None were supplied.` }); } } /** * From the map of options (group to option), formulate the group and options in the form of: * * OPTION GROUP * ------------ * * option1 * * Description of option1 * * option2 * * Description of option2 * * @return {string} */ buildCommandOptionsSection() { let result = ""; for (const group of Object.keys(this.groupToOption)) { if (group === constants_1.Constants.GLOBAL_GROUP) { // skip global options for now, we'll put them somewhere else continue; } result += this.buildHeader(group); for (const optionString of this.groupToOption[group]) { result += this.buildOptionText(optionString, this.optionToDescription[optionString]); } } return this.renderHelp(result); } /** * Build the text for option: * * --option * * The description for this option * * @param {string} optionString - The option string (-- form or positional, etc.) * @param {string} description - The description for the option * @return {string} - The option text */ buildOptionText(optionString, description) { if (this.mProduceMarkdown) { description = this.escapeMarkdown(description); // escape Markdown special characters } if (this.skipTextWrap) { description = utilities_1.TextUtils.indentLines(description.trim(), DefaultHelpGenerator.HELP_INDENT + DefaultHelpGenerator.HELP_INDENT); } else { description = utilities_1.TextUtils.wordWrap(description.trim(), undefined, DefaultHelpGenerator.HELP_INDENT + DefaultHelpGenerator.HELP_INDENT); } if (this.mProduceMarkdown) { // for markdown, remove leading spaces from the description so that the first line // is not indented description = description.replace(/^\s*/, ""); } return this.renderHelp((0, util_1.format)("{{bullet}}%s\n\n{{indent}}{{bullet}}{{space}}%s\n\n", DefaultHelpGenerator.HELP_INDENT + optionString, description)); } /** * Produces a header for the current section in help: * * COMMANDS * -------- * * @param {string} header - the header text (e.g. COMMANDS) * @returns {string} - The header * @memberof DefaultHelpGenerator */ buildHeader(header) { return this.renderHelp((0, util_1.format)("{{header}}{{header}}{{header}}{{header}}{{space}}%s\n\n", this.mProduceMarkdown ? header : DefaultHelpGenerator.formatHelpHeader(header, undefined, this.mPrimaryHighlightColor))); } /** * Build the examples text for the command. Examples include the command example (which normally is able to * be copy/pasted verbatim) and the description for the example. * TODO - we should remove wordwrap from this * @returns {string} - The example text * @memberof DefaultHelpGenerator */ buildExamplesSection() { let examplesText = ""; if (this.mCommandDefinition.examples != null) { examplesText = this.mCommandDefinition.examples.map((example) => { const prefix = example.prefix != null ? example.prefix + "{{space}} " : ""; const exampleHyphen = this.mProduceMarkdown ? "" : "-"; const options = example.options.length > 0 ? ` ${example.options}` : ""; const description = this.mProduceMarkdown ? this.escapeMarkdown(example.description) : example.description; let exampleText = this.mProduceMarkdown ? "{{bullet}}" + exampleHyphen + " {{space}}" + description + ":\n\n" : exampleHyphen + " " + description + ":\n\n"; if (this.skipTextWrap) { exampleText = utilities_1.TextUtils.indentLines(exampleText, this.mProduceMarkdown ? "" : DefaultHelpGenerator.HELP_INDENT); } else { exampleText = utilities_1.TextUtils.wordWrap(exampleText, undefined, this.mProduceMarkdown ? "" : DefaultHelpGenerator.HELP_INDENT); } exampleText += " {{bullet}}{{space}}{{codeBegin}}$ {{space}}" + prefix + this.mRootCommandName + " " + CommandUtils_1.CommandUtils.getFullCommandName(this.mCommandDefinition, this.mDefinitionTree) + options + "{{codeEnd}}\n"; return exampleText; }).join("\n"); if (this.mCommandDefinition.examples.length > 0) { examplesText = this.buildHeader("Examples") + examplesText + "\n"; } } return this.renderHelp(examplesText); } /** * Get a blurb explaining experimental commands if this command is experimental * @returns {string} - If this command is experimental, returns the experimental command explanation block * @memberof DefaultHelpGenerator */ getExperimentalCommandSection() { if (!this.mCommandDefinition.experimental || this.mProduceMarkdown) { return ""; } let experimentalSection = ""; experimentalSection += DefaultHelpGenerator.formatHelpHeader("About Experimental Commands", undefined, this.mPrimaryHighlightColor); if (this.skipTextWrap) { experimentalSection += utilities_1.TextUtils.indentLines(this.mExperimentalCommandDescription, DefaultHelpGenerator.HELP_INDENT); } else { experimentalSection += "\n\n" + utilities_1.TextUtils.wordWrap(this.mExperimentalCommandDescription, undefined, DefaultHelpGenerator.HELP_INDENT) + "\n\n"; } return this.renderHelp(experimentalSection); } /** * Utility function to escape Markdown special characters. * Note: This should only be called once to avoid double escaping. * @param {string} text - The text to escape * @return {string} - The escaped string */ escapeMarkdown(text) { return stripAnsi(text).replace(/([*#\-`_[\]+!\\])/g, "\\$1"); } } exports.DefaultHelpGenerator = DefaultHelpGenerator; /** * The help indent for spacing/alignment * @static * @memberof DefaultHelpGenerator */ DefaultHelpGenerator.HELP_INDENT = " "; /** * Standard imperative error message tag for errors thrown by the help generator * @private * @static * @type {string} * @memberof DefaultHelpGenerator */ DefaultHelpGenerator.ERROR_TAG = "Help Generator Error:"; //# sourceMappingURL=DefaultHelpGenerator.js.map