diff --git a/src/cli/colors.js b/src/cli/colors.js index bb4ea2887e..35f30376ae 100644 --- a/src/cli/colors.js +++ b/src/cli/colors.js @@ -1,58 +1,25 @@ 'use strict'; - -// override commander functions +// override commander help formatting functions // to include color styling in the output // so the CLI looks nice -const { Command } = require('commander'); - -const commandColor = 'yellow'; -const optionColor = 'cyan'; -const argColor = 'magenta'; -const subCommandColor = 'green'; -const subOptionColor = 'blue'; -const subArgColor = 'red'; - -Command.prototype.helpInformation = function () { - let desc = []; - if (this._description) { - desc = [ - ` ${this._description}`, - '', - ]; - } - - let cmdName = this._name; - if (this._alias) { - cmdName = `${cmdName} | ${this._alias}`; - } - const usage = [ - '', - ` Usage: ${cmdName[commandColor]}${' '.reset}${this.usage()}`, - '', - ]; - - let cmds = []; - const commandHelp = this.commandHelp(); - if (commandHelp) { - cmds = [commandHelp]; - } - - const options = [ - '', - ' Options:', - '', - `${this.optionHelp().replace(/^/gm, ' ')}`, - '', - ]; - - return usage - .concat(desc) - .concat(options) - .concat(cmds) - .join('\n'.reset); -}; +const { Command, Help } = require('commander'); + +const colors = [ + // depth = 0, top-level command + { + command: 'yellow', + option: 'cyan', + arg: 'magenta', + }, + // depth = 1, second-level commands + { + command: 'green', + option: 'blue', + arg: 'red', + }, +]; function humanReadableArgName(arg) { const nameOutput = arg.name + (arg.variadic === true ? '...' : ''); @@ -60,58 +27,119 @@ function humanReadableArgName(arg) { return arg.required ? `<${nameOutput}>` : `[${nameOutput}]`; } -Command.prototype.usage = function () { - const args = this._args.map(arg => humanReadableArgName(arg)); +// get depth of command +// 0 = top, 1 = subcommand of top, etc +Command.prototype.depth = function () { + if (this._depth === undefined) { + let depth = 0; + let { parent } = this; + while (parent) { depth += 1; parent = parent.parent; } - const usage = '[options]'[optionColor] + - (this.commands.length ? ' [command]' : '')[subCommandColor] + - (this._args.length ? ` ${args.join(' ')}` : '')[argColor]; - - return usage; -}; - -function pad(str, width) { - const len = Math.max(0, width - str.length); - return str + Array(len + 1).join(' '); -} - -Command.prototype.commandHelp = function () { - if (!this.commands.length) { - return ''; + this._depth = depth; } - - const commands = this.commands.filter(cmd => !cmd._noHelp).map((cmd) => { - const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); - - return [ - `${cmd._name[subCommandColor] + - (cmd._alias ? ` | ${cmd._alias}` : '')[subCommandColor] + - (cmd.options.length ? ' [options]' : '')[subOptionColor] - } ${args[subArgColor]}`, - cmd._description, - ]; - }); - - const width = commands.reduce((max, command) => Math.max(max, command[0].length), 0); - - return [ - '', - ' Commands:', - '', - commands.map((cmd) => { - const desc = cmd[1] ? ` ${cmd[1]}` : ''; - return pad(cmd[0], width) + desc; - }).join('\n').replace(/^/gm, ' '), - '', - ].join('\n'); + return this._depth; }; -Command.prototype.optionHelp = function () { - const width = this.largestOptionLength(); - - // Append the help information - return this.options - .map(option => `${pad(option.flags, width)[optionColor]} ${option.description}`) - .concat([`${pad('-h, --help', width)[optionColor]} output usage information`]) - .join('\n'); +module.exports = { + commandUsage(cmd) { + const depth = cmd.depth(); + + // Usage + let cmdName = cmd._name; + if (cmd._aliases[0]) { + cmdName = `${cmdName}|${cmd._aliases[0]}`; + } + let parentCmdNames = ''; + let parentCmd = cmd.parent; + let parentDepth = depth - 1; + while (parentCmd) { + parentCmdNames = `${parentCmd.name()[colors[parentDepth].command]} ${parentCmdNames}`; + + parentCmd = parentCmd.parent; + parentDepth -= 1; + } + + // from Command.prototype.usage() + const args = cmd._args.map(arg => humanReadableArgName(arg)[colors[depth].arg]); + const cmdUsage = [].concat( + (cmd.options.length || cmd._hasHelpOption ? '[options]'[colors[depth].option] : []), + (cmd.commands.length ? '[command]'[colors[depth + 1].command] : []), + (cmd._args.length ? args : []) + ).join(' '); + + return `${parentCmdNames}${cmdName[colors[depth].command]} ${cmdUsage}`; + }, + subcommandTerm(cmd) { + const depth = cmd.depth(); + + // 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]}` : '' + ))[colors[depth].command] + + (cmd.options.length ? ' [options]' : '')[colors[depth].option] + // simplistic check for non-help option + (args ? ` ${args}` : '')[colors[depth].arg]; + }, + longestOptionTermLength(cmd, helper) { + return Help.prototype.longestOptionTermLength.call(this, cmd, helper) + ''.red.length; + }, + longestArgumentTermLength(cmd, helper) { + return Help.prototype.longestArgumentTermLength.call(this, cmd, helper) + ''.red.length; + }, + formatHelp(cmd, helper) { + const depth = cmd.depth(); + + 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 => formatItem( + argument.term[colors[depth].arg], + argument.description + )); + if (argumentList.length > 0) { + output = output.concat(['Arguments:', formatList(argumentList), '']); + } + + // Options + const optionList = helper.visibleOptions(cmd).map(option => formatItem( + helper.optionTerm(option)[colors[depth].option], + helper.optionDescription(option) + )); + if (optionList.length > 0) { + output = output.concat(['Options:', formatList(optionList), '']); + } + + // Commands + const commandList = helper.visibleCommands(cmd).map(cmd => formatItem( + helper.subcommandTerm(cmd), + helper.subcommandDescription(cmd) + )); + if (commandList.length > 0) { + output = output.concat(['Commands:', formatList(commandList), '']); + } + + return output.join('\n'); + }, }; diff --git a/src/cli/index.js b/src/cli/index.js index fafc4857ad..e69910cf1f 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -77,6 +77,8 @@ const pkg = require('../../package.json'); const file = require('../file'); const prestart = require('../prestart'); +program.configureHelp(require('./colors')); + program .name('./nodebb') .description('Welcome to NodeBB') @@ -305,8 +307,6 @@ program } }); -require('./colors'); - if (process.argv.length === 2) { program.help(); }