"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.usage = void 0; // this file handles outputting usage instructions, // failures, etc. keeps logging in one place. const common_types_1 = require("./common-types"); const obj_filter_1 = require("./obj-filter"); const path = require("path"); const yerror_1 = require("./yerror"); const decamelize = require("decamelize"); const setBlocking = require("set-blocking"); const stringWidth = require("string-width"); function usage(yargs, y18n) { const __ = y18n.__; const self = {}; // methods for ouputting/building failure message. const fails = []; self.failFn = function failFn(f) { fails.push(f); }; let failMessage = null; let showHelpOnFail = true; self.showHelpOnFail = function showHelpOnFailFn(arg1 = true, arg2) { function parseFunctionArgs() { return typeof arg1 === 'string' ? [true, arg1] : [arg1, arg2]; } const [enabled, message] = parseFunctionArgs(); failMessage = message; showHelpOnFail = enabled; return self; }; let failureOutput = false; self.fail = function fail(msg, err) { const logger = yargs._getLoggerInstance(); if (fails.length) { for (let i = fails.length - 1; i >= 0; --i) { fails[i](msg, err, self); } } else { if (yargs.getExitProcess()) setBlocking(true); // don't output failure message more than once if (!failureOutput) { failureOutput = true; if (showHelpOnFail) { yargs.showHelp('error'); logger.error(); } if (msg || err) logger.error(msg || err); if (failMessage) { if (msg || err) logger.error(''); logger.error(failMessage); } } err = err || new yerror_1.YError(msg); if (yargs.getExitProcess()) { return yargs.exit(1); } else if (yargs._hasParseCallback()) { return yargs.exit(1, err); } else { throw err; } } }; // methods for ouputting/building help (usage) message. let usages = []; let usageDisabled = false; self.usage = (msg, description) => { if (msg === null) { usageDisabled = true; usages = []; return self; } usageDisabled = false; usages.push([msg, description || '']); return self; }; self.getUsage = () => { return usages; }; self.getUsageDisabled = () => { return usageDisabled; }; self.getPositionalGroupName = () => { return __('Positionals:'); }; let examples = []; self.example = (cmd, description) => { examples.push([cmd, description || '']); }; let commands = []; self.command = function command(cmd, description, isDefault, aliases, deprecated = false) { // the last default wins, so cancel out any previously set default if (isDefault) { commands = commands.map((cmdArray) => { cmdArray[2] = false; return cmdArray; }); } commands.push([cmd, description || '', isDefault, aliases, deprecated]); }; self.getCommands = () => commands; let descriptions = {}; self.describe = function describe(keyOrKeys, desc) { if (Array.isArray(keyOrKeys)) { keyOrKeys.forEach((k) => { self.describe(k, desc); }); } else if (typeof keyOrKeys === 'object') { Object.keys(keyOrKeys).forEach((k) => { self.describe(k, keyOrKeys[k]); }); } else { descriptions[keyOrKeys] = desc; } }; self.getDescriptions = () => descriptions; let epilogs = []; self.epilog = (msg) => { epilogs.push(msg); }; let wrapSet = false; let wrap; self.wrap = (cols) => { wrapSet = true; wrap = cols; }; function getWrap() { if (!wrapSet) { wrap = windowWidth(); wrapSet = true; } return wrap; } const deferY18nLookupPrefix = '__yargsString__:'; self.deferY18nLookup = str => deferY18nLookupPrefix + str; self.help = function help() { if (cachedHelpMessage) return cachedHelpMessage; normalizeAliases(); // handle old demanded API const base$0 = yargs.customScriptName ? yargs.$0 : path.basename(yargs.$0); const demandedOptions = yargs.getDemandedOptions(); const demandedCommands = yargs.getDemandedCommands(); const deprecatedOptions = yargs.getDeprecatedOptions(); const groups = yargs.getGroups(); const options = yargs.getOptions(); let keys = []; keys = keys.concat(Object.keys(descriptions)); keys = keys.concat(Object.keys(demandedOptions)); keys = keys.concat(Object.keys(demandedCommands)); keys = keys.concat(Object.keys(options.default)); keys = keys.filter(filterHiddenOptions); keys = Object.keys(keys.reduce((acc, key) => { if (key !== '_') acc[key] = true; return acc; }, {})); const theWrap = getWrap(); const ui = require('cliui')({ width: theWrap, wrap: !!theWrap }); // the usage string. if (!usageDisabled) { if (usages.length) { // user-defined usage. usages.forEach((usage) => { ui.div(`${usage[0].replace(/\$0/g, base$0)}`); if (usage[1]) { ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] }); } }); ui.div(); } else if (commands.length) { let u = null; // demonstrate how commands are used. if (demandedCommands._) { u = `${base$0} <${__('command')}>\n`; } else { u = `${base$0} [${__('command')}]\n`; } ui.div(`${u}`); } } // your application's commands, i.e., non-option // arguments populated in '_'. if (commands.length) { ui.div(__('Commands:')); const context = yargs.getContext(); const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''; if (yargs.getParserConfiguration()['sort-commands'] === true) { commands = commands.sort((a, b) => a[0].localeCompare(b[0])); } commands.forEach((command) => { const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}`; // drop $0 from default commands. ui.span({ text: commandString, padding: [0, 2, 0, 2], width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4 }, { text: command[1] }); const hints = []; if (command[2]) hints.push(`[${__('default')}]`); if (command[3] && command[3].length) { hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`); } if (command[4]) { if (typeof command[4] === 'string') { hints.push(`[${__('deprecated: %s', command[4])}]`); } else { hints.push(`[${__('deprecated')}]`); } } if (hints.length) { ui.div({ text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right' }); } else { ui.div(); } }); ui.div(); } // perform some cleanup on the keys array, making it // only include top-level keys not their aliases. const aliasKeys = (Object.keys(options.alias) || []) .concat(Object.keys(yargs.parsed.newAliases) || []); keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1)); // populate 'Options:' group with any keys that have not // explicitly had a group set. const defaultGroup = __('Options:'); if (!groups[defaultGroup]) groups[defaultGroup] = []; addUngroupedKeys(keys, options.alias, groups, defaultGroup); // display 'Options:' table along with any custom tables: Object.keys(groups).forEach((groupName) => { if (!groups[groupName].length) return; // if we've grouped the key 'f', but 'f' aliases 'foobar', // normalizedKeys should contain only 'foobar'. const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => { if (~aliasKeys.indexOf(key)) return key; for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) { if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey; } return key; }); if (normalizedKeys.length < 1) return; ui.div(groupName); // actually generate the switches string --foo, -f, --bar. const switches = normalizedKeys.reduce((acc, key) => { acc[key] = [key].concat(options.alias[key] || []) .map(sw => { // for the special positional group don't // add '--' or '-' prefix. if (groupName === self.getPositionalGroupName()) return sw; else { return ( // matches yargs-parser logic in which single-digits // aliases declared with a boolean type are now valid /^[0-9]$/.test(sw) ? ~options.boolean.indexOf(key) ? '-' : '--' : sw.length > 1 ? '--' : '-') + sw; } }) .join(', '); return acc; }, {}); normalizedKeys.forEach((key) => { const kswitch = switches[key]; let desc = descriptions[key] || ''; let type = null; if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length)); if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`; if (~options.count.indexOf(key)) type = `[${__('count')}]`; if (~options.string.indexOf(key)) type = `[${__('string')}]`; if (~options.normalize.indexOf(key)) type = `[${__('string')}]`; if (~options.array.indexOf(key)) type = `[${__('array')}]`; if (~options.number.indexOf(key)) type = `[${__('number')}]`; const deprecatedExtra = (deprecated) => typeof deprecated === 'string' ? `[${__('deprecated: %s', deprecated)}]` : `[${__('deprecated')}]`; const extra = [ (key in deprecatedOptions) ? deprecatedExtra(deprecatedOptions[key]) : null, type, (key in demandedOptions) ? `[${__('required')}]` : null, options.choices && options.choices[key] ? `[${__('choices:')} ${self.stringifiedValues(options.choices[key])}]` : null, defaultString(options.default[key], options.defaultDescription[key]) ].filter(Boolean).join(' '); ui.span({ text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4 }, desc); if (extra) ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' }); else ui.div(); }); ui.div(); }); // describe some common use-cases for your application. if (examples.length) { ui.div(__('Examples:')); examples.forEach((example) => { example[0] = example[0].replace(/\$0/g, base$0); }); examples.forEach((example) => { if (example[1] === '') { ui.div({ text: example[0], padding: [0, 2, 0, 2] }); } else { ui.div({ text: example[0], padding: [0, 2, 0, 2], width: maxWidth(examples, theWrap) + 4 }, { text: example[1] }); } }); ui.div(); } // the usage string. if (epilogs.length > 0) { const e = epilogs.map(epilog => epilog.replace(/\$0/g, base$0)).join('\n'); ui.div(`${e}\n`); } // Remove the trailing white spaces return ui.toString().replace(/\s*$/, ''); }; // return the maximum width of a string // in the left-hand column of a table. function maxWidth(table, theWrap, modifier) { let width = 0; // table might be of the form [leftColumn], // or {key: leftColumn} if (!Array.isArray(table)) { table = Object.values(table).map(v => [v]); } table.forEach((v) => { width = Math.max(stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]), width); }); // if we've enabled 'wrap' we should limit // the max-width of the left-column. if (theWrap) width = Math.min(width, parseInt((theWrap * 0.5).toString(), 10)); return width; } // make sure any options set for aliases, // are copied to the keys being aliased. function normalizeAliases() { // handle old demanded API const demandedOptions = yargs.getDemandedOptions(); const options = yargs.getOptions(); (Object.keys(options.alias) || []).forEach((key) => { options.alias[key].forEach((alias) => { // copy descriptions. if (descriptions[alias]) self.describe(key, descriptions[alias]); // copy demanded. if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias]); // type messages. if (~options.boolean.indexOf(alias)) yargs.boolean(key); if (~options.count.indexOf(alias)) yargs.count(key); if (~options.string.indexOf(alias)) yargs.string(key); if (~options.normalize.indexOf(alias)) yargs.normalize(key); if (~options.array.indexOf(alias)) yargs.array(key); if (~options.number.indexOf(alias)) yargs.number(key); }); }); } // if yargs is executing an async handler, we take a snapshot of the // help message to display on failure: let cachedHelpMessage; self.cacheHelpMessage = function () { cachedHelpMessage = this.help(); }; // however this snapshot must be cleared afterwards // not to be be used by next calls to parse self.clearCachedHelpMessage = function () { cachedHelpMessage = undefined; }; // given a set of keys, place any keys that are // ungrouped under the 'Options:' grouping. function addUngroupedKeys(keys, aliases, groups, defaultGroup) { let groupedKeys = []; let toCheck = null; Object.keys(groups).forEach((group) => { groupedKeys = groupedKeys.concat(groups[group]); }); keys.forEach((key) => { toCheck = [key].concat(aliases[key]); if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) { groups[defaultGroup].push(key); } }); return groupedKeys; } function filterHiddenOptions(key) { return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt]; } self.showHelp = (level) => { const logger = yargs._getLoggerInstance(); if (!level) level = 'error'; const emit = typeof level === 'function' ? level : logger[level]; emit(self.help()); }; self.functionDescription = (fn) => { const description = fn.name ? decamelize(fn.name, '-') : __('generated-value'); return ['(', description, ')'].join(''); }; self.stringifiedValues = function stringifiedValues(values, separator) { let string = ''; const sep = separator || ', '; const array = [].concat(values); if (!values || !array.length) return string; array.forEach((value) => { if (string.length) string += sep; string += JSON.stringify(value); }); return string; }; // format the default-value-string displayed in // the right-hand column. function defaultString(value, defaultDescription) { let string = `[${__('default:')} `; if (value === undefined && !defaultDescription) return null; if (defaultDescription) { string += defaultDescription; } else { switch (typeof value) { case 'string': string += `"${value}"`; break; case 'object': string += JSON.stringify(value); break; default: string += value; } } return `${string}]`; } // guess the width of the console window, max-width 80. function windowWidth() { const maxWidth = 80; // CI is not a TTY /* c8 ignore next 2 */ if (typeof process === 'object' && process.stdout && process.stdout.columns) { return Math.min(maxWidth, process.stdout.columns); } else { return maxWidth; } } // logic for displaying application version. let version = null; self.version = (ver) => { version = ver; }; self.showVersion = () => { const logger = yargs._getLoggerInstance(); logger.log(version); }; self.reset = function reset(localLookup) { // do not reset wrap here // do not reset fails here failMessage = null; failureOutput = false; usages = []; usageDisabled = false; epilogs = []; examples = []; commands = []; descriptions = obj_filter_1.objFilter(descriptions, k => !localLookup[k]); return self; }; const frozens = []; self.freeze = function freeze() { frozens.push({ failMessage, failureOutput, usages, usageDisabled, epilogs, examples, commands, descriptions }); }; self.unfreeze = function unfreeze() { const frozen = frozens.pop(); common_types_1.assertNotStrictEqual(frozen, undefined); ({ failMessage, failureOutput, usages, usageDisabled, epilogs, examples, commands, descriptions } = frozen); }; return self; } exports.usage = usage;