UNPKG

@zowe/imperative

Version:
667 lines 29 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. * */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CliUtils = void 0; const error_1 = require("../../error"); const constants_1 = require("../../constants"); const TextUtils_1 = require("./TextUtils"); const ProfileUtils_1 = require("../../profiles/src/utils/ProfileUtils"); const read_1 = require("read"); /** * Cli Utils contains a set of static methods/helpers that are CLI related (forming options, censoring args, etc.) * @export * @class CliUtils */ class CliUtils { /** * Get the 'dash form' of an option as it would appear in a user's command, * appending the proper number of dashes depending on the length of the option name * @param {string} optionName - e.g. my-option * @returns {string} - e.g. --my-option */ static getDashFormOfOption(optionName) { if (optionName !== undefined && optionName !== null && optionName.length >= 1) { const dashes = optionName.length > 1 ? constants_1.Constants.OPT_LONG_DASH : constants_1.Constants.OPT_SHORT_DASH; return dashes + optionName; } else { throw new error_1.ImperativeError({ msg: "A null or blank option was supplied. Please correct the option definition." }); } } /** * Copy and censor any sensitive CLI arguments before logging/printing * @param {string[]} args - The args list to censor * @returns {string[]} * @deprecated Use Censor.censorCLIArgs */ static censorCLIArgs(args) { const newArgs = JSON.parse(JSON.stringify(args)); // eslint-disable-next-line deprecation/deprecation const censoredValues = CliUtils.CENSORED_OPTIONS.map(CliUtils.getDashFormOfOption); for (const value of censoredValues) { if (args.indexOf(value) >= 0) { const valueIndex = args.indexOf(value); if (valueIndex < args.length - 1) { // eslint-disable-next-line deprecation/deprecation newArgs[valueIndex + 1] = CliUtils.CENSOR_RESPONSE; // censor the argument after the option name } } } return newArgs; } /** * Copy and censor a yargs argument object before logging * @param {yargs.Arguments} args the args to censor * @returns {yargs.Arguments} a censored copy of the arguments * @deprecated Use Censor.censorYargsArguments */ static censorYargsArguments(args) { const newArgs = JSON.parse(JSON.stringify(args)); for (const optionName of Object.keys(newArgs)) { // eslint-disable-next-line deprecation/deprecation if (CliUtils.CENSORED_OPTIONS.indexOf(optionName) >= 0) { const valueToCensor = newArgs[optionName]; // eslint-disable-next-line deprecation/deprecation newArgs[optionName] = CliUtils.CENSOR_RESPONSE; for (const checkAliasKey of Object.keys(newArgs)) { if (newArgs[checkAliasKey] === valueToCensor) { // eslint-disable-next-line deprecation/deprecation newArgs[checkAliasKey] = CliUtils.CENSOR_RESPONSE; } } } } return newArgs; } /** * Searches properties in team configuration and attempts to match the option names supplied with profile keys. * @param {Config} config - Team config API * @param {ICommandProfile} profileDef - Profile definition of invoked command * @param {ICommandArguments} args - Arguments from command line and environment * @param {(Array<ICommandOptionDefinition | ICommandPositionalDefinition>)} options - the full set of command options * for the command being processed * * @returns {*} * * @memberof CliUtils */ static getOptValuesFromConfig(config, profileDef, args, options) { // Build a list of all profile types - this will help us search the CLI // options for profiles specified by the user let allTypes = []; if (profileDef != null) { if (profileDef.required != null) allTypes = allTypes.concat(profileDef.required); if (profileDef.optional != null) allTypes = allTypes.concat(profileDef.optional); } // Build an object that contains all the options loaded from config let fromCnfg = {}; for (const profileType of allTypes) { const opt = ProfileUtils_1.ProfileUtils.getProfileOptionAndAlias(profileType)[0]; // If the config contains the requested profiles, then "remember" // that this type has been fulfilled - so that we do NOT load from // the traditional profile location const profileTypePrefix = profileType + "_"; let p = null; if (args[opt] != null && config.api.profiles.exists(args[opt])) { p = config.api.profiles.get(args[opt]); } else if (args[opt] != null && !args[opt].startsWith(profileTypePrefix) && config.api.profiles.exists(profileTypePrefix + args[opt])) { p = config.api.profiles.get(profileTypePrefix + args[opt]); } else if (args[opt] == null && config.properties.defaults[profileType] != null && config.api.profiles.exists(config.properties.defaults[profileType])) { p = config.api.profiles.defaultGet(profileType); } if (p == null && (profileDef === null || profileDef === void 0 ? void 0 : profileDef.required) != null && (profileDef === null || profileDef === void 0 ? void 0 : profileDef.required.indexOf(profileType)) >= 0) { throw new error_1.ImperativeError({ msg: `Profile of type "${profileType}" does not exist within the loaded profiles for the command and it is marked as required.`, additionalDetails: `This is an internal imperative error. ` + `Command preparation was attempting to extract option values from this profile.` }); } fromCnfg = Object.assign(Object.assign({}, p !== null && p !== void 0 ? p : {}), fromCnfg); } // Convert each property extracted from the config to the correct yargs // style cases for the command handler (kebab and camel) options.forEach((opt) => { let cases = CliUtils.getOptionFormat(opt.name); if (fromCnfg[opt.name] == null && "aliases" in opt) { // Use aliases for backwards compatibility // Search for first alias available in the profile const oldOption = opt.aliases.find(o => fromCnfg[o] != null); // Get the camel and kebab case if (oldOption) cases = CliUtils.getOptionFormat(oldOption); } const profileKebab = fromCnfg[cases.kebabCase]; const profileCamel = fromCnfg[cases.camelCase]; if ((profileCamel !== undefined || profileKebab !== undefined) && (!Object.hasOwn(args, cases.kebabCase) && !Object.hasOwn(args, cases.camelCase))) { // If both case properties are present in the profile, use the one that matches // the option name explicitly const shouldUseKebab = profileKebab !== undefined && profileCamel !== undefined ? opt.name === cases.kebabCase : profileKebab !== undefined; const value = shouldUseKebab ? profileKebab : profileCamel; const keys = CliUtils.setOptionValue(opt.name, "aliases" in opt ? opt.aliases : [], value); fromCnfg = Object.assign(Object.assign({}, fromCnfg), keys); } }); return fromCnfg; } /** * Using Object.assign(), merges objects in the order they appear in call. Object.assign() copies and overwrites * existing properties in the target object, meaning property precedence is least to most (left to right). * * See details on Object.assign() for nuance. * * @param {...any[]} args - variadic set of objects to be merged * * @returns {*} - the merged object * */ static mergeArguments(...args) { let merged = {}; args.forEach((obj) => { merged = Object.assign(Object.assign({}, merged), obj); }); return merged; } /** * Accepts the full set of command options and extracts their values from environment variables that are set. * * @param {(Array<ICommandOptionDefinition | ICommandPositionalDefinition>)} options - the full set of options * specified on the command definition. Includes both the option definitions and the positional definitions. * * @returns {ICommandArguments["args"]} - the argument style object with both camel and kebab case keys for each * option specified in environment variables. * */ static extractEnvForOptions(envPrefix, options) { let args = {}; options.forEach((opt) => { let envValue = CliUtils.getEnvValForOption(envPrefix, opt.name); if (envValue != null) { // Perform the proper conversion if necessary for the type // ENV vars are extracted as strings switch (opt.type) { // convert strings to booleans if the option is boolean type case "boolean": if (envValue.toUpperCase() === "TRUE") { envValue = true; } else if (envValue.toUpperCase() === "FALSE") { envValue = false; } break; // convert strings to numbers if the option is number type case "number": { const BASE_TEN = 10; const oldEnvValue = envValue; envValue = parseInt(envValue, BASE_TEN); // if parsing fails, we'll re-insert the original value so that the // syntax failure message is clearer if (isNaN(envValue)) { envValue = oldEnvValue; } break; } // convert to an array of strings if the type is array case "array": { envValue = this.extractArrayFromEnvValue(envValue); break; } // Do nothing for other option types default: break; } const keys = CliUtils.setOptionValue(opt.name, "aliases" in opt ? opt.aliases : [], envValue); args = Object.assign(Object.assign({}, args), keys); } }); return args; } /** * Convert an array of strings provided as an environment variable * * @param envValue String form of the array * @returns String[] based on environment variable */ static extractArrayFromEnvValue(envValue) { const regex = /(["'])(?:(?=(\\?))\2.)*?\1/g; let arr = []; let match = regex.exec(envValue); let removed = envValue; while (match != null) { removed = removed.replace(match[0], ""); const replace = match[0].replace("\\'", "'"); const trimmed = replace.replace(/(^')|('$)/g, ""); arr.push(trimmed); match = regex.exec(envValue); } removed = removed.trim(); arr = arr.concat(removed.split(/[\s\n]+/g)); return arr; } /** * Get the value of an environment variable associated with the specified option name. * The environment variable name will be formed by concatenating an environment name prefix, * and the cmdOption using underscore as the delimiter. * * The cmdOption name can be specified in camelCase or in kabab-style. * Regardless of the style, it will be converted to upper case. * We replace dashes in Kabab-style values with underscores. We replace each uppercase * character in a camelCase value with underscore and that character. * * The envPrefix will be used exactly as specified. * * Example: The values myEnv-Prefix and someOptionName would retrieve * the value of an environment variable named * myEnv-Prefix_SOME_OPTION_NAME * * @param {string} envPrefix - The prefix for environment variables for this CLI. * Our caller can use the value obtained by ImperativeConfig.instance.envVariablePrefix, * which will use the envVariablePrefix from the Imperative config object, * and will use the rootCommandName as a fallback value. * * @param {string} cmdOption - The name of the option in either camelCase or kabab-style. * * @returns {string | null} - The value of the environment variable which corresponds * to the supplied option for the supplied command. If no such environment variable * exists we return null. * * @memberof CliUtils */ static getEnvValForOption(envPrefix, cmdOption) { // Form envPrefix and cmdOption into an environment variable const envDelim = "_"; let envVarName = CliUtils.getOptionFormat(cmdOption).kebabCase; envVarName = envPrefix + envDelim + "OPT" + envDelim + envVarName.toUpperCase().replace(/-/g, envDelim); // Get the value of the environment variable if (Object.prototype.hasOwnProperty.call(process.env, envVarName)) { return process.env[envVarName]; } // no corresponding environment variable exists return null; } /** * Constructs the yargs style positional argument string. * @static * @param {boolean} positionalRequired - Indicates that this positional is required * @param {string} positionalName - The name of the positional * @returns {string} - The yargs style positional argument string (e.g. <name>); * @memberof CliUtils */ static getPositionalSyntaxString(positionalRequired, positionalName) { const leftChar = positionalRequired ? "<" : "["; const rightChar = positionalRequired ? ">" : "]"; return leftChar + positionalName + rightChar; } /** * Format the help header - normally used in help generation etc. * @static * @param {string} header * @param {string} [indent=" "] * @param {string} color * @returns {string} * @memberof CliUtils */ static formatHelpHeader(header, indent = " ", color) { if (header === undefined || header === null || header.trim().length === 0) { throw new error_1.ImperativeError({ msg: "Null or empty header provided; could not be formatted." }); } const numDashes = header.length + 1; const headerText = TextUtils_1.TextUtils.formatMessage("{{indent}}{{headerText}}\n{{indent}}{{dashes}}", { headerText: header.toUpperCase(), dashes: Array(numDashes).join("-"), indent }); return TextUtils_1.TextUtils.chalk[color](headerText); } static generateDeprecatedMessage(cmdDefinition, showWarning) { let message = ""; if (cmdDefinition.deprecatedReplacement != null) { const noNewlineInText = cmdDefinition.deprecatedReplacement.replace(/\n/g, " "); if (showWarning) message += "\n\nWarning: This " + cmdDefinition.type + " has been deprecated.\n"; if (cmdDefinition.deprecatedReplacement === "") { message += "Obsolete component. No replacement exists"; } else { message += "Recommended replacement: " + noNewlineInText; } } return message; } /** * Display a message when the command is deprecated. * @static * @param {string} handlerParms - the IHandlerParameters supplied to * a command handler's process() function. * @memberof CliUtils */ static showMsgWhenDeprecated(handlerParms) { if (handlerParms.definition.deprecatedReplacement || handlerParms.definition.deprecatedReplacement === "") { // form the command that is deprecated const oldCmd = handlerParms.positionals.join(" "); // display the message handlerParms.response.console.error("\nWarning: The command '" + oldCmd + "' is deprecated."); // Use consolidated deprecated message logic const deprecatedMessage = CliUtils.generateDeprecatedMessage(handlerParms.definition); handlerParms.response.console.error(deprecatedMessage); } } /** * Accepts an option name, and array of option aliases, and their value * and returns the arguments style object. * * @param {string} optName - The command option name, usually in kebab case (or a single word) * * @param {string[]} optAliases - An array of alias names for this option * * @param {*} value - The value to assign to the argument * * @returns {ICommandArguments["args"]} - The argument style object * * @example <caption>Create Argument Object</caption> * * CliUtils.setOptionValue("my-option", ["mo", "o"], "value"); * * // returns * { * "myOption": "value", * "my-option": "value", * "mo": "value", * "o": "value" * } * */ static setOptionValue(optName, optAliases, value) { let names = CliUtils.getOptionFormat(optName); const args = {}; args[names.camelCase] = value; args[names.kebabCase] = value; for (const optAlias of optAliases) { if (optAlias.length === 1) { // for single character aliases, set the value using the alias verbatim args[optAlias] = value; } else { names = CliUtils.getOptionFormat(optAlias); args[names.camelCase] = value; args[names.kebabCase] = value; } } return args; } /** * Sleep for the specified number of miliseconds. * @param timeInMs Number of miliseconds to sleep * * @example * // create a synchronous delay as follows: * await CliUtils.sleep(3000); */ static sleep(timeInMs) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve) => { setTimeout(resolve, timeInMs); }); }); } /** * Prompt the user with a question and wait for an answer, * but only up to the specified timeout. * * @param message The text with which we will prompt the user. * * @param opts.hideText Should we hide the text. True = display stars. * False = display text. Default = false. * * @param opts.secToWait The number of seconds that we will wait for an answer. * If not supplied, the default is 10 minutes. * If 0 is specified, we will never timeout. * Numbers larger than 3600 (1 hour) are not allowed. * * @param opts.maskChar The character that should be used to mask hidden text. * If null is specified, then no characters will be echoed back. * * @return A string containing the user's answer, or null if we timeout. * * @example * const answer = await CliUtils.readPrompt("Type your answer here: "); * if (answer === null) { * // abort the operation that you wanted to perform * } else { * // use answer in some operation * } */ static readPrompt(message, opts) { return __awaiter(this, void 0, void 0, function* () { // Ensure that we use a reasonable timeout let secToWait = (opts === null || opts === void 0 ? void 0 : opts.secToWait) || 600; // eslint-disable-line @typescript-eslint/no-magic-numbers const maxSecToWait = 3600; // 1 hour max if (secToWait > maxSecToWait || secToWait < 0) { secToWait = maxSecToWait; } let response; try { response = yield (0, read_1.read)({ input: process.stdin, output: process.stdout, terminal: true, prompt: message, silent: opts === null || opts === void 0 ? void 0 : opts.hideText, replace: opts === null || opts === void 0 ? void 0 : opts.maskChar, timeout: secToWait * 1000 // eslint-disable-line @typescript-eslint/no-magic-numbers }); if (opts === null || opts === void 0 ? void 0 : opts.hideText) { process.stdout.write("\r\n"); } } catch (err) { if (err.message === "canceled") { process.exit(2); } else if (err.message === "timed out") { return null; } else { throw err; } } return response; }); } /** * Accepts the yargs argument object and constructs the base imperative * argument object. The objects are identical to maintain compatibility with * existing CLIs and plugins, but the intent is to eventually phase out * having CLIs import anything from Yargs (types, etc). * * @param {Arguments} args - Yargs argument object * * @returns {ICommandArguments} - Imperative argument object * */ static buildBaseArgs(args) { const impArgs = Object.assign({}, args); Object.keys(impArgs).forEach((key) => { if (key !== "_" && key !== "$0" && impArgs[key] === undefined) { delete impArgs[key]; } }); return impArgs; } /** * Takes a key and converts it to both camelCase and kebab-case. * * @param key The key to transform * * @returns An object that contains the new format. * * @example <caption>Conversion of keys</caption> * * CliUtils.getOptionFormat("helloWorld"); * * // returns * const return1 = { * key: "helloWorld", * camelCase: "helloWorld", * kebabCase: "hello-world" * } * * ///////////////////////////////////////////////////// * * CliUtils.getOptionFormat("hello-world"); * * // returns * const return2 = { * key: "hello-world", * camelCase: "helloWorld", * kebabCase: "hello-world" * } * * ///////////////////////////////////////////////////// * * CliUtils.getOptionFormat("hello--------world"); * * // returns * const return3 = { * key: "hello--------world", * camelCase: "helloWorld", * kebabCase: "hello-world" * } * * ///////////////////////////////////////////////////// * * CliUtils.getOptionFormat("hello-World-"); * * // returns * const return4 = { * key: "hello-World-", * camelCase: "helloWorld", * kebabCase: "hello-world" * } */ static getOptionFormat(key) { return { camelCase: key.replace(/(-+\w?)/g, (match, p1) => { /* * Regular expression checks for 1 or more "-" characters followed by 0 or 1 word character * The last character in each match is converted to upper case and returned only if it * isn't equal to "-" * * Examples: (input -> output) * * - helloWorld -> helloWorld * - hello-world -> helloWorld * - hello--------world -> helloWorld * - hello-World- -> helloWorld */ const returnChar = p1.slice(-1).toUpperCase(); return returnChar !== "-" ? returnChar : ""; }), kebabCase: key.replace(/(-*[A-Z]|-{2,}|-$)/g, (match, p1, offset, inputString) => { /* * Regular expression matches the following: * * 1. Any string segment containing 0 or more "-" characters followed by any uppercase letter. * 2. Any string segment containing 2 or more consecutive "-" characters * 3. Any string segment where the last character is "-" * * Matches for 1. * * - "A" -> If condition 1.2 * - "-B" -> If condition 2.2 * - "------C" -> If condition 2.2 * * Matches for 2. * * - "--" -> If condition 2.1.1 * - "-------" -> If condition 2.1.1 or 2.1.2 * * 2.1.1 will be entered if the match is the last sequence of the string * 2.1.2 will be entered if the match is not the last sequence of the string * * Matches for 3. * - "-<end_of_string>" -> If condition 1.1 * * Examples: (input -> output) * * - helloWorld -> hello-world * - hello-world -> hello-world * - hello--------world -> hello-world * - hello-World- -> hello-world */ if (p1.length === 1) { // 1 if (p1 === "-") { // 1.1 // Strip trailing - return ""; } else { // 1.2 // Change "letter" to "-letter" return "-" + p1.toLowerCase(); } } else { // 2 const returnChar = p1.slice(-1); // Get the last character of the sequence if (returnChar === "-") { // 2.1 if (offset + p1.length === inputString.length) { // 2.1.1 // Strip a trailing -------- sequence return ""; } else { // 2.1.2 // Change a sequence of -------- to a - return "-"; } } else { // 2.2 // Change a sequence of "-------letter" to "-letter" return "-" + returnChar.toLowerCase(); } } }), key }; } } exports.CliUtils = CliUtils; /** * Used as the place holder when censoring arguments in messages/command output * @static * @memberof CliUtils * @deprecated Use Censor.CENSOR_RESPONSE */ CliUtils.CENSOR_RESPONSE = "****"; /** * A list of cli options/keywords that should normally be censored * @static * @memberof CliUtils * @deprecated Use Censor.CENSORED_OPTIONS */ CliUtils.CENSORED_OPTIONS = ["auth", "p", "pass", "password", "passphrase", "credentials", "authentication", "basic-auth", "basicAuth"]; //# sourceMappingURL=CliUtils.js.map