diff --git a/Readme.md b/Readme.md index 04f38a080..892a2852a 100644 --- a/Readme.md +++ b/Readme.md @@ -16,23 +16,22 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Common option types, boolean and value](#common-option-types-boolean-and-value) - [Default option value](#default-option-value) - [Other option types, negatable boolean and boolean|value](#other-option-types-negatable-boolean-and-booleanvalue) - - [Extra option features](#extra-option-features) - - [Custom option processing](#custom-option-processing) - [Required option](#required-option) - [Variadic option](#variadic-option) - [Version option](#version-option) + - [More configuration](#more-configuration) + - [Custom option processing](#custom-option-processing) - [Commands](#commands) - [Specify the argument syntax](#specify-the-argument-syntax) - [Action handler (sub)commands](#action-handler-subcommands) - [Stand-alone executable (sub)commands](#stand-alone-executable-subcommands) - [Automated help](#automated-help) - [Custom help](#custom-help) + - [Display help from code](#display-help-from-code) - [.usage and .name](#usage-and-name) - - [.help()](#help) - - [.outputHelp()](#outputhelp) - - [.helpInformation()](#helpinformation) - [.helpOption(flags, description)](#helpoptionflags-description) - [.addHelpCommand()](#addhelpcommand) + - [More configuration](#more-configuration-1) - [Custom event listeners](#custom-event-listeners) - [Bits and pieces](#bits-and-pieces) - [.parse() and .parseAsync()](#parse-and-parseasync) @@ -208,7 +207,79 @@ add cheese type mozzarella For information about possible ambiguous cases, see [options taking varying arguments](./docs/options-taking-varying-arguments.md). -### Extra option features +### Required option + +You may specify a required (mandatory) option using `.requiredOption`. The option must have a value after parsing, usually specified on the command line, or perhaps from a default value (say from environment). The method is otherwise the same as `.option` in format, taking flags and description, and optional default value or custom processing. + +Example file: [options-required.js](./examples/options-required.js) + +```js +program + .requiredOption('-c, --cheese ', 'pizza must have cheese'); + +program.parse(process.argv); +``` + +```bash +$ pizza +error: required option '-c, --cheese ' not specified +``` + +### Variadic option + +You may make an option variadic by appending `...` to the value placeholder when declaring the option. On the command line you +can then specify multiple option-arguments, and the parsed option value will be an array. The extra arguments +are read until the first argument starting with a dash. The special argument `--` stops option processing entirely. If a value +is specified in the same argument as the option then no further values are read. + +Example file: [options-variadic.js](./examples/options-variadic.js) + +```js +program + .option('-n, --number ', 'specify numbers') + .option('-l, --letter [letters...]', 'specify letters'); + +program.parse(); + +console.log('Options: ', program.opts()); +console.log('Remaining arguments: ', program.args); +``` + +```bash +$ collect -n 1 2 3 --letter a b c +Options: { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] } +Remaining arguments: [] +$ collect --letter=A -n80 operand +Options: { number: [ '80' ], letter: [ 'A' ] } +Remaining arguments: [ 'operand' ] +$ collect --letter -n 1 -n 2 3 -- operand +Options: { number: [ '1', '2', '3' ], letter: true } +Remaining arguments: [ 'operand' ] +``` + +For information about possible ambiguous cases, see [options taking varying arguments](./docs/options-taking-varying-arguments.md). + +### Version option + +The optional `version` method adds handling for displaying the command version. The default option flags are `-V` and `--version`, and when present the command prints the version number and exits. + +```js +program.version('0.0.1'); +``` + +```bash +$ ./examples/pizza -V +0.0.1 +``` + +You may change the flags and description by passing additional parameters to the `version` method, using +the same syntax for flags as the `option` method. + +```js +program.version('0.0.1', '-v, --vers', 'output the current version'); +``` + +### More configuration You can add most options using the `.option()` method, but there are some additional features available by constructing an `Option` explicitly for less common cases. @@ -294,78 +365,6 @@ $ custom --list x,y,z [ 'x', 'y', 'z' ] ``` -### Required option - -You may specify a required (mandatory) option using `.requiredOption`. The option must have a value after parsing, usually specified on the command line, or perhaps from a default value (say from environment). The method is otherwise the same as `.option` in format, taking flags and description, and optional default value or custom processing. - -Example file: [options-required.js](./examples/options-required.js) - -```js -program - .requiredOption('-c, --cheese ', 'pizza must have cheese'); - -program.parse(process.argv); -``` - -```bash -$ pizza -error: required option '-c, --cheese ' not specified -``` - -### Variadic option - -You may make an option variadic by appending `...` to the value placeholder when declaring the option. On the command line you -can then specify multiple option-arguments, and the parsed option value will be an array. The extra arguments -are read until the first argument starting with a dash. The special argument `--` stops option processing entirely. If a value -is specified in the same argument as the option then no further values are read. - -Example file: [options-variadic.js](./examples/options-variadic.js) - -```js -program - .option('-n, --number ', 'specify numbers') - .option('-l, --letter [letters...]', 'specify letters'); - -program.parse(); - -console.log('Options: ', program.opts()); -console.log('Remaining arguments: ', program.args); -``` - -```bash -$ collect -n 1 2 3 --letter a b c -Options: { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] } -Remaining arguments: [] -$ collect --letter=A -n80 operand -Options: { number: [ '80' ], letter: [ 'A' ] } -Remaining arguments: [ 'operand' ] -$ collect --letter -n 1 -n 2 3 -- operand -Options: { number: [ '1', '2', '3' ], letter: true } -Remaining arguments: [ 'operand' ] -``` - -For information about possible ambiguous cases, see [options taking varying arguments](./docs/options-taking-varying-arguments.md). - -### Version option - -The optional `version` method adds handling for displaying the command version. The default option flags are `-V` and `--version`, and when present the command prints the version number and exits. - -```js -program.version('0.0.1'); -``` - -```bash -$ ./examples/pizza -V -0.0.1 -``` - -You may change the flags and description by passing additional parameters to the `version` method, using -the same syntax for flags as the `option` method. - -```js -program.version('0.0.1', '-v, --vers', 'output the current version'); -``` - ## Commands You can specify (sub)commands using `.command()` or `.addCommand()`. There are two ways these can be implemented: using an action handler attached to the command, or as a stand-alone executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)). @@ -582,6 +581,14 @@ The second parameter can be a string, or a function returning a string. The func - error: a boolean for whether the help is being displayed due to a usage error - command: the Command which is displaying the help +### Display help from code + +`.help()`: display help information and exit immediately. You can optionally pass `{ error: true }` to display on stderr and exit with an error status. + +`.outputHelp()`: output help information without exiting. You can optionally pass `{ error: true }` to display on stderr. + +`.helpInformation()`: get the built-in command help information as a string for processing or displaying yourself. + ### .usage and .name These allow you to customise the usage description in the first line of the help. The name is otherwise @@ -599,21 +606,9 @@ The help will start with: Usage: my-command [global options] command ``` -### .help() - -Output help information and exit immediately. You can optionally pass `{ error: true }` to display on stderr and exit with an error status. - -### .outputHelp() - -Output help information without exiting. You can optionally pass `{ error: true }` to display on stderr. - -### .helpInformation() - -Get the built-in command help information as a string for processing or displaying yourself. - ### .helpOption(flags, description) -Override the default help flags and description. Pass false to disable the built-in help option. +By default every command has a help option. Override the default help flags and description. Pass false to disable the built-in help option. ```js program @@ -622,7 +617,7 @@ program ### .addHelpCommand() -You can explicitly turn on or off the implicit help command with `.addHelpCommand()` and `.addHelpCommand(false)`. +A help command is added by default if your command has subcommands. You can explicitly turn on or off the implicit help command with `.addHelpCommand()` and `.addHelpCommand(false)`. You can both turn on and customise the help command by supplying the name and description: @@ -630,6 +625,28 @@ You can both turn on and customise the help command by supplying the name and de program.addHelpCommand('assist [command]', 'show assistance'); ``` +### More configuration + +The built-in help is formatted using the Help class. +You can configure the Help behaviour by modifying data properties and methods using `.configureHelp()`, or by subclassing using `.createHelp()` if you prefer. + +The data properties are: + +- `columns`: specify the wrap width, useful for unit tests +- `sortSubcommands`: sort the subcommands alphabetically +- `sortOptions`: sort the options alphabetically + +There are methods getting the visible lists of arguments, options, and subcommands. There are methods for formatting the items in the lists, with each item having a _term_ and _description_. Take a look at `.formatHelp()` to see how they are used. + +Example file: [configure-help.js](./examples/configure-help.js) + +``` +program.configureHelp({ + sortSubcommands: true, + subcommandTerm: (cmd) => cmd.name() // Just show the name, instead of short usage. +}); +``` + ## Custom event listeners You can execute custom actions by listening to command and option events. diff --git a/examples/configure-help.js b/examples/configure-help.js new file mode 100644 index 000000000..d1b7b0d29 --- /dev/null +++ b/examples/configure-help.js @@ -0,0 +1,24 @@ +// const commander = require('commander'); // (normal include) +const commander = require('../'); // include commander in git clone of commander repo + +const program = new commander.Command(); + +// This example shows a simple use of configureHelp. +// This is used as an example in the README. + +program.configureHelp({ + sortSubcommands: true, + subcommandTerm: (cmd) => cmd.name() // Just show the name, instead of short usage. +}); + +program.command('zebra ', 'African equines with distinctive black-and-white striped coats'); +program.command('aardvark [colour]', 'medium-sized, burrowing, nocturnal mammal'); +program + .command('beaver', 'large, semiaquatic rodent') + .option('--pond') + .option('--river'); + +program.parse(); + +// Try the following: +// node configure-help.js --help diff --git a/index.js b/index.js index 7129b2126..1aad60969 100644 --- a/index.js +++ b/index.js @@ -9,13 +9,344 @@ const fs = require('fs'); // @ts-check +// Although this is a class, methods are static in style to allow override using subclass or just functions. +class Help { + constructor() { + this.columns = process.stdout.columns || 80; + 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 args = cmd._helpCommandnameAndArgs.split(/ +/); + const helpCommand = cmd.createCommand(args.shift()) + .helpOption(false); + helpCommand.description(cmd._helpCommandDescription); + helpCommand._parseExpectedArgs(args); + visibleCommands.push(helpCommand); + } + if (this.sortSubcommands) { + visibleCommands.sort((a, b) => { + 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 = new Option(cmd._helpLongFlag, cmd._helpDescription); + } else if (!showLongHelpFlag) { + helpOption = new Option(cmd._helpShortFlag, cmd._helpDescription); + } else { + helpOption = new Option(cmd._helpFlags, cmd._helpDescription); + } + visibleOptions.push(helpOption); + } + if (this.sortOptions) { + visibleOptions.sort((a, b) => { + const compare = a.attributeName().localeCompare(b.attributeName()); + if (compare === 0) { + return (a.negate) ? +1 : -1; + } + return compare; + }); + } + return visibleOptions; + } + + /** + * Get an array of the arguments which have descriptions. + * + * @param {Command} cmd + * @returns {{ term: string, description:string }[]} + */ + + visibleArguments(cmd) { + if (cmd._argsDescription && cmd._args.length) { + return cmd._args.map((argument) => { + return { term: argument.name, description: cmd._argsDescription[argument.name] || '' }; + }, 0); + } + 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 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, argument.term.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) { + if (option.negate) { + return option.description; + } + const extraInfo = []; + if (option.argChoices) { + 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) { + extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`); + } + if (extraInfo.length > 0) { + return `${option.description} (${extraInfo.join(', ')})`; + } + return option.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 columns = helper.columns; + const itemIndentWidth = 2; + const itemSeparatorWidth = 2; + // itemIndent term itemSeparator description + const descriptionWidth = columns - termWidth - itemIndentWidth - itemSeparatorWidth; + function formatItem(term, description) { + if (description) { + return term.padEnd(termWidth + itemSeparatorWidth) + helper.wrap(description, descriptionWidth, 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(argument.term, argument.description); + }); + 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) + ); + }; + + /** + * Optionally wrap the given str to a max width of width characters per line + * while indenting with indent spaces. Do not wrap if insufficient width or + * string is manually formatted. + * + * @param {string} str + * @param {number} width + * @param {number} indent + * @return {string} + */ + + wrap(str, width, indent) { + // 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 to narrow columns (or can end up with a word per line). + const minWidth = 40; + if (width < minWidth) return str; + + const indentString = ' '.repeat(indent); + const regex = new RegExp('.{1,' + (width - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g'); + const lines = str.match(regex) || []; + return lines.map((line, i) => { + if (line.slice(-1) === '\n') { + line = line.slice(0, line.length - 1); + } + return ((i > 0 && indent) ? indentString : '') + line.trimRight(); + }).join('\n'); + } +} + class Option { /** * Initialize a new `Option` with the given `flags` and `description`. * * @param {string} flags * @param {string} [description] - * @api public */ constructor(flags, description) { @@ -47,7 +378,6 @@ class Option { * @param {any} value * @param {string} [description] * @return {Option} - * @api public */ default(value, description) { @@ -56,38 +386,11 @@ class Option { return this; }; - /** - * Calculate the full description, including defaultValue etc. - * - * @return {string} - * @api public - */ - - fullDescription() { - if (this.negate) { - return this.description; - } - const extraInfo = []; - if (this.argChoices) { - extraInfo.push( - // use stringify to match the display of the default value - `choices: ${this.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`); - } - if (this.defaultValue !== undefined) { - extraInfo.push(`default: ${this.defaultValueDescription || JSON.stringify(this.defaultValue)}`); - } - if (extraInfo.length > 0) { - return `${this.description} (${extraInfo.join(', ')})`; - } - return this.description; - }; - /** * Set the custom handler for processing CLI option arguments into option values. * * @param {Function} [fn] * @return {Option} - * @api public */ argParser(fn) { @@ -100,7 +403,6 @@ class Option { * * @param {boolean} [value] * @return {Option} - * @api public */ makeOptionMandatory(value) { @@ -113,7 +415,6 @@ class Option { * * @param {boolean} [value] * @return {Option} - * @api public */ hideHelp(value) { @@ -126,7 +427,6 @@ class Option { * Intended for use from custom argument processing functions. * * @param {string} message - * @api public */ argumentRejected(message) { throw new CommanderError(1, 'commander.optionArgumentRejected', message); @@ -137,7 +437,6 @@ class Option { * * @param {string[]} values * @return {Option} - * @api public */ choices(values) { @@ -155,7 +454,6 @@ class Option { * Return option name. * * @return {string} - * @api public */ name() { @@ -218,7 +516,6 @@ class Command extends EventEmitter { * Initialize a new `Command`. * * @param {string} [name] - * @api public */ constructor(name) { @@ -243,6 +540,8 @@ class Command extends EventEmitter { this._exitCallback = null; this._aliases = []; this._combineFlagAndOptionalValue = true; + this._description = ''; + this._argsDescription = undefined; this._hidden = false; this._hasHelpOption = true; @@ -250,10 +549,11 @@ class Command extends EventEmitter { this._helpDescription = 'display help for command'; this._helpShortFlag = '-h'; this._helpLongFlag = '--help'; - this._hasImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false + this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; + this._helpConfiguration = {}; } /** @@ -280,7 +580,6 @@ class Command extends EventEmitter { * @param {Object|string} [actionOptsOrExecDesc] - configuration options (for action), or description (for executable) * @param {Object} [execOpts] - configuration options (for executable) * @return {Command} returns new command for action handler, or `this` for executable command - * @api public */ command(nameAndArgs, actionOptsOrExecDesc, execOpts) { @@ -309,6 +608,7 @@ class Command extends EventEmitter { cmd._helpCommandName = this._helpCommandName; cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs; cmd._helpCommandDescription = this._helpCommandDescription; + cmd._helpConfiguration = this._helpConfiguration; cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; @@ -331,13 +631,38 @@ class Command extends EventEmitter { * * @param {string} [name] * @return {Command} new command - * @api public */ createCommand(name) { return new Command(name); }; + /** + * You can customise the help with a subclass of Help by overriding createHelp, + * or by overriding Help properties using configureHelp(). + * + * @return {Help} + */ + + createHelp() { + return Object.assign(new Help(), this.configureHelp()); + }; + + /** + * You can customise the help by overriding Help properties using configureHelp(), + * or with a subclass of Help by overriding createHelp(). + * + * @param {Object} [configuration] - configuration options + * @return {Command|Object} `this` command for chaining, or stored configuration + */ + + configureHelp(configuration) { + if (configuration === undefined) return this._helpConfiguration; + + this._helpConfiguration = configuration; + return this; + } + /** * Add a prepared subcommand. * @@ -346,7 +671,6 @@ class Command extends EventEmitter { * @param {Command} cmd - new subcommand * @param {Object} [opts] - configuration options * @return {Command} `this` command for chaining - * @api public */ addCommand(cmd, opts) { @@ -375,8 +699,6 @@ class Command extends EventEmitter { /** * Define argument syntax for the command. - * - * @api public */ arguments(desc) { @@ -391,14 +713,13 @@ class Command extends EventEmitter { * addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom details * * @return {Command} `this` command for chaining - * @api public */ addHelpCommand(enableOrNameAndArgs, description) { if (enableOrNameAndArgs === false) { - this._hasImplicitHelpCommand = false; + this._addImplicitHelpCommand = false; } else { - this._hasImplicitHelpCommand = true; + this._addImplicitHelpCommand = true; if (typeof enableOrNameAndArgs === 'string') { this._helpCommandName = enableOrNameAndArgs.split(' ')[0]; this._helpCommandnameAndArgs = enableOrNameAndArgs; @@ -413,11 +734,11 @@ class Command extends EventEmitter { * @api private */ - _lazyHasImplicitHelpCommand() { - if (this._hasImplicitHelpCommand === undefined) { - this._hasImplicitHelpCommand = this.commands.length && !this._actionHandler && !this._findCommand('help'); + _hasImplicitHelpCommand() { + if (this._addImplicitHelpCommand === undefined) { + return this.commands.length && !this._actionHandler && !this._findCommand('help'); } - return this._hasImplicitHelpCommand; + return this._addImplicitHelpCommand; }; /** @@ -470,7 +791,6 @@ class Command extends EventEmitter { * * @param {Function} [fn] optional callback which will be passed a CommanderError, defaults to throwing * @return {Command} `this` command for chaining - * @api public */ exitOverride(fn) { @@ -520,7 +840,6 @@ class Command extends EventEmitter { * * @param {Function} fn * @return {Command} `this` command for chaining - * @api public */ action(fn) { @@ -740,7 +1059,6 @@ Read more on https://git.io/JJc0W`); * @param {Function|*} [fn] - custom option processing function or default value * @param {*} [defaultValue] * @return {Command} `this` command for chaining - * @api public */ option(flags, description, fn, defaultValue) { @@ -758,7 +1076,6 @@ Read more on https://git.io/JJc0W`); * @param {Function|*} [fn] - custom option processing function or default value * @param {*} [defaultValue] * @return {Command} `this` command for chaining - * @api public */ requiredOption(flags, description, fn, defaultValue) { @@ -775,7 +1092,6 @@ Read more on https://git.io/JJc0W`); * .combineFlagAndOptionalValue(false) // `-fb` is treated like `-f -b` * * @param {Boolean} [arg] - if `true` or omitted, an optional value can be specified directly after the flag. - * @api public */ combineFlagAndOptionalValue(arg) { this._combineFlagAndOptionalValue = (arg === undefined) || arg; @@ -787,7 +1103,6 @@ Read more on https://git.io/JJc0W`); * * @param {Boolean} [arg] - if `true` or omitted, no error will be thrown * for unknown options. - * @api public */ allowUnknownOption(arg) { this._allowUnknownOption = (arg === undefined) || arg; @@ -800,7 +1115,6 @@ Read more on https://git.io/JJc0W`); * * @param {boolean} value * @return {Command} `this` command for chaining - * @api public */ storeOptionsAsProperties(value) { @@ -818,7 +1132,6 @@ Read more on https://git.io/JJc0W`); * * @param {boolean} value * @return {Command} `this` command for chaining - * @api public */ passCommandToAction(value) { @@ -873,7 +1186,6 @@ Read more on https://git.io/JJc0W`); * @param {Object} [parseOptions] - optionally specify style of options with from: node/user/electron * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron' * @return {Command} `this` command for chaining - * @api public */ parse(argv, parseOptions) { @@ -885,7 +1197,7 @@ Read more on https://git.io/JJc0W`); // Default to using process.argv if (argv === undefined) { argv = process.argv; - // @ts-ignore + // @ts-ignore: unknown property if (process.versions && process.versions.electron) { parseOptions.from = 'electron'; } @@ -901,7 +1213,7 @@ Read more on https://git.io/JJc0W`); userArgs = argv.slice(2); break; case 'electron': - // @ts-ignore + // @ts-ignore: unknown property if (process.defaultApp) { this._scriptPath = argv[1]; userArgs = argv.slice(2); @@ -915,9 +1227,9 @@ Read more on https://git.io/JJc0W`); default: throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`); } - // @ts-ignore + // @ts-ignore: unknown property if (!this._scriptPath && process.mainModule) { - // @ts-ignore + // @ts-ignore: unknown property this._scriptPath = process.mainModule.filename; } @@ -948,7 +1260,6 @@ Read more on https://git.io/JJc0W`); * @param {Object} [parseOptions] * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' * @return {Promise} - * @api public */ parseAsync(argv, parseOptions) { @@ -973,9 +1284,9 @@ Read more on https://git.io/JJc0W`); // Want the entry script as the reference for command name and directory for searching for other files. let scriptPath = this._scriptPath; // Fallback in case not set, due to how Command created or called. - // @ts-ignore + // @ts-ignore: unknown property if (!scriptPath && process.mainModule) { - // @ts-ignore + // @ts-ignore: unknown property scriptPath = process.mainModule.filename; } @@ -1097,7 +1408,7 @@ Read more on https://git.io/JJc0W`); if (operands && this._findCommand(operands[0])) { this._dispatchSubcommand(operands[0], operands.slice(1), unknown); - } else if (this._lazyHasImplicitHelpCommand() && operands[0] === this._helpCommandName) { + } else if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { if (operands.length === 1) { this.help(); } else { @@ -1201,7 +1512,6 @@ Read more on https://git.io/JJc0W`); * * @param {String[]} argv * @return {{operands: String[], unknown: String[]}} - * @api public */ parseOptions(argv) { @@ -1297,7 +1607,6 @@ Read more on https://git.io/JJc0W`); * Return an object containing options as key-value pairs * * @return {Object} - * @api public */ opts() { if (this._storeOptionsAsProperties) { @@ -1404,7 +1713,6 @@ Read more on https://git.io/JJc0W`); * @param {string} [flags] * @param {string} [description] * @return {this | string} `this` command for chaining, or version string if no arguments - * @api public */ version(str, flags, description) { @@ -1425,12 +1733,10 @@ Read more on https://git.io/JJc0W`); /** * Set the description to `str`. * - * @param {string} str + * @param {string} [str] * @param {Object} [argsDescription] * @return {string|Command} - * @api public */ - description(str, argsDescription) { if (str === undefined && argsDescription === undefined) return this._description; this._description = str; @@ -1445,7 +1751,6 @@ Read more on https://git.io/JJc0W`); * * @param {string} [alias] * @return {string|Command} - * @api public */ alias(alias) { @@ -1470,7 +1775,6 @@ Read more on https://git.io/JJc0W`); * * @param {string[]} [aliases] * @return {string[]|Command} - * @api public */ aliases(aliases) { @@ -1486,7 +1790,6 @@ Read more on https://git.io/JJc0W`); * * @param {string} [str] * @return {String|Command} - * @api public */ usage(str) { @@ -1511,8 +1814,7 @@ Read more on https://git.io/JJc0W`); * Get or set the name of the command * * @param {string} [str] - * @return {String|Command} - * @api public + * @return {string|Command} */ name(str) { @@ -1521,237 +1823,15 @@ Read more on https://git.io/JJc0W`); return this; }; - /** - * Return prepared commands. - * - * @return {Array} - * @api private - */ - - prepareCommands() { - const commandDetails = this.commands.filter((cmd) => { - return !cmd._hidden; - }).map((cmd) => { - const args = cmd._args.map((arg) => { - return humanReadableArgName(arg); - }).join(' '); - - return [ - cmd._name + - (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') + - (cmd.options.length ? ' [options]' : '') + - (args ? ' ' + args : ''), - cmd._description - ]; - }); - - if (this._lazyHasImplicitHelpCommand()) { - commandDetails.push([this._helpCommandnameAndArgs, this._helpCommandDescription]); - } - return commandDetails; - }; - - /** - * Return the largest command length. - * - * @return {number} - * @api private - */ - - largestCommandLength() { - const commands = this.prepareCommands(); - return commands.reduce((max, command) => { - return Math.max(max, command[0].length); - }, 0); - }; - - /** - * Return the largest option length. - * - * @return {number} - * @api private - */ - - largestOptionLength() { - const options = [].slice.call(this.options); - options.push({ - flags: this._helpFlags - }); - - return options.reduce((max, option) => { - return Math.max(max, option.flags.length); - }, 0); - }; - - /** - * Return the largest arg length. - * - * @return {number} - * @api private - */ - - largestArgLength() { - return this._args.reduce((max, arg) => { - return Math.max(max, arg.name.length); - }, 0); - }; - - /** - * Return the pad width. - * - * @return {number} - * @api private - */ - - padWidth() { - let width = this.largestOptionLength(); - if (this._argsDescription && this._args.length) { - if (this.largestArgLength() > width) { - width = this.largestArgLength(); - } - } - - if (this.commands && this.commands.length) { - if (this.largestCommandLength() > width) { - width = this.largestCommandLength(); - } - } - - return width; - }; - - /** - * Any visible options? - * - * @return {boolean} - * @api private - */ - _hasVisibleOptions() { - return this._hasHelpOption || this.options.some((option) => !option.hidden); - } - - /** - * Return help for options. - * - * @return {string} - * @api private - */ - - optionHelp() { - const width = this.padWidth(); - const columns = process.stdout.columns || 80; - const descriptionWidth = columns - width - 4; - function padOptionDetails(flags, description) { - return pad(flags, width) + ' ' + optionalWrap(description, descriptionWidth, width + 2); - }; - - // Explicit options (including version) - const visibleOptions = this.options.filter((option) => !option.hidden); - const help = visibleOptions.map((option) => { - return padOptionDetails(option.flags, option.fullDescription()); - }); - - // Implicit help - const showShortHelpFlag = this._hasHelpOption && this._helpShortFlag && !this._findOption(this._helpShortFlag); - const showLongHelpFlag = this._hasHelpOption && !this._findOption(this._helpLongFlag); - if (showShortHelpFlag || showLongHelpFlag) { - let helpFlags = this._helpFlags; - if (!showShortHelpFlag) { - helpFlags = this._helpLongFlag; - } else if (!showLongHelpFlag) { - helpFlags = this._helpShortFlag; - } - help.push(padOptionDetails(helpFlags, this._helpDescription)); - } - - return help.join('\n'); - }; - - /** - * Return command help documentation. - * - * @return {string} - * @api private - */ - - commandHelp() { - if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return ''; - - const commands = this.prepareCommands(); - const width = this.padWidth(); - - const columns = process.stdout.columns || 80; - const descriptionWidth = columns - width - 4; - - return [ - 'Commands:', - commands.map((cmd) => { - const desc = cmd[1] ? ' ' + cmd[1] : ''; - return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2); - }).join('\n').replace(/^/gm, ' '), - '' - ].join('\n'); - }; - /** * Return program help documentation. * * @return {string} - * @api public */ helpInformation() { - let desc = []; - if (this._description) { - desc = [ - this._description, - '' - ]; - - const argsDescription = this._argsDescription; - if (argsDescription && this._args.length) { - const width = this.padWidth(); - const columns = process.stdout.columns || 80; - const descriptionWidth = columns - width - 5; - desc.push('Arguments:'); - this._args.forEach((arg) => { - desc.push(' ' + pad(arg.name, width) + ' ' + wrap(argsDescription[arg.name] || '', descriptionWidth, width + 4)); - }); - desc.push(''); - } - } - - let cmdName = this._name; - if (this._aliases[0]) { - cmdName = cmdName + '|' + this._aliases[0]; - } - let parentCmdNames = ''; - for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) { - parentCmdNames = parentCmd.name() + ' ' + parentCmdNames; - } - const usage = [ - 'Usage: ' + parentCmdNames + cmdName + ' ' + this.usage(), - '' - ]; - - let cmds = []; - const commandHelp = this.commandHelp(); - if (commandHelp) cmds = [commandHelp]; - - let options = []; - if (this._hasVisibleOptions()) { - options = [ - 'Options:', - '' + this.optionHelp().replace(/^/gm, ' '), - '' - ]; - } - - return usage - .concat(desc) - .concat(options) - .concat(cmds) - .join('\n'); + const helper = this.createHelp(); + return helper.formatHelp(this, helper); }; /** @@ -1778,7 +1858,6 @@ Read more on https://git.io/JJc0W`); * Outputs built-in help, and custom text added using `.addHelpText()`. * * @param {{ error: boolean } | Function} [contextOptions] - pass {error:true} to write to stderr instead of stdout - * @api public */ outputHelp(contextOptions) { @@ -1821,7 +1900,6 @@ Read more on https://git.io/JJc0W`); * @param {string | boolean} [flags] * @param {string} [description] * @return {Command} `this` command for chaining - * @api public */ helpOption(flags, description) { @@ -1845,7 +1923,6 @@ Read more on https://git.io/JJc0W`); * Outputs built-in help, and custom text added using `.addHelpText()`. * * @param {{ error: boolean }} [contextOptions] - pass {error:true} to write to stderr instead of stdout - * @api public */ help(contextOptions) { @@ -1905,6 +1982,7 @@ exports.program = exports; // More explicit access to global command. exports.Command = Command; exports.Option = Option; exports.CommanderError = CommanderError; +exports.Help = Help; /** * Camel-case the given `flag` @@ -1920,63 +1998,6 @@ function camelcase(flag) { }); } -/** - * Pad `str` to `width`. - * - * @param {string} str - * @param {number} width - * @return {string} - * @api private - */ - -function pad(str, width) { - const len = Math.max(0, width - str.length); - return str + Array(len + 1).join(' '); -} - -/** - * Wraps the given string with line breaks at the specified width while breaking - * words and indenting every but the first line on the left. - * - * @param {string} str - * @param {number} width - * @param {number} indent - * @return {string} - * @api private - */ -function wrap(str, width, indent) { - const regex = new RegExp('.{1,' + (width - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g'); - const lines = str.match(regex) || []; - return lines.map((line, i) => { - if (line.slice(-1) === '\n') { - line = line.slice(0, line.length - 1); - } - return ((i > 0 && indent) ? Array(indent + 1).join(' ') : '') + line.trimRight(); - }).join('\n'); -} - -/** - * Optionally wrap the given str to a max width of width characters per line - * while indenting with indent spaces. Do not wrap if insufficient width or - * string is manually formatted. - * - * @param {string} str - * @param {number} width - * @param {number} indent - * @return {string} - * @api private - */ -function optionalWrap(str, width, indent) { - // 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 to narrow columns (or can end up with a word per line). - const minWidth = 40; - if (width < minWidth) return str; - - return wrap(str, width, indent); -} - /** * Output help information if help flags specified * diff --git a/package.json b/package.json index 5e52f178e..a35826945 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "typescript-lint": "eslint typings/*.ts", "test": "jest && npm run test-typings", "test-typings": "tsc -p tsconfig.json", - "typescript-checkJS": "tsc --allowJS --checkJS index.js --noEmit" + "typescript-checkJS": "tsc --allowJS --checkJS index.js --noEmit", + "test-all": "npm run test && npm run lint && npm run typescript-lint && npm run typescript-checkJS" }, "main": "index", "files": [ diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 6a94a37c5..5979c3ab0 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -129,4 +129,10 @@ describe('Command methods that should return this for chaining', () => { const result = program.addHelpText('before', 'example'); expect(result).toBe(program); }); + + test('when call .configureHelp() then returns this', () => { + const program = new Command(); + const result = program.configureHelp({ }); + expect(result).toBe(program); + }); }); diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index 4f6198c94..0a7875baa 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -1,19 +1,19 @@ const commander = require('../'); -// Note: .commandHelp is not currently documented in the README. This is a ported legacy test. +// This is a ported legacy test. -test('when program has command then appears in commandHelp', () => { +test('when program has command then appears in help', () => { const program = new commander.Command(); program .command('bare'); - const commandHelp = program.commandHelp(); + const commandHelp = program.helpInformation(); expect(commandHelp).toMatch(/Commands:\n +bare\n/); }); -test('when program has command with optional arg then appears in commandHelp', () => { +test('when program has command with optional arg then appears in help', () => { const program = new commander.Command(); program .command('bare [bare-arg]'); - const commandHelp = program.commandHelp(); + const commandHelp = program.helpInformation(); expect(commandHelp).toMatch(/Commands:\n +bare \[bare-arg\]\n/); }); diff --git a/tests/command.configureHelp.test.js b/tests/command.configureHelp.test.js new file mode 100644 index 000000000..51c119fd7 --- /dev/null +++ b/tests/command.configureHelp.test.js @@ -0,0 +1,31 @@ +const commander = require('../'); + +test('when configure program then affects program helpInformation', () => { + const program = new commander.Command(); + program.configureHelp({ formatHelp: () => { return 'custom'; } }); + expect(program.helpInformation()).toEqual('custom'); +}); + +test('when configure program then affects subcommand helpInformation', () => { + const program = new commander.Command(); + program.configureHelp({ formatHelp: () => { return 'custom'; } }); + const sub = program.command('sub'); + expect(sub.helpInformation()).toEqual('custom'); +}); + +test('when configure with unknown property then createHelp has unknown property', () => { + const program = new commander.Command(); + program.configureHelp({ mySecretValue: 'secret' }); + expect(program.createHelp().mySecretValue).toEqual('secret'); +}); + +test('when configure with unknown property then helper passed to formatHelp has unknown property', () => { + const program = new commander.Command(); + program.configureHelp({ + mySecretValue: 'secret', + formatHelp: (cmd, helper) => { + return helper.mySecretValue; + } + }); + expect(program.helpInformation()).toEqual('secret'); +}); diff --git a/tests/command.createHelp.test.js b/tests/command.createHelp.test.js new file mode 100644 index 000000000..24830ae1d --- /dev/null +++ b/tests/command.createHelp.test.js @@ -0,0 +1,18 @@ +const commander = require('../'); + +test('when override createCommand then affects help', () => { + class MyHelp extends commander.Help { + formatHelp(cmd, helper) { + return 'custom'; + } + } + + class MyCommand extends commander.Command { + createHelp() { + return Object.assign(new MyHelp(), this.configureHelp()); + }; + } + + const program = new MyCommand(); + expect(program.helpInformation()).toEqual('custom'); +}); diff --git a/tests/command.helpCommand.test.js b/tests/command.helpCommand.test.js index be2d5a5b3..f1f769845 100644 --- a/tests/command.helpCommand.test.js +++ b/tests/command.helpCommand.test.js @@ -31,9 +31,9 @@ describe('help command listed in helpInformation', () => { test('when add custom help command then custom help command', () => { const program = new commander.Command(); - program.addHelpCommand('help command', 'help description'); + program.addHelpCommand('myHelp', 'help description'); const helpInformation = program.helpInformation(); - expect(helpInformation).toMatch(/help command +help description/); + expect(helpInformation).toMatch(/myHelp +help description/); }); }); diff --git a/tests/help.columns.test.js b/tests/help.columns.test.js new file mode 100644 index 000000000..cecaa4186 --- /dev/null +++ b/tests/help.columns.test.js @@ -0,0 +1,22 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('columns', () => { + test('when default then columns from stdout', () => { + const hold = process.stdout.columns; + process.stdout.columns = 123; + const program = new commander.Command(); + const helper = program.createHelp(); + expect(helper.columns).toEqual(123); + process.stdout.columns = hold; + }); + + test('when configure columns then value from user', () => { + const program = new commander.Command(); + program.configureHelp({ columns: 321 }); + const helper = program.createHelp(); + expect(helper.columns).toEqual(321); + }); +}); diff --git a/tests/help.commandDescription.test.js b/tests/help.commandDescription.test.js new file mode 100644 index 000000000..49eb797cd --- /dev/null +++ b/tests/help.commandDescription.test.js @@ -0,0 +1,20 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('subcommandDescription', () => { + test('when program has no description then empty string', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.subcommandDescription(program)).toEqual(''); + }); + + test('when program has description then return description', () => { + const description = 'description'; + const program = new commander.Command(); + program.description(description); + const helper = new commander.Help(); + expect(helper.subcommandDescription(program)).toEqual(description); + }); +}); diff --git a/tests/help.commandTerm.test.js b/tests/help.commandTerm.test.js new file mode 100644 index 000000000..cfb003a3e --- /dev/null +++ b/tests/help.commandTerm.test.js @@ -0,0 +1,43 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +// subcommandTerm does not currently respect helpOption or ignore hidden options, so not testing those. +describe('subcommandTerm', () => { + test('when plain command then returns name', () => { + const command = new commander.Command('program'); + const helper = new commander.Help(); + expect(helper.subcommandTerm(command)).toEqual('program'); + }); + + test('when command has alias then returns name|alias', () => { + const command = new commander.Command('program') + .alias('alias'); + const helper = new commander.Help(); + expect(helper.subcommandTerm(command)).toEqual('program|alias'); + }); + + test('when command has options then returns name [options]', () => { + const command = new commander.Command('program') + .option('-a,--all'); + const helper = new commander.Help(); + expect(helper.subcommandTerm(command)).toEqual('program [options]'); + }); + + test('when command has then returns name ', () => { + const command = new commander.Command('program') + .arguments(''); + const helper = new commander.Help(); + expect(helper.subcommandTerm(command)).toEqual('program '); + }); + + test('when command has everything then returns name|alias [options] ', () => { + const command = new commander.Command('program') + .alias('alias') + .option('-a,--all') + .arguments(''); + const helper = new commander.Help(); + expect(helper.subcommandTerm(command)).toEqual('program|alias [options] '); + }); +}); diff --git a/tests/help.commandUsage.test.js b/tests/help.commandUsage.test.js new file mode 100644 index 000000000..abeaceba0 --- /dev/null +++ b/tests/help.commandUsage.test.js @@ -0,0 +1,49 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('commandUsage', () => { + test('when single program then "program [options]"', () => { + const program = new commander.Command(); + program.name('program'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('program [options]'); + }); + + test('when multi program then "program [options] [command]"', () => { + const program = new commander.Command(); + program.name('program'); + program.command('sub'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('program [options] [command]'); + }); + + test('when program has alias then usage includes alias', () => { + const program = new commander.Command(); + program + .name('program') + .alias('alias'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('program|alias [options]'); + }); + + test('when help for subcommand then usage includes hierarchy', () => { + const program = new commander.Command(); + program + .name('program'); + const sub = program.command('sub') + .name('sub'); + const helper = new commander.Help(); + expect(helper.commandUsage(sub)).toEqual('program sub [options]'); + }); + + test('when program has argument then usage includes argument', () => { + const program = new commander.Command(); + program + .name('program') + .arguments(''); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('program [options] '); + }); +}); diff --git a/tests/help.longestArgumentTermLength.test.js b/tests/help.longestArgumentTermLength.test.js new file mode 100644 index 000000000..4a0340d39 --- /dev/null +++ b/tests/help.longestArgumentTermLength.test.js @@ -0,0 +1,32 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('longestArgumentTermLength', () => { + test('when no arguments then returns zero', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.longestArgumentTermLength(program, helper)).toEqual(0); + }); + + test('when has argument description then returns argument length', () => { + const program = new commander.Command(); + program.arguments(''); + program.description('dummy', { wonder: 'wonder description' }); + const helper = new commander.Help(); + expect(helper.longestArgumentTermLength(program, helper)).toEqual('wonder'.length); + }); + + test('when has multiple argument descriptions then returns longest', () => { + const program = new commander.Command(); + program.arguments(' '); + program.description('dummy', { + alpha: 'x', + longest: 'x', + beta: 'x' + }); + const helper = new commander.Help(); + expect(helper.longestArgumentTermLength(program, helper)).toEqual('longest'.length); + }); +}); diff --git a/tests/help.longestCommandTermLength.test.js b/tests/help.longestCommandTermLength.test.js new file mode 100644 index 000000000..b63601558 --- /dev/null +++ b/tests/help.longestCommandTermLength.test.js @@ -0,0 +1,52 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('longestSubcommandTermLength', () => { + test('when no commands then returns zero', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(0); + }); + + test('when command and no help then returns length of term', () => { + const sub = new commander.Command('sub'); + const program = new commander.Command(); + program + .addHelpCommand(false) + .addCommand(sub); + const helper = new commander.Help(); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(helper.subcommandTerm(sub).length); + }); + + test('when command with arg and no help then returns length of term', () => { + const sub = new commander.Command('sub { + const longestCommandName = 'alphabet-soup '; + const program = new commander.Command(); + program + .addHelpCommand(false) + .command('before', 'desc') + .command(longestCommandName, 'desc') + .command('after', 'desc'); + const helper = new commander.Help(); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(longestCommandName.length); + }); + + test('when just help command then returns length of help term', () => { + const program = new commander.Command(); + program + .addHelpCommand(true); + const helper = new commander.Help(); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual('help [command]'.length); + }); +}); diff --git a/tests/help.longestOptionTermLength.test.js b/tests/help.longestOptionTermLength.test.js new file mode 100644 index 000000000..81731accd --- /dev/null +++ b/tests/help.longestOptionTermLength.test.js @@ -0,0 +1,30 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('longestOptionTermLength', () => { + test('when no option then returns zero', () => { + const program = new commander.Command(); + program.helpOption(false); + const helper = new commander.Help(); + expect(helper.longestOptionTermLength(program, helper)).toEqual(0); + }); + + test('when just implicit help option returns length of help flags', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.longestOptionTermLength(program, helper)).toEqual('-h, --help'.length); + }); + + test('when multiple option then returns longest length', () => { + const longestOptionFlags = '-l, --longest '; + const program = new commander.Command(); + program + .option('--before', 'optional description of flags') + .option(longestOptionFlags) + .option('--after'); + const helper = new commander.Help(); + expect(helper.longestOptionTermLength(program, helper)).toEqual(longestOptionFlags.length); + }); +}); diff --git a/tests/help.optionDescription.test.js b/tests/help.optionDescription.test.js new file mode 100644 index 000000000..e849cf196 --- /dev/null +++ b/tests/help.optionDescription.test.js @@ -0,0 +1,42 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('optionDescription', () => { + test('when option has no description then empty string', () => { + const option = new commander.Option('-a'); + const helper = new commander.Help(); + expect(helper.optionDescription(option)).toEqual(''); + }); + + test('when option has description then return description', () => { + const description = 'description'; + const option = new commander.Option('-a', description); + const helper = new commander.Help(); + expect(helper.optionDescription(option)).toEqual(description); + }); + + test('when option has default value then return description and default value', () => { + const description = 'description'; + const option = new commander.Option('-a', description).default('default'); + const helper = new commander.Help(); + expect(helper.optionDescription(option)).toEqual('description (default: "default")'); + }); + + test('when option has default value description then return description and custom default description', () => { + const description = 'description'; + const defaultValueDescription = 'custom'; + const option = new commander.Option('-a', description).default('default value', defaultValueDescription); + const helper = new commander.Help(); + expect(helper.optionDescription(option)).toEqual(`description (default: ${defaultValueDescription})`); + }); + + test('when option has choices then return description and choices', () => { + const description = 'description'; + const choices = ['one', 'two']; + const option = new commander.Option('-a', description).choices(choices); + const helper = new commander.Help(); + expect(helper.optionDescription(option)).toEqual('description (choices: "one", "two")'); + }); +}); diff --git a/tests/help.optionTerm.test.js b/tests/help.optionTerm.test.js new file mode 100644 index 000000000..73fbafe75 --- /dev/null +++ b/tests/help.optionTerm.test.js @@ -0,0 +1,34 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('optionTerm', () => { + test('when -s flags then returns flags', () => { + const flags = '-s'; + const option = new commander.Option(flags); + const helper = new commander.Help(); + expect(helper.optionTerm(option)).toEqual(flags); + }); + + test('when --short flags then returns flags', () => { + const flags = '--short'; + const option = new commander.Option(flags); + const helper = new commander.Help(); + expect(helper.optionTerm(option)).toEqual(flags); + }); + + test('when -s,--short flags then returns flags', () => { + const flags = '-s,--short'; + const option = new commander.Option(flags); + const helper = new commander.Help(); + expect(helper.optionTerm(option)).toEqual(flags); + }); + + test('when -s|--short flags then returns flags', () => { + const flags = '-s|--short'; + const option = new commander.Option(flags); + const helper = new commander.Help(); + expect(helper.optionTerm(option)).toEqual(flags); + }); +}); diff --git a/tests/help.padWidth.test.js b/tests/help.padWidth.test.js new file mode 100644 index 000000000..496e5b2e0 --- /dev/null +++ b/tests/help.padWidth.test.js @@ -0,0 +1,45 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('padWidth', () => { + test('when argument term longest return argument length', () => { + const longestThing = 'veryLongThingBiggerThanOthers'; + const program = new commander.Command(); + program + .arguments(`<${longestThing}>`) + .description('description', { veryLongThingBiggerThanOthers: 'desc' }) + .option('-o'); + program + .command('sub'); + const helper = new commander.Help(); + expect(helper.padWidth(program, helper)).toEqual(longestThing.length); + }); + + test('when option term longest return option length', () => { + const longestThing = '--very-long-thing-bigger-than-others'; + const program = new commander.Command(); + program + .arguments('') + .description('description', { file: 'desc' }) + .option(longestThing); + program + .command('sub'); + const helper = new commander.Help(); + expect(helper.padWidth(program, helper)).toEqual(longestThing.length); + }); + + test('when command term longest return command length', () => { + const longestThing = 'very-long-thing-bigger-than-others'; + const program = new commander.Command(); + program + .arguments('') + .description('description', { file: 'desc' }) + .option('-o'); + program + .command(longestThing); + const helper = new commander.Help(); + expect(helper.padWidth(program, helper)).toEqual(longestThing.length); + }); +}); diff --git a/tests/help.sortCommands.test.js b/tests/help.sortCommands.test.js new file mode 100644 index 000000000..947490d14 --- /dev/null +++ b/tests/help.sortCommands.test.js @@ -0,0 +1,29 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('sortSubcommands', () => { + test('when unsorted then commands in order added', () => { + const program = new commander.Command(); + program + .command('ccc', 'desc') + .command('aaa', 'desc') + .command('bbb', 'desc'); + const helper = program.createHelp(); + const visibleCommandNames = helper.visibleCommands(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['ccc', 'aaa', 'bbb', 'help']); + }); + + test('when sortSubcommands:true then commands sorted', () => { + const program = new commander.Command(); + program + .configureHelp({ sortSubcommands: true }) + .command('ccc', 'desc') + .command('aaa', 'desc') + .command('bbb', 'desc'); + const helper = program.createHelp(); + const visibleCommandNames = helper.visibleCommands(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'ccc', 'help']); + }); +}); diff --git a/tests/help.sortOptions.test.js b/tests/help.sortOptions.test.js new file mode 100644 index 000000000..69a502abb --- /dev/null +++ b/tests/help.sortOptions.test.js @@ -0,0 +1,67 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('sortOptions', () => { + test('when unsorted then options in order added', () => { + const program = new commander.Command(); + program + .option('--zzz', 'desc') + .option('--aaa', 'desc') + .option('--bbb', 'desc'); + const helper = program.createHelp(); + const visibleOptionNames = helper.visibleOptions(program).map(option => option.name()); + expect(visibleOptionNames).toEqual(['zzz', 'aaa', 'bbb', 'help']); + }); + + test('when sortOptions:true then options sorted alphabetically', () => { + const program = new commander.Command(); + program + .configureHelp({ sortOptions: true }) + .option('--zzz', 'desc') + .option('--aaa', 'desc') + .option('--bbb', 'desc'); + const helper = program.createHelp(); + const visibleOptionNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleOptionNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + }); + + test('when short and long flags then sort on long flag (name)', () => { + const program = new commander.Command(); + program + .configureHelp({ sortOptions: true }) + .option('-m,--zzz', 'desc') + .option('-n,--aaa', 'desc') + .option('-o,--bbb', 'desc'); + const helper = program.createHelp(); + const visibleOptionNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleOptionNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + }); + + test('when negated option with positive then sort together with negative after positive', () => { + const program = new commander.Command(); + program + .configureHelp({ sortOptions: true }) + .option('--bbb', 'desc') + .option('--ccc', 'desc') + .option('--no-bbb', 'desc') + .option('--aaa', 'desc'); + const helper = program.createHelp(); + const visibleOptionNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleOptionNames).toEqual(['aaa', 'bbb', 'no-bbb', 'ccc', 'help']); + }); + + test('when negated option without positive then still sorts using attribute name', () => { + // Sorting '--no-foo' as 'foo' (mainly for when also 'foo' so sort together)! + const program = new commander.Command(); + program + .configureHelp({ sortOptions: true }) + .option('--ccc', 'desc') + .option('--aaa', 'desc') + .option('--no-bbb', 'desc'); + const helper = program.createHelp(); + const visibleOptionNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleOptionNames).toEqual(['aaa', 'no-bbb', 'ccc', 'help']); + }); +}); diff --git a/tests/help.visibleArgumnets.test.js b/tests/help.visibleArgumnets.test.js new file mode 100644 index 000000000..98d884e18 --- /dev/null +++ b/tests/help.visibleArgumnets.test.js @@ -0,0 +1,27 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('visibleArguments', () => { + test('when no arguments then empty array', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.visibleArguments(program)).toEqual([]); + }); + + test('when argument but no argument description then empty array', () => { + const program = new commander.Command(); + program.arguments(''); + const helper = new commander.Help(); + expect(helper.visibleArguments(program)).toEqual([]); + }); + + test('when argument and argument description then returned', () => { + const program = new commander.Command(); + program.arguments(''); + program.description('dummy', { file: 'file description' }); + const helper = new commander.Help(); + expect(helper.visibleArguments(program)).toEqual([{ term: 'file', description: 'file description' }]); + }); +}); diff --git a/tests/help.visibleCommands.test.js b/tests/help.visibleCommands.test.js new file mode 100644 index 000000000..bc8aed7e0 --- /dev/null +++ b/tests/help.visibleCommands.test.js @@ -0,0 +1,33 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('visibleCommands', () => { + test('when no subcommands then empty array', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.visibleCommands(program)).toEqual([]); + }); + + test('when add command then visible (with help)', () => { + const program = new commander.Command(); + program + .command('sub'); + const helper = new commander.Help(); + const visibleCommandNames = helper.visibleCommands(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['sub', 'help']); + }); + + test('when commands hidden then not visible', () => { + const program = new commander.Command(); + program + .command('visible', 'desc') + .command('invisible executable', 'desc', { hidden: true }); + program + .command('invisible action', { hidden: true }); + const helper = new commander.Help(); + const visibleCommandNames = helper.visibleCommands(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['visible', 'help']); + }); +}); diff --git a/tests/help.visibleOptions.test.js b/tests/help.visibleOptions.test.js new file mode 100644 index 000000000..e34749460 --- /dev/null +++ b/tests/help.visibleOptions.test.js @@ -0,0 +1,70 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('visibleOptions', () => { + test('when no options then just help visible', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + const visibleOptionNames = helper.visibleOptions(program).map(option => option.name()); + expect(visibleOptionNames).toEqual(['help']); + }); + + test('when no options and no help option then empty array', () => { + const program = new commander.Command(); + program.helpOption(false); + const helper = new commander.Help(); + expect(helper.visibleOptions(program)).toEqual([]); + }); + + test('when add option then visible (with help)', () => { + const program = new commander.Command(); + program.option('-v,--visible'); + const helper = new commander.Help(); + const visibleOptionNames = helper.visibleOptions(program).map(option => option.name()); + expect(visibleOptionNames).toEqual(['visible', 'help']); + }); + + test('when option hidden then not visible', () => { + const program = new commander.Command(); + program + .option('-v,--visible') + .addOption(new commander.Option('--invisible').hideHelp()); + const helper = new commander.Help(); + const visibleOptionNames = helper.visibleOptions(program).map(option => option.name()); + expect(visibleOptionNames).toEqual(['visible', 'help']); + }); +}); + +describe('implicit help', () => { + test('when default then help term is -h, --help', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + const implicitHelp = helper.visibleOptions(program)[0]; + expect(helper.optionTerm(implicitHelp)).toEqual('-h, --help'); + }); + + test('when short flag obscured then help term is --help', () => { + const program = new commander.Command(); + program.addOption(new commander.Option('-h, --huge').hideHelp()); + const helper = new commander.Help(); + const implicitHelp = helper.visibleOptions(program)[0]; + expect(helper.optionTerm(implicitHelp)).toEqual('--help'); + }); + + test('when long flag obscured then help term is --h', () => { + const program = new commander.Command(); + program.addOption(new commander.Option('-H, --help').hideHelp()); + const helper = new commander.Help(); + const implicitHelp = helper.visibleOptions(program)[0]; + expect(helper.optionTerm(implicitHelp)).toEqual('-h'); + }); + + test('when help flags obscured then implicit help hidden', () => { + const program = new commander.Command(); + program.addOption(new commander.Option('-h, --help').hideHelp()); + const helper = new commander.Help(); + expect(helper.visibleOptions(program)).toEqual([]); + }); +}); diff --git a/tests/help.wrap.test.js b/tests/help.wrap.test.js new file mode 100644 index 000000000..0c1881ec1 --- /dev/null +++ b/tests/help.wrap.test.js @@ -0,0 +1,164 @@ +const commander = require('../'); + +// These are tests of the Help class, not of the Command help. +// There is some overlap with the higher level Command tests (which predate Help). + +describe('wrap', () => { + test('when string fits into width then no wrap', () => { + const text = 'a '.repeat(24) + 'a'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 50, 3); + expect(wrapped).toEqual(text); + }); + + test('when string exceeds width then wrap', () => { + const text = 'a '.repeat(30) + 'a'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 50, 0); + expect(wrapped).toEqual(`${'a '.repeat(24)}a +${'a '.repeat(5)}a`); + }); + + test('when string exceeds width then wrap and indent', () => { + const text = 'a '.repeat(30) + 'a'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 50, 10); + expect(wrapped).toEqual(`${'a '.repeat(24)}a +${' '.repeat(10)}${'a '.repeat(5)}a`); + }); + + test('when width < 40 then do not wrap', () => { + const text = 'a '.repeat(30) + 'a'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 39, 0); + expect(wrapped).toEqual(text); + }); + + test('when text has line breaks then respect and indent', () => { + const text = 'foo\nbar'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 50, 3); + expect(wrapped).toEqual('foo\n bar'); + }); + + test('when text already formatted with line breaks and indent then do not touch', () => { + const text = 'a '.repeat(25) + '\n ' + 'a '.repeat(25) + 'a'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 39, 0); + expect(wrapped).toEqual(text); + }); +}); + +describe('wrapping by formatHelp', () => { + // Test auto wrap and indent with some manual strings. + // Fragile tests with complete help output. + + test('when long option description then wrap and indent', () => { + const program = new commander.Command(); + program + .configureHelp({ columns: 80 }) + .option('-x --extra-long-option-switch', 'kjsahdkajshkahd kajhsd akhds kashd kajhs dkha dkh aksd ka dkha kdh kasd ka kahs dkh sdkh askdh aksd kashdk ahsd kahs dkha skdh'); + + const expectedOutput = +`Usage: [options] + +Options: + -x --extra-long-option-switch kjsahdkajshkahd kajhsd akhds kashd kajhs dkha + dkh aksd ka dkha kdh kasd ka kahs dkh sdkh + askdh aksd kashdk ahsd kahs dkha skdh + -h, --help display help for command +`; + + expect(program.helpInformation()).toBe(expectedOutput); + }); + + test('when long option description and default then wrap and indent', () => { + const program = new commander.Command(); + program + .configureHelp({ columns: 80 }) + .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); + + const expectedOutput = +`Usage: [options] + +Options: + -x --extra-long-option kjsahdkajshkahd kajhsd akhds (default: "aaa + bbb ccc ddd eee fff ggg") + -h, --help display help for command +`; + + expect(program.helpInformation()).toBe(expectedOutput); + }); + + test('when long command description then wrap and indent', () => { + const program = new commander.Command(); + program + .configureHelp({ columns: 80 }) + .option('-x --extra-long-option-switch', 'x') + .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); + + const expectedOutput = +`Usage: [options] [command] + +Options: + -x --extra-long-option-switch x + -h, --help display help for command + +Commands: + alpha Lorem mollit quis dolor ex do eu quis ad insa + a commodo esse. + help [command] display help for command +`; + + expect(program.helpInformation()).toBe(expectedOutput); + }); + + test('when not enough room then help not wrapped', () => { + // Not wrapping if less than 40 columns available for wrapping. + const program = new commander.Command(); + const commandDescription = 'description text of very long command which should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.'; + program + .configureHelp({ columns: 60 }) + .command('1234567801234567890x', commandDescription); + + const expectedOutput = +`Usage: [options] [command] + +Options: + -h, --help display help for command + +Commands: + 1234567801234567890x ${commandDescription} + help [command] display help for command +`; + + expect(program.helpInformation()).toBe(expectedOutput); + }); + + test('when option descripton preformatted then only add small indent', () => { + // #396: leave custom format alone, apart from space-space indent + const optionSpec = '-t, --time '; + const program = new commander.Command(); + program + .configureHelp({ columns: 80 }) + .option(optionSpec, `select time + +Time can also be specified using special values: + "dawn" - From night to sunrise. +`); + + const expectedOutput = +`Usage: [options] + +Options: + ${optionSpec} select time + + Time can also be specified using special values: + "dawn" - From night to sunrise. + + -h, --help display help for command +`; + + expect(program.helpInformation()).toBe(expectedOutput); + }); +}); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js deleted file mode 100644 index 0184bdd76..000000000 --- a/tests/helpwrap.test.js +++ /dev/null @@ -1,125 +0,0 @@ -const commander = require('../'); - -// Test auto wrap and indent with some manual strings. -// Fragile tests with complete help output. - -test('when long option description then wrap and indent', () => { - const oldColumns = process.stdout.columns; - process.stdout.columns = 80; - const program = new commander.Command(); - program - .option('-x --extra-long-option-switch', 'kjsahdkajshkahd kajhsd akhds kashd kajhs dkha dkh aksd ka dkha kdh kasd ka kahs dkh sdkh askdh aksd kashdk ahsd kahs dkha skdh'); - - const expectedOutput = -`Usage: [options] - -Options: - -x --extra-long-option-switch kjsahdkajshkahd kajhsd akhds kashd kajhs dkha - dkh aksd ka dkha kdh kasd ka kahs dkh sdkh - askdh aksd kashdk ahsd kahs dkha skdh - -h, --help display help for command -`; - - expect(program.helpInformation()).toBe(expectedOutput); - process.stdout.columns = oldColumns; -}); - -test('when long option description and default then wrap and indent', () => { - const oldColumns = process.stdout.columns; - process.stdout.columns = 80; - const program = new commander.Command(); - program - .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); - - const expectedOutput = -`Usage: [options] - -Options: - -x --extra-long-option kjsahdkajshkahd kajhsd akhds (default: "aaa - bbb ccc ddd eee fff ggg") - -h, --help display help for command -`; - - expect(program.helpInformation()).toBe(expectedOutput); - process.stdout.columns = oldColumns; -}); - -test('when long command description then wrap and indent', () => { - const oldColumns = process.stdout.columns; - process.stdout.columns = 80; - const program = new commander.Command(); - program - .option('-x --extra-long-option-switch', 'x') - .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); - - const expectedOutput = -`Usage: [options] [command] - -Options: - -x --extra-long-option-switch x - -h, --help display help for command - -Commands: - alpha Lorem mollit quis dolor ex do eu quis ad - insa a commodo esse. - help [command] display help for command -`; - - expect(program.helpInformation()).toBe(expectedOutput); - process.stdout.columns = oldColumns; -}); - -test('when not enough room then help not wrapped', () => { - // Not wrapping if less than 40 columns available for wrapping. - const oldColumns = process.stdout.columns; - process.stdout.columns = 60; - const program = new commander.Command(); - const commandDescription = 'description text of very long command which should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.'; - program - .command('1234567801234567890x', commandDescription); - - const expectedOutput = -`Usage: [options] [command] - -Options: - -h, --help display help for command - -Commands: - 1234567801234567890x ${commandDescription} - help [command] display help for command -`; - - expect(program.helpInformation()).toBe(expectedOutput); - process.stdout.columns = oldColumns; -}); - -test('when option descripton preformatted then only add small indent', () => { - const oldColumns = process.stdout.columns; - process.stdout.columns = 80; - // #396: leave custom format alone, apart from space-space indent - const optionSpec = '-t, --time '; - const program = new commander.Command(); - program - .option(optionSpec, `select time - -Time can also be specified using special values: - "dawn" - From night to sunrise. -`); - - const expectedOutput = -`Usage: [options] - -Options: - ${optionSpec} select time - - Time can also be specified using special values: - "dawn" - From night to sunrise. - - -h, --help display help for command -`; - - expect(program.helpInformation()).toBe(expectedOutput); - process.stdout.columns = oldColumns; -}); - -// test for argsDescription passed to command ???? diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 130c18994..a410f402f 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -244,6 +244,44 @@ myProgram.myFunction(); const mySub = myProgram.command('sub'); mySub.myFunction(); +// configureHelp + +const createHelpInstance: commander.Help = program.createHelp(); +const configureHelpThis: commander.Command = program.configureHelp({ + sortSubcommands: true, // override property + visibleCommands: (cmd: commander.Command) => [] // override method +}); +const helpConfiguration: commander.HelpConfiguration = program.configureHelp(); + +// Help +const helper = new commander.Help(); +const helperCommand = new commander.Command(); +const helperOption = new commander.Option('-a, --all'); + +helper.columns = 3; +helper.sortSubcommands = true; +helper.sortOptions = false; + +const subcommandTermStr: string = helper.subcommandTerm(helperCommand); +const commandUsageStr: string = helper.commandUsage(helperCommand); +const commandDescriptionStr: string = helper.commandDescription(helperCommand); +const subcommandDescriptionStr: string = helper.subcommandDescription(helperCommand); +const optionTermStr: string = helper.optionTerm(helperOption); +const optionDescriptionStr: string = helper.optionDescription(helperOption); + +const visibleCommands: commander.Command[] = helper.visibleCommands(helperCommand); +const visibleOptions: commander.Option[] = helper.visibleOptions(helperCommand); +const visibleArguments: Array<{ term: string; description: string}> = helper.visibleArguments(helperCommand); + +const widestCommand: number = helper.longestSubcommandTermLength(helperCommand, helper); +const widestOption: number = helper.longestOptionTermLength(helperCommand, helper); +const widestArgument: number = helper.longestArgumentTermLength(helperCommand, helper); +const widest: number = helper.padWidth(helperCommand, helper); + +const wrapped: string = helper.wrap('a b c', 50, 3); + +const formatted: string = helper.formatHelp(helperCommand, helper); + // Option methods const baseOption = new commander.Option('-f,--foo', 'foo description'); diff --git a/typings/index.d.ts b/typings/index.d.ts index a41db6740..5dea0b333 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -72,6 +72,55 @@ declare namespace commander { } type OptionConstructor = new (flags: string, description?: string) => Option; + interface Help { + /** output columns, long lines are wrapped to fit */ + columns: number; + sortSubcommands: boolean; + sortOptions: boolean; + + /** Get the command term to show in the list of subcommands. */ + subcommandTerm(cmd: Command): string; + /** Get the command description to show in the list of subcommands. */ + subcommandDescription(cmd: Command): string; + /** Get the option term to show in the list of options. */ + optionTerm(option: Option): string; + /** Get the option description to show in the list of options. */ + optionDescription(option: Option): string; + + /** Get the command usage to be displayed at the top of the built-in help. */ + commandUsage(cmd: Command): string; + /** Get the description for the command. */ + commandDescription(cmd: Command): string; + + /** Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one. */ + visibleCommands(cmd: Command): Command[]; + /** Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. */ + visibleOptions(cmd: Command): Option[]; + /** Get an array of the arguments which have descriptions. */ + visibleArguments(cmd: Command): Array<{ term: string; description: string}>; + + /** Get the longest command term length. */ + longestSubcommandTermLength(cmd: Command, helper: Help): number; + /** Get the longest option term length. */ + longestOptionTermLength(cmd: Command, helper: Help): number; + /** Get the longest argument term length. */ + longestArgumentTermLength(cmd: Command, helper: Help): number; + /** Calculate the pad width from the maximum term length. */ + padWidth(cmd: Command, helper: Help): number; + + /** + * Optionally wrap the given str to a max width of width characters per line + * while indenting with indent spaces. Do not wrap if insufficient width or + * string is manually formatted. + */ + wrap(str: string, width: number, indent: number): string; + + /** Generate the built-in help text. */ + formatHelp(cmd: Command, helper: Help): string; + } + type HelpConstructor = new () => Help; + type HelpConfiguration = Partial; + interface ParseOptions { from: 'node' | 'electron' | 'user'; } @@ -172,6 +221,20 @@ declare namespace commander { */ exitOverride(callback?: (err: CommanderError) => never|void): this; + /** + * You can customise the help with a subclass of Help by overriding createHelp, + * or by overriding Help properties using configureHelp(). + */ + createHelp(): Help; + + /** + * You can customise the help by overriding Help properties using configureHelp(), + * or with a subclass of Help by overriding createHelp(). + */ + configureHelp(configuration: HelpConfiguration): this; + /** Get configuration */ + configureHelp(): HelpConfiguration; + /** * Register callback `fn` for the command. * @@ -466,6 +529,7 @@ declare namespace commander { Command: CommandConstructor; Option: OptionConstructor; CommanderError: CommanderErrorConstructor; + Help: HelpConstructor; } }