From dde2535d0b0ea9835fd369b3fbae499a2e242e77 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 19:55:04 +1300 Subject: [PATCH 01/66] Start filling out HelpUtils to try pattern --- index.js | 158 ++++++++++++++++++------------ tests/command.commandHelp.test.js | 4 +- 2 files changed, 96 insertions(+), 66 deletions(-) diff --git a/index.js b/index.js index 7129b2126..2ca2265c5 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,77 @@ const fs = require('fs'); // @ts-check +// Although this is a class, treating it as just an interface and to allow flexible overrides. +class HelpUtils { + visibleCommands(cmd) { + const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); + if (cmd._lazyHasImplicitHelpCommand()) { + const helpCommand = new Command(cmd._helpCommandnameAndArgs) + .description(cmd._helpCommandDescription) + .helpOption(false); + visibleCommands.push(helpCommand); + } + return visibleCommands; + } + + /** + * Pad `str` to `width`. + * + * @param {string} str + * @param {number} width + * @return {string} + * @api private + */ + + 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 + */ + 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 + */ + optionalWrap(str, width, indent, helper) { + // 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 helper.wrap(str, width, indent); + } +} + class Option { /** * Initialize a new `Option` with the given `flags` and `description`. @@ -338,6 +409,20 @@ class Command extends EventEmitter { return new Command(name); }; + /** + * Factory routine to create a Help class of utility routines. + * + * You can override createHelpUtils to customise the Help. + * + * @param {string} [name] + * @return {Command} new command + * @api public + */ + + createHelpUtils() { + return new HelpUtils(); + }; + /** * Add a prepared subcommand. * @@ -1637,12 +1722,12 @@ Read more on https://git.io/JJc0W`); * @api private */ - optionHelp() { + optionHelp(helper) { 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); + return helper.pad(flags, width) + ' ' + helper.optionalWrap(description, descriptionWidth, width + 2, helper); }; // Explicit options (including version) @@ -1674,7 +1759,7 @@ Read more on https://git.io/JJc0W`); * @api private */ - commandHelp() { + commandHelp(helper) { if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return ''; const commands = this.prepareCommands(); @@ -1687,7 +1772,7 @@ Read more on https://git.io/JJc0W`); 'Commands:', commands.map((cmd) => { const desc = cmd[1] ? ' ' + cmd[1] : ''; - return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2); + return (desc ? helper.pad(cmd[0], width) : cmd[0]) + helper.optionalWrap(desc, descriptionWidth, width + 2, helper); }).join('\n').replace(/^/gm, ' '), '' ].join('\n'); @@ -1701,6 +1786,7 @@ Read more on https://git.io/JJc0W`); */ helpInformation() { + const helper = this.createHelpUtils(); let desc = []; if (this._description) { desc = [ @@ -1715,7 +1801,7 @@ Read more on https://git.io/JJc0W`); 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(' ' + helper.pad(arg.name, width) + ' ' + helper.wrap(argsDescription[arg.name] || '', descriptionWidth, width + 4)); }); desc.push(''); } @@ -1735,14 +1821,14 @@ Read more on https://git.io/JJc0W`); ]; let cmds = []; - const commandHelp = this.commandHelp(); + const commandHelp = this.commandHelp(helper); if (commandHelp) cmds = [commandHelp]; let options = []; if (this._hasVisibleOptions()) { options = [ 'Options:', - '' + this.optionHelp().replace(/^/gm, ' '), + '' + this.optionHelp(helper).replace(/^/gm, ' '), '' ]; } @@ -1905,6 +1991,7 @@ exports.program = exports; // More explicit access to global command. exports.Command = Command; exports.Option = Option; exports.CommanderError = CommanderError; +exports.HelpUtils = HelpUtils; /** * Camel-case the given `flag` @@ -1920,63 +2007,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/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index 4f6198c94..fcab28a6d 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -6,7 +6,7 @@ test('when program has command then appears in commandHelp', () => { const program = new commander.Command(); program .command('bare'); - const commandHelp = program.commandHelp(); + const commandHelp = program.commandHelp(program.createHelpUtils()); expect(commandHelp).toMatch(/Commands:\n +bare\n/); }); @@ -14,6 +14,6 @@ test('when program has command with optional arg then appears in commandHelp', ( const program = new commander.Command(); program .command('bare [bare-arg]'); - const commandHelp = program.commandHelp(); + const commandHelp = program.commandHelp(program.createHelpUtils()); expect(commandHelp).toMatch(/Commands:\n +bare \[bare-arg\]\n/); }); From 70038b55002f1628e822af12a766dc90091a31ae Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 20:21:26 +1300 Subject: [PATCH 02/66] Shift the largestFoo routines into helper --- index.js | 141 +++++++++++++++++++++++++++---------------------------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/index.js b/index.js index 2ca2265c5..40b2d95e5 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,72 @@ class HelpUtils { return visibleCommands; } + /* WIP */ + visibleOptions(cmd) { + const visibleOptions = cmd.options.slice(); // Hidden not added until PR #1331 lands + // 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); + } + return visibleOptions; + } + + /* WIP */ + visibleArguments() { + if (this._argsDescription && this._args.length) { + return this._args.map((argument) => { + return { term: argument.name, description: this._argsDescription[argument.name] || '' }; + }, 0); + } + return []; + } + + /* WIP: */ + commandTerm(cmd) { + // Why not just use usage?! + const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); + return cmd._name + + (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') + + (cmd.options.length ? ' [options]' : '') + // simple check for non-help option + (args ? ' ' + args : ''); + } + + largestCommandTermLength(cmd, helper) { + return helper.visibleCommands(cmd).reduce((max, command) => { + return Math.max(max, helper.commandTerm(command).length); + }, 0); + }; + + largestOptionTermLength(cmd, helper) { + return helper.visibleOptions(cmd).reduce((max, option) => { + return Math.max(max, option.flags.length); + }, 0); + }; + + largestArgLength(cmd, helper) { + return helper.visibleArguments(cmd).reduce((max, argument) => { + return Math.max(max, argument.term.length); + }, 0); + }; + + padWidth(cmd, helper) { + return Math.max( + helper.largestOptionTermLength(cmd, helper), + helper.largestCommandTermLength(cmd, helper), + helper.largestArgLength(cmd, helper) + ); + }; + /** * Pad `str` to `width`. * @@ -1636,75 +1702,6 @@ Read more on https://git.io/JJc0W`); 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? * @@ -1723,7 +1720,7 @@ Read more on https://git.io/JJc0W`); */ optionHelp(helper) { - const width = this.padWidth(); + const width = helper.padWidth(this, helper); const columns = process.stdout.columns || 80; const descriptionWidth = columns - width - 4; function padOptionDetails(flags, description) { @@ -1763,7 +1760,7 @@ Read more on https://git.io/JJc0W`); if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return ''; const commands = this.prepareCommands(); - const width = this.padWidth(); + const width = helper.padWidth(this, helper); const columns = process.stdout.columns || 80; const descriptionWidth = columns - width - 4; @@ -1796,7 +1793,7 @@ Read more on https://git.io/JJc0W`); const argsDescription = this._argsDescription; if (argsDescription && this._args.length) { - const width = this.padWidth(); + const width = helper.padWidth(this, helper); const columns = process.stdout.columns || 80; const descriptionWidth = columns - width - 5; desc.push('Arguments:'); From 9c08b355c6fd7305f8a509ac61f6a649208bcf3b Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 21:00:22 +1300 Subject: [PATCH 03/66] Update generation of Commands section of help --- index.js | 52 ++++++++++++++----------------- tests/command.commandHelp.test.js | 8 ++--- tests/helpwrap.test.js | 4 +-- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/index.js b/index.js index 40b2d95e5..75fe16e90 100644 --- a/index.js +++ b/index.js @@ -1749,32 +1749,6 @@ Read more on https://git.io/JJc0W`); return help.join('\n'); }; - /** - * Return command help documentation. - * - * @return {string} - * @api private - */ - - commandHelp(helper) { - if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return ''; - - const commands = this.prepareCommands(); - const width = helper.padWidth(this, helper); - - const columns = process.stdout.columns || 80; - const descriptionWidth = columns - width - 4; - - return [ - 'Commands:', - commands.map((cmd) => { - const desc = cmd[1] ? ' ' + cmd[1] : ''; - return (desc ? helper.pad(cmd[0], width) : cmd[0]) + helper.optionalWrap(desc, descriptionWidth, width + 2, helper); - }).join('\n').replace(/^/gm, ' '), - '' - ].join('\n'); - }; - /** * Return program help documentation. * @@ -1784,6 +1758,19 @@ Read more on https://git.io/JJc0W`); helpInformation() { const helper = this.createHelpUtils(); + const width = helper.padWidth(this, helper); + const columns = process.stdout.columns || 80; + const descriptionWidth = columns - width - 4; + function formatItem(term, description) { + if (description) { + return helper.pad(term, width) + ' ' + helper.optionalWrap(description, descriptionWidth, width + 2, helper); + } + return term; + }; + function formatList(text) { + return text.join('\n').replace(/^/gm, ' '); + } + let desc = []; if (this._description) { desc = [ @@ -1817,9 +1804,16 @@ Read more on https://git.io/JJc0W`); '' ]; - let cmds = []; - const commandHelp = this.commandHelp(helper); - if (commandHelp) cmds = [commandHelp]; + // Commands + let cmds = []; // temp + const visibleCommands = helper.visibleCommands(this); + if (visibleCommands.length) { + const commandList = visibleCommands.map((cmd) => { + return formatItem(helper.commandTerm(cmd), cmd.description()); + }); + // output = output.concat(['Commands:', formatList(commandList), '']); + cmds = ['Commands:', formatList(commandList), '']; + } let options = []; if (this._hasVisibleOptions()) { diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index fcab28a6d..8a0199e1e 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -2,18 +2,18 @@ const commander = require('../'); // Note: .commandHelp is not currently documented in the README. 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(program.createHelpUtils()); + 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(program.createHelpUtils()); + const commandHelp = program.helpInformation(); expect(commandHelp).toMatch(/Commands:\n +bare \[bare-arg\]\n/); }); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 0184bdd76..d73be19b4 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -60,8 +60,8 @@ Options: -h, --help display help for command Commands: - alpha Lorem mollit quis dolor ex do eu quis ad - insa a commodo esse. + alpha Lorem mollit quis dolor ex do eu quis ad insa + a commodo esse. help [command] display help for command `; From 59d988e76e59e99cad4496f9f9da76ed60ba14f4 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 21:25:39 +1300 Subject: [PATCH 04/66] Rework helpInformation in consistent new style --- index.js | 78 +++++++++++++++++++++++--------------------------------- 1 file changed, 32 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index 75fe16e90..3059f2d02 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,7 @@ class HelpUtils { /* WIP */ visibleOptions(cmd) { - const visibleOptions = cmd.options.slice(); // Hidden not added until PR #1331 lands + 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); @@ -43,10 +43,10 @@ class HelpUtils { } /* WIP */ - visibleArguments() { - if (this._argsDescription && this._args.length) { - return this._args.map((argument) => { - return { term: argument.name, description: this._argsDescription[argument.name] || '' }; + visibleArguments(cmd) { + if (cmd._argsDescription && cmd._args.length) { + return cmd._args.map((argument) => { + return { term: argument.name, description: cmd._argsDescription[argument.name] || '' }; }, 0); } return []; @@ -1771,26 +1771,7 @@ Read more on https://git.io/JJc0W`); return text.join('\n').replace(/^/gm, ' '); } - let desc = []; - if (this._description) { - desc = [ - this._description, - '' - ]; - - const argsDescription = this._argsDescription; - if (argsDescription && this._args.length) { - const width = helper.padWidth(this, helper); - const columns = process.stdout.columns || 80; - const descriptionWidth = columns - width - 5; - desc.push('Arguments:'); - this._args.forEach((arg) => { - desc.push(' ' + helper.pad(arg.name, width) + ' ' + helper.wrap(argsDescription[arg.name] || '', descriptionWidth, width + 4)); - }); - desc.push(''); - } - } - + // Usage let cmdName = this._name; if (this._aliases[0]) { cmdName = cmdName + '|' + this._aliases[0]; @@ -1799,36 +1780,41 @@ Read more on https://git.io/JJc0W`); for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) { parentCmdNames = parentCmd.name() + ' ' + parentCmdNames; } - const usage = [ - 'Usage: ' + parentCmdNames + cmdName + ' ' + this.usage(), - '' - ]; + let output = ['Usage: ' + parentCmdNames + cmdName + ' ' + this.usage(), '']; + + // Description + if (this._description) { + output = output.concat([this._description, '']); + } + + // Arguments + const visibleArguments = helper.visibleArguments(this); + if (visibleArguments.length) { + const argumentsList = visibleArguments.map((argument) => { + return formatItem(argument.term, argument.description); + }); + output = output.concat(['Arguments:', formatList(argumentsList), '']); + } + + // Optioms + const visibleOptions = helper.visibleOptions(this); + if (visibleOptions.length) { + const optionList = visibleOptions.map((option) => { + return formatItem(option.flags, option.fullDescription()); + }); + output = output.concat(['Options:', formatList(optionList), '']); + } // Commands - let cmds = []; // temp const visibleCommands = helper.visibleCommands(this); if (visibleCommands.length) { const commandList = visibleCommands.map((cmd) => { return formatItem(helper.commandTerm(cmd), cmd.description()); }); - // output = output.concat(['Commands:', formatList(commandList), '']); - cmds = ['Commands:', formatList(commandList), '']; - } - - let options = []; - if (this._hasVisibleOptions()) { - options = [ - 'Options:', - '' + this.optionHelp(helper).replace(/^/gm, ' '), - '' - ]; + output = output.concat(['Commands:', formatList(commandList), '']); } - return usage - .concat(desc) - .concat(options) - .concat(cmds) - .join('\n'); + return output.join('\n'); }; /** From fe26f381f9489409659f58846b9105081d0f3a54 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 21:34:03 +1300 Subject: [PATCH 05/66] Remove unused routines --- index.js | 77 -------------------------------------------------------- 1 file changed, 77 deletions(-) diff --git a/index.js b/index.js index 3059f2d02..8f2d45fa2 100644 --- a/index.js +++ b/index.js @@ -1672,83 +1672,6 @@ 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; - }; - - /** - * 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(helper) { - const width = helper.padWidth(this, helper); - const columns = process.stdout.columns || 80; - const descriptionWidth = columns - width - 4; - function padOptionDetails(flags, description) { - return helper.pad(flags, width) + ' ' + helper.optionalWrap(description, descriptionWidth, width + 2, helper); - }; - - // 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 program help documentation. * From f8c5cb231cdd0ceec6b51d9dedc8408b6a0dc89e Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 21:55:40 +1300 Subject: [PATCH 06/66] Make columns part of HelpUtils --- index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 8f2d45fa2..31f9816cd 100644 --- a/index.js +++ b/index.js @@ -9,8 +9,13 @@ const fs = require('fs'); // @ts-check -// Although this is a class, treating it as just an interface and to allow flexible overrides. +// Although this is a class, methods are static in style to allow override using subclass or arrow functions. +// (Need to reconcile what is private when decide public/private methods????) class HelpUtils { + columns() { + return process.stdout.columns || 80; + } + visibleCommands(cmd) { const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); if (cmd._lazyHasImplicitHelpCommand()) { @@ -1682,7 +1687,7 @@ Read more on https://git.io/JJc0W`); helpInformation() { const helper = this.createHelpUtils(); const width = helper.padWidth(this, helper); - const columns = process.stdout.columns || 80; + const columns = helper.columns(); const descriptionWidth = columns - width - 4; function formatItem(term, description) { if (description) { From 720b3824e0b7adba0effe68721f5c131a633c9b6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 22:11:11 +1300 Subject: [PATCH 07/66] Offer a light weight override to HelpUtils --- index.js | 13 +++++++++---- tests/helpwrap.test.js | 20 +++++--------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 31f9816cd..ce015df66 100644 --- a/index.js +++ b/index.js @@ -396,6 +396,7 @@ class Command extends EventEmitter { this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; + this._helpUtilOverrides = {}; } /** @@ -481,9 +482,8 @@ class Command extends EventEmitter { }; /** - * Factory routine to create a Help class of utility routines. - * - * You can override createHelpUtils to customise the Help. + * You can customise the help with eitehr a subclass by overriding createHelpUtils, + * or by supplying routines using helpOverrides. * * @param {string} [name] * @return {Command} new command @@ -491,9 +491,14 @@ class Command extends EventEmitter { */ createHelpUtils() { - return new HelpUtils(); + return Object.assign(new HelpUtils(), this._helpUtilOverrides); }; + helpUtilOverrides(overrides) { + this._helpUtilOverrides = overrides; + return this; + } + /** * Add a prepared subcommand. * diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index d73be19b4..7379e4835 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -4,10 +4,9 @@ const commander = require('../'); // 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 + .helpUtilOverrides({ 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 = @@ -21,14 +20,12 @@ Options: `; 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 + .helpUtilOverrides({ columns: () => 80 }) .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); const expectedOutput = @@ -41,14 +38,12 @@ Options: `; 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 + .helpUtilOverrides({ columns: () => 80 }) .option('-x --extra-long-option-switch', 'x') .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); @@ -66,16 +61,14 @@ Commands: `; 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 + .helpUtilOverrides({ columns: () => 60 }) .command('1234567801234567890x', commandDescription); const expectedOutput = @@ -90,16 +83,14 @@ Commands: `; 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 + .helpUtilOverrides({ columns: () => 80 }) .option(optionSpec, `select time Time can also be specified using special values: @@ -119,7 +110,6 @@ Options: `; expect(program.helpInformation()).toBe(expectedOutput); - process.stdout.columns = oldColumns; }); // test for argsDescription passed to command ???? From 65edc7a2fde4dbdaba76319ac8ea87a42d635adf Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 22:22:40 +1300 Subject: [PATCH 08/66] Tweak comment --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ce015df66..c32776403 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const fs = require('fs'); // @ts-check -// Although this is a class, methods are static in style to allow override using subclass or arrow functions. +// Although this is a class, methods are static in style to allow override using subclass or just functions. // (Need to reconcile what is private when decide public/private methods????) class HelpUtils { columns() { From 08cc5061928d7768a24936bf4d0260cc2a79ba5f Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 22:35:55 +1300 Subject: [PATCH 09/66] Add chain test for helpUtilOverrides --- tests/command.chain.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 6a94a37c5..12c842139 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 .helpUtilOverrides() then returns this', () => { + const program = new Command(); + const result = program.helpUtilOverrides({ }); + expect(result).toBe(program); + }); }); From c81b32293d9e1423a1cfc727bbd3b13aea6fca18 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 22:59:28 +1300 Subject: [PATCH 10/66] Add itemIndent as another proof of concept of allowing overrides --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index c32776403..bd61e46d6 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,10 @@ class HelpUtils { return process.stdout.columns || 80; } + itemIndent() { + return ' '; + } + visibleCommands(cmd) { const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); if (cmd._lazyHasImplicitHelpCommand()) { @@ -1701,7 +1705,7 @@ Read more on https://git.io/JJc0W`); return term; }; function formatList(text) { - return text.join('\n').replace(/^/gm, ' '); + return text.join('\n').replace(/^/gm, helper.itemIndent()); } // Usage From 3b8fa89228a1a7371fbe094d0d76ecc5d403f5ea Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 23:04:11 +1300 Subject: [PATCH 11/66] Avoid Utils contraction --- index.js | 18 +++++++++--------- tests/command.chain.test.js | 4 ++-- tests/helpwrap.test.js | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index bd61e46d6..550545c19 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,7 @@ const fs = require('fs'); // Although this is a class, methods are static in style to allow override using subclass or just functions. // (Need to reconcile what is private when decide public/private methods????) -class HelpUtils { +class HelpTools { columns() { return process.stdout.columns || 80; } @@ -400,7 +400,7 @@ class Command extends EventEmitter { this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; - this._helpUtilOverrides = {}; + this._helpToolsOverrides = {}; } /** @@ -486,7 +486,7 @@ class Command extends EventEmitter { }; /** - * You can customise the help with eitehr a subclass by overriding createHelpUtils, + * You can customise the help with eitehr a subclass by overriding createHelpTools, * or by supplying routines using helpOverrides. * * @param {string} [name] @@ -494,12 +494,12 @@ class Command extends EventEmitter { * @api public */ - createHelpUtils() { - return Object.assign(new HelpUtils(), this._helpUtilOverrides); + createHelpTools() { + return Object.assign(new HelpTools(), this._helpToolsOverrides); }; - helpUtilOverrides(overrides) { - this._helpUtilOverrides = overrides; + helpToolsOverrides(overrides) { + this._helpToolsOverrides = overrides; return this; } @@ -1694,7 +1694,7 @@ Read more on https://git.io/JJc0W`); */ helpInformation() { - const helper = this.createHelpUtils(); + const helper = this.createHelpTools(); const width = helper.padWidth(this, helper); const columns = helper.columns(); const descriptionWidth = columns - width - 4; @@ -1905,7 +1905,7 @@ exports.program = exports; // More explicit access to global command. exports.Command = Command; exports.Option = Option; exports.CommanderError = CommanderError; -exports.HelpUtils = HelpUtils; +exports.HelpTools = HelpTools; /** * Camel-case the given `flag` diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 12c842139..be707cf96 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -130,9 +130,9 @@ describe('Command methods that should return this for chaining', () => { expect(result).toBe(program); }); - test('when call .helpUtilOverrides() then returns this', () => { + test('when call .helpToolsOverrides() then returns this', () => { const program = new Command(); - const result = program.helpUtilOverrides({ }); + const result = program.helpToolsOverrides({ }); expect(result).toBe(program); }); }); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 7379e4835..ed1ff55d2 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -6,7 +6,7 @@ const commander = require('../'); test('when long option description then wrap and indent', () => { const program = new commander.Command(); program - .helpUtilOverrides({ columns: () => 80 }) + .helpToolsOverrides({ 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 = @@ -25,7 +25,7 @@ Options: test('when long option description and default then wrap and indent', () => { const program = new commander.Command(); program - .helpUtilOverrides({ columns: () => 80 }) + .helpToolsOverrides({ columns: () => 80 }) .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); const expectedOutput = @@ -43,7 +43,7 @@ Options: test('when long command description then wrap and indent', () => { const program = new commander.Command(); program - .helpUtilOverrides({ columns: () => 80 }) + .helpToolsOverrides({ columns: () => 80 }) .option('-x --extra-long-option-switch', 'x') .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); @@ -68,7 +68,7 @@ test('when not enough room then help not wrapped', () => { 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 - .helpUtilOverrides({ columns: () => 60 }) + .helpToolsOverrides({ columns: () => 60 }) .command('1234567801234567890x', commandDescription); const expectedOutput = @@ -90,7 +90,7 @@ test('when option descripton preformatted then only add small indent', () => { const optionSpec = '-t, --time '; const program = new commander.Command(); program - .helpUtilOverrides({ columns: () => 80 }) + .helpToolsOverrides({ columns: () => 80 }) .option(optionSpec, `select time Time can also be specified using special values: From fc5e4da7517b247fd2ea9b994ab7c8ad0a84040e Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Sep 2020 23:04:53 +1300 Subject: [PATCH 12/66] Update comments --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 550545c19..b8de415ee 100644 --- a/index.js +++ b/index.js @@ -486,8 +486,8 @@ class Command extends EventEmitter { }; /** - * You can customise the help with eitehr a subclass by overriding createHelpTools, - * or by supplying routines using helpOverrides. + * You can customise the help with either a subclass by overriding createHelpTools, + * or by supplying routines using helpToolsOverrides. * * @param {string} [name] * @return {Command} new command From 9d1cc4d625c419623e469b82119bf34c75a7fec6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 18:48:46 +1300 Subject: [PATCH 13/66] Switch columns from function to data property --- index.js | 6 +++--- tests/helpwrap.test.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index b8de415ee..ab8dbbb07 100644 --- a/index.js +++ b/index.js @@ -12,8 +12,8 @@ const fs = require('fs'); // Although this is a class, methods are static in style to allow override using subclass or just functions. // (Need to reconcile what is private when decide public/private methods????) class HelpTools { - columns() { - return process.stdout.columns || 80; + constructor() { + this.columns = process.stdout.columns || 80; } itemIndent() { @@ -1696,7 +1696,7 @@ Read more on https://git.io/JJc0W`); helpInformation() { const helper = this.createHelpTools(); const width = helper.padWidth(this, helper); - const columns = helper.columns(); + const columns = helper.columns; const descriptionWidth = columns - width - 4; function formatItem(term, description) { if (description) { diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index ed1ff55d2..0c4db5f55 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -6,7 +6,7 @@ const commander = require('../'); test('when long option description then wrap and indent', () => { const program = new commander.Command(); program - .helpToolsOverrides({ columns: () => 80 }) + .helpToolsOverrides({ 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 = @@ -25,7 +25,7 @@ Options: test('when long option description and default then wrap and indent', () => { const program = new commander.Command(); program - .helpToolsOverrides({ columns: () => 80 }) + .helpToolsOverrides({ columns: 80 }) .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); const expectedOutput = @@ -43,7 +43,7 @@ Options: test('when long command description then wrap and indent', () => { const program = new commander.Command(); program - .helpToolsOverrides({ columns: () => 80 }) + .helpToolsOverrides({ columns: 80 }) .option('-x --extra-long-option-switch', 'x') .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); @@ -68,7 +68,7 @@ test('when not enough room then help not wrapped', () => { 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 - .helpToolsOverrides({ columns: () => 60 }) + .helpToolsOverrides({ columns: 60 }) .command('1234567801234567890x', commandDescription); const expectedOutput = @@ -90,7 +90,7 @@ test('when option descripton preformatted then only add small indent', () => { const optionSpec = '-t, --time '; const program = new commander.Command(); program - .helpToolsOverrides({ columns: () => 80 }) + .helpToolsOverrides({ columns: 80 }) .option(optionSpec, `select time Time can also be specified using special values: From 965b7f9955252683a294296e357360a6488b7d72 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 18:50:33 +1300 Subject: [PATCH 14/66] Remove itemIndent(), not useful enough alone or as a pattern for now --- index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/index.js b/index.js index ab8dbbb07..1452fc3bd 100644 --- a/index.js +++ b/index.js @@ -16,10 +16,6 @@ class HelpTools { this.columns = process.stdout.columns || 80; } - itemIndent() { - return ' '; - } - visibleCommands(cmd) { const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); if (cmd._lazyHasImplicitHelpCommand()) { @@ -1705,7 +1701,7 @@ Read more on https://git.io/JJc0W`); return term; }; function formatList(text) { - return text.join('\n').replace(/^/gm, helper.itemIndent()); + return text.join('\n').replace(/^/gm, ' '); } // Usage From 8a2bbfa57bfb59cfdae15309f0171426adf934ab Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 18:53:10 +1300 Subject: [PATCH 15/66] Make _helpToolsOverrides inherited --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 1452fc3bd..34140c68d 100644 --- a/index.js +++ b/index.js @@ -452,6 +452,7 @@ class Command extends EventEmitter { cmd._helpCommandName = this._helpCommandName; cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs; cmd._helpCommandDescription = this._helpCommandDescription; + cmd._helpToolsOverrides = this._helpToolsOverrides; cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; From fa73c6c8290f57933657284df40dc9225ad1d9f6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 19:44:15 +1300 Subject: [PATCH 16/66] Improve naming for termWidth --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 34140c68d..b718151e6 100644 --- a/index.js +++ b/index.js @@ -1692,12 +1692,12 @@ Read more on https://git.io/JJc0W`); helpInformation() { const helper = this.createHelpTools(); - const width = helper.padWidth(this, helper); + const termWidth = helper.padWidth(this, helper); const columns = helper.columns; - const descriptionWidth = columns - width - 4; + const descriptionWidth = columns - termWidth - 4; function formatItem(term, description) { if (description) { - return helper.pad(term, width) + ' ' + helper.optionalWrap(description, descriptionWidth, width + 2, helper); + return helper.pad(term, termWidth) + ' ' + helper.optionalWrap(description, descriptionWidth, termWidth + 2, helper); } return term; }; From c91c5158f694eab1b2c0d7f521c495ff13fc834f Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 20:51:55 +1300 Subject: [PATCH 17/66] Move usage into HelpTools --- index.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index b718151e6..ff0f60f8b 100644 --- a/index.js +++ b/index.js @@ -85,6 +85,19 @@ class HelpTools { }, 0); }; + 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 'Usage: ' + parentCmdNames + cmdName + ' ' + cmd.usage(); + } + padWidth(cmd, helper) { return Math.max( helper.largestOptionTermLength(cmd, helper), @@ -1706,15 +1719,7 @@ Read more on https://git.io/JJc0W`); } // Usage - 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; - } - let output = ['Usage: ' + parentCmdNames + cmdName + ' ' + this.usage(), '']; + let output = [helper.commandUsage(this), '']; // Description if (this._description) { From f4de6683c8beec92b649a2807f7460cea4a3b5be Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 21:24:15 +1300 Subject: [PATCH 18/66] Add term and description routines to HelpTools so symmetrical pattern --- index.js | 68 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index ff0f60f8b..9167a4aa4 100644 --- a/index.js +++ b/index.js @@ -67,6 +67,10 @@ class HelpTools { (args ? ' ' + args : ''); } + optionTerm(option) { + return `${option.flags}`; + } + largestCommandTermLength(cmd, helper) { return helper.visibleCommands(cmd).reduce((max, command) => { return Math.max(max, helper.commandTerm(command).length); @@ -75,7 +79,7 @@ class HelpTools { largestOptionTermLength(cmd, helper) { return helper.visibleOptions(cmd).reduce((max, option) => { - return Math.max(max, option.flags.length); + return Math.max(max, helper.optionTerm(option).length); }, 0); }; @@ -98,6 +102,36 @@ class HelpTools { return 'Usage: ' + parentCmdNames + cmdName + ' ' + cmd.usage(); } + commandDescription(cmd) { + return cmd.description(); + } + + /** + * Calculate the full description, including defaultValue etc. + * + * @return {string} + * @api public + */ + + 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}`; + }; + padWidth(cmd, helper) { return Math.max( helper.largestOptionTermLength(cmd, helper), @@ -211,32 +245,6 @@ 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. * @@ -1735,11 +1743,11 @@ Read more on https://git.io/JJc0W`); output = output.concat(['Arguments:', formatList(argumentsList), '']); } - // Optioms + // Options const visibleOptions = helper.visibleOptions(this); if (visibleOptions.length) { const optionList = visibleOptions.map((option) => { - return formatItem(option.flags, option.fullDescription()); + return formatItem(helper.optionTerm(option), helper.optionDescription(option)); }); output = output.concat(['Options:', formatList(optionList), '']); } @@ -1748,7 +1756,7 @@ Read more on https://git.io/JJc0W`); const visibleCommands = helper.visibleCommands(this); if (visibleCommands.length) { const commandList = visibleCommands.map((cmd) => { - return formatItem(helper.commandTerm(cmd), cmd.description()); + return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd)); }); output = output.concat(['Commands:', formatList(commandList), '']); } From 478e86c0b3a8a3345ed9982208f6b84f41efc49b Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 21:58:21 +1300 Subject: [PATCH 19/66] Name the magic numbers --- index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 9167a4aa4..bc6901025 100644 --- a/index.js +++ b/index.js @@ -1715,15 +1715,18 @@ Read more on https://git.io/JJc0W`); const helper = this.createHelpTools(); const termWidth = helper.padWidth(this, helper); const columns = helper.columns; - const descriptionWidth = columns - termWidth - 4; + const itemIndentWidth = 2; + const itemSeparatorWidth = 2; + // itemIndent term itemSeparator description + const descriptionWidth = columns - termWidth - itemIndentWidth - itemSeparatorWidth; function formatItem(term, description) { if (description) { - return helper.pad(term, termWidth) + ' ' + helper.optionalWrap(description, descriptionWidth, termWidth + 2, helper); + return helper.pad(term, termWidth + itemSeparatorWidth) + helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); } return term; }; - function formatList(text) { - return text.join('\n').replace(/^/gm, ' '); + function formatList(textArray) { + return textArray.join('\n').replace(/^/gm, Array(itemIndentWidth + 1).join(' ')); } // Usage From 11be18abd5cb73ae06e8023f8cd969d3d6c96d0e Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 22:44:05 +1300 Subject: [PATCH 20/66] More consistent naming --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index bc6901025..87e7567ba 100644 --- a/index.js +++ b/index.js @@ -83,7 +83,7 @@ class HelpTools { }, 0); }; - largestArgLength(cmd, helper) { + largestArgTermLength(cmd, helper) { return helper.visibleArguments(cmd).reduce((max, argument) => { return Math.max(max, argument.term.length); }, 0); @@ -136,7 +136,7 @@ class HelpTools { return Math.max( helper.largestOptionTermLength(cmd, helper), helper.largestCommandTermLength(cmd, helper), - helper.largestArgLength(cmd, helper) + helper.largestArgTermLength(cmd, helper) ); }; From 78017aa93ad40926f1854af3ee1d88651c39fee0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 22:52:12 +1300 Subject: [PATCH 21/66] Remove reference to removed routine --- tests/command.commandHelp.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index 8a0199e1e..0a7875baa 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -1,6 +1,6 @@ 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 help', () => { const program = new commander.Command(); From 162dca6385ddfce0189731df3404e0cdb2311278 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 23:11:43 +1300 Subject: [PATCH 22/66] Move help formatting into HelpTools --- index.js | 108 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/index.js b/index.js index 87e7567ba..4083f5581 100644 --- a/index.js +++ b/index.js @@ -132,6 +132,61 @@ class HelpTools { return `${option.description}`; }; + 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 helper.pad(term, termWidth + itemSeparatorWidth) + helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); + } + return term; + }; + function formatList(textArray) { + return textArray.join('\n').replace(/^/gm, Array(itemIndentWidth + 1).join(' ')); + } + + // Usage + let output = [helper.commandUsage(cmd), '']; + + // Description + if (cmd.description()) { + output = output.concat([cmd.description(), '']); + } + + // Arguments + const visibleArguments = helper.visibleArguments(cmd); + if (visibleArguments.length) { + const argumentsList = visibleArguments.map((argument) => { + return formatItem(argument.term, argument.description); + }); + output = output.concat(['Arguments:', formatList(argumentsList), '']); + } + + // Options + const visibleOptions = helper.visibleOptions(cmd); + if (visibleOptions.length) { + const optionList = visibleOptions.map((option) => { + return formatItem(helper.optionTerm(option), helper.optionDescription(option)); + }); + output = output.concat(['Options:', formatList(optionList), '']); + } + + // Commands + const visibleCommands = helper.visibleCommands(cmd); + if (visibleCommands.length) { + const commandList = visibleCommands.map((cmd) => { + return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd)); + }); + output = output.concat(['Commands:', formatList(commandList), '']); + } + + return output.join('\n'); + } + padWidth(cmd, helper) { return Math.max( helper.largestOptionTermLength(cmd, helper), @@ -1713,58 +1768,7 @@ Read more on https://git.io/JJc0W`); helpInformation() { const helper = this.createHelpTools(); - const termWidth = helper.padWidth(this, 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 helper.pad(term, termWidth + itemSeparatorWidth) + helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); - } - return term; - }; - function formatList(textArray) { - return textArray.join('\n').replace(/^/gm, Array(itemIndentWidth + 1).join(' ')); - } - - // Usage - let output = [helper.commandUsage(this), '']; - - // Description - if (this._description) { - output = output.concat([this._description, '']); - } - - // Arguments - const visibleArguments = helper.visibleArguments(this); - if (visibleArguments.length) { - const argumentsList = visibleArguments.map((argument) => { - return formatItem(argument.term, argument.description); - }); - output = output.concat(['Arguments:', formatList(argumentsList), '']); - } - - // Options - const visibleOptions = helper.visibleOptions(this); - if (visibleOptions.length) { - const optionList = visibleOptions.map((option) => { - return formatItem(helper.optionTerm(option), helper.optionDescription(option)); - }); - output = output.concat(['Options:', formatList(optionList), '']); - } - - // Commands - const visibleCommands = helper.visibleCommands(this); - if (visibleCommands.length) { - const commandList = visibleCommands.map((cmd) => { - return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd)); - }); - output = output.concat(['Commands:', formatList(commandList), '']); - } - - return output.join('\n'); + return helper.formatHelp(this, helper); }; /** From a0d18620f750a62adab8a62b8026d14b2cb3c4f6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Sep 2020 23:26:03 +1300 Subject: [PATCH 23/66] Fix typescript-checkJS errors --- index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 4083f5581..995c184cd 100644 --- a/index.js +++ b/index.js @@ -20,8 +20,8 @@ class HelpTools { const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); if (cmd._lazyHasImplicitHelpCommand()) { const helpCommand = new Command(cmd._helpCommandnameAndArgs) - .description(cmd._helpCommandDescription) - .helpOption(false); + .helpOption(false) + .description(cmd._helpCommandDescription); visibleCommands.push(helpCommand); } return visibleCommands; @@ -562,8 +562,7 @@ class Command extends EventEmitter { * You can customise the help with either a subclass by overriding createHelpTools, * or by supplying routines using helpToolsOverrides. * - * @param {string} [name] - * @return {Command} new command + * @return {HelpTools} * @api public */ From f17be74b409b3803d347e8d71a827a2d6ff32574 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 30 Sep 2020 19:50:19 +1300 Subject: [PATCH 24/66] Simpler naming --- index.js | 24 ++++++++++++------------ tests/command.chain.test.js | 4 ++-- tests/helpwrap.test.js | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 995c184cd..937980d00 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,7 @@ const fs = require('fs'); // Although this is a class, methods are static in style to allow override using subclass or just functions. // (Need to reconcile what is private when decide public/private methods????) -class HelpTools { +class Help { constructor() { this.columns = process.stdout.columns || 80; } @@ -472,7 +472,7 @@ class Command extends EventEmitter { this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; - this._helpToolsOverrides = {}; + this._helpOverrides = {}; } /** @@ -528,7 +528,7 @@ class Command extends EventEmitter { cmd._helpCommandName = this._helpCommandName; cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs; cmd._helpCommandDescription = this._helpCommandDescription; - cmd._helpToolsOverrides = this._helpToolsOverrides; + cmd._helpOverrides = this._helpOverrides; cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; @@ -559,19 +559,19 @@ class Command extends EventEmitter { }; /** - * You can customise the help with either a subclass by overriding createHelpTools, - * or by supplying routines using helpToolsOverrides. + * You can customise the help with either a subclass by overriding createHelp, + * or by supplying routines using helpOverrides. * - * @return {HelpTools} + * @return {Help} * @api public */ - createHelpTools() { - return Object.assign(new HelpTools(), this._helpToolsOverrides); + createHelp() { + return Object.assign(new Help(), this._helpOverrides); }; - helpToolsOverrides(overrides) { - this._helpToolsOverrides = overrides; + helpOverrides(overrides) { + this._helpOverrides = overrides; return this; } @@ -1766,7 +1766,7 @@ Read more on https://git.io/JJc0W`); */ helpInformation() { - const helper = this.createHelpTools(); + const helper = this.createHelp(); return helper.formatHelp(this, helper); }; @@ -1921,7 +1921,7 @@ exports.program = exports; // More explicit access to global command. exports.Command = Command; exports.Option = Option; exports.CommanderError = CommanderError; -exports.HelpTools = HelpTools; +exports.Help = Help; /** * Camel-case the given `flag` diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index be707cf96..62c4b6fc9 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -130,9 +130,9 @@ describe('Command methods that should return this for chaining', () => { expect(result).toBe(program); }); - test('when call .helpToolsOverrides() then returns this', () => { + test('when call .helpOverrides() then returns this', () => { const program = new Command(); - const result = program.helpToolsOverrides({ }); + const result = program.helpOverrides({ }); expect(result).toBe(program); }); }); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 0c4db5f55..043b7fe86 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -6,7 +6,7 @@ const commander = require('../'); test('when long option description then wrap and indent', () => { const program = new commander.Command(); program - .helpToolsOverrides({ columns: 80 }) + .helpOverrides({ 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 = @@ -25,7 +25,7 @@ Options: test('when long option description and default then wrap and indent', () => { const program = new commander.Command(); program - .helpToolsOverrides({ columns: 80 }) + .helpOverrides({ columns: 80 }) .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); const expectedOutput = @@ -43,7 +43,7 @@ Options: test('when long command description then wrap and indent', () => { const program = new commander.Command(); program - .helpToolsOverrides({ columns: 80 }) + .helpOverrides({ columns: 80 }) .option('-x --extra-long-option-switch', 'x') .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); @@ -68,7 +68,7 @@ test('when not enough room then help not wrapped', () => { 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 - .helpToolsOverrides({ columns: 60 }) + .helpOverrides({ columns: 60 }) .command('1234567801234567890x', commandDescription); const expectedOutput = @@ -90,7 +90,7 @@ test('when option descripton preformatted then only add small indent', () => { const optionSpec = '-t, --time '; const program = new commander.Command(); program - .helpToolsOverrides({ columns: 80 }) + .helpOverrides({ columns: 80 }) .option(optionSpec, `select time Time can also be specified using special values: From 943d01aacf84545ab96c3d625f7fb5fb36cd79fe Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 30 Sep 2020 22:41:04 +1300 Subject: [PATCH 25/66] Slightly simplify code --- index.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 937980d00..679cb5695 100644 --- a/index.js +++ b/index.js @@ -158,29 +158,26 @@ class Help { } // Arguments - const visibleArguments = helper.visibleArguments(cmd); - if (visibleArguments.length) { - const argumentsList = visibleArguments.map((argument) => { - return formatItem(argument.term, argument.description); - }); - output = output.concat(['Arguments:', formatList(argumentsList), '']); + 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 visibleOptions = helper.visibleOptions(cmd); - if (visibleOptions.length) { - const optionList = visibleOptions.map((option) => { - return formatItem(helper.optionTerm(option), helper.optionDescription(option)); - }); + 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 visibleCommands = helper.visibleCommands(cmd); - if (visibleCommands.length) { - const commandList = visibleCommands.map((cmd) => { - return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd)); - }); + const commandList = helper.visibleCommands(cmd).map((cmd) => { + return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd)); + }); + if (commandList.length > 0) { output = output.concat(['Commands:', formatList(commandList), '']); } From 79deb3a85fc6296fb77fc606a78ec8d4d9602641 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 2 Oct 2020 22:17:45 +1300 Subject: [PATCH 26/66] Add getter/setter to assist overrides --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 679cb5695..dc0b66b67 100644 --- a/index.js +++ b/index.js @@ -568,6 +568,8 @@ class Command extends EventEmitter { }; helpOverrides(overrides) { + if (overrides === undefined) return this._helpOverrides; + this._helpOverrides = overrides; return this; } From 9425003e1be0bbda60eb22480aea8305d6c6edda Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 00:17:26 +1300 Subject: [PATCH 27/66] Add sort overrides --- index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/index.js b/index.js index dc0b66b67..343fd4e0e 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,8 @@ const fs = require('fs'); class Help { constructor() { this.columns = process.stdout.columns || 80; + this.sortCommands = false; + this.sortOptions = false; } visibleCommands(cmd) { @@ -24,6 +26,11 @@ class Help { .description(cmd._helpCommandDescription); visibleCommands.push(helpCommand); } + if (this.sortCommands) { + visibleCommands.sort((a, b) => { + return a.name().localeCompare(b.name()); + }); + } return visibleCommands; } @@ -44,6 +51,11 @@ class Help { } visibleOptions.push(helpOption); } + if (this.sortOptions) { + visibleOptions.sort((a, b) => { + return a.name().localeCompare(b.name()); + }); + } return visibleOptions; } From 8fc29df57020917113fdce4af878586d13202b53 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 13:08:26 +1300 Subject: [PATCH 28/66] First cut at TypeScript definitions for Help, no TSDoc yet --- typings/index.d.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index a41db6740..20a3e5409 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -72,6 +72,35 @@ declare namespace commander { } type OptionConstructor = new (flags: string, description?: string) => Option; + interface Help { + columns: number; + sortCommands: boolean; + sortOptions: boolean; + + visibleCommands(cmd: Command): Command[]; + visibleOptions(cmd: Command): Option[]; + visibleArguments(cmd: Command): Array<{ term: string; description: string}>; + + commandTerm(cmd: Command): string; + optionTerm(option: Option): string; + + largestCommandTermLength(cmd: Command, helper: Help): number; + largestOptionTermLength(cmd: Command, helper: Help): number; + largestArgTermLength(cmd: Command, helper: Help): number; + padWidth(cmd: Command, helper: Help): number; + + commandUsage(cmd: Command): string; + commandDescription(cmd: Command): string; + optionDescription(option: Option): string; + + wrap(str: string, width: number, indent: number): string; + optionalWrap(str: string, width: number, indent: number, helper: Help): string; + + formatHelp(cmd: Command, helper: Help): string; + } + type HelpConstructor = new () => Help; + type HelpOverridesConfiguration = Partial; + interface ParseOptions { from: 'node' | 'electron' | 'user'; } @@ -172,6 +201,9 @@ declare namespace commander { */ exitOverride(callback?: (err: CommanderError) => never|void): this; + helpOverrides(): HelpOverridesConfiguration; + helpOverrides(overrides: HelpOverridesConfiguration | object): this; + /** * Register callback `fn` for the command. * From 63f58d94d21860eff14480109d809028f23f777e Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 13:26:57 +1300 Subject: [PATCH 29/66] Replace pad and low level indents with modern calls --- index.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 343fd4e0e..0f869876f 100644 --- a/index.js +++ b/index.js @@ -153,12 +153,12 @@ class Help { const descriptionWidth = columns - termWidth - itemIndentWidth - itemSeparatorWidth; function formatItem(term, description) { if (description) { - return helper.pad(term, termWidth + itemSeparatorWidth) + helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); + return term.padEnd(termWidth + itemSeparatorWidth) + helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); } return term; }; function formatList(textArray) { - return textArray.join('\n').replace(/^/gm, Array(itemIndentWidth + 1).join(' ')); + return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth)); } // Usage @@ -204,20 +204,6 @@ class Help { ); }; - /** - * Pad `str` to `width`. - * - * @param {string} str - * @param {number} width - * @return {string} - * @api private - */ - - 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. @@ -229,13 +215,14 @@ class Help { * @api private */ wrap(str, width, indent) { + 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) ? Array(indent + 1).join(' ') : '') + line.trimRight(); + return ((i > 0 && indent) ? indentString : '') + line.trimRight(); }).join('\n'); } From 25f240f0f3d7cbed88b1e72be910d3727316da47 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 14:46:00 +1300 Subject: [PATCH 30/66] Rename and rework type for HelpConfiguration --- index.js | 14 +++++++------- tests/command.chain.test.js | 4 ++-- tests/helpwrap.test.js | 10 +++++----- typings/index.d.ts | 8 +++++--- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 0f869876f..aa1b77dab 100644 --- a/index.js +++ b/index.js @@ -468,7 +468,7 @@ class Command extends EventEmitter { this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; - this._helpOverrides = {}; + this._helpConfiguration = {}; } /** @@ -524,7 +524,7 @@ class Command extends EventEmitter { cmd._helpCommandName = this._helpCommandName; cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs; cmd._helpCommandDescription = this._helpCommandDescription; - cmd._helpOverrides = this._helpOverrides; + cmd._helpConfiguration = this._helpConfiguration; cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; @@ -556,20 +556,20 @@ class Command extends EventEmitter { /** * You can customise the help with either a subclass by overriding createHelp, - * or by supplying routines using helpOverrides. + * or by supplying routines using configureHelp(). * * @return {Help} * @api public */ createHelp() { - return Object.assign(new Help(), this._helpOverrides); + return Object.assign(new Help(), this.configureHelp()); }; - helpOverrides(overrides) { - if (overrides === undefined) return this._helpOverrides; + configureHelp(configuration) { + if (configuration === undefined) return this._helpConfiguration; - this._helpOverrides = overrides; + this._helpConfiguration = configuration; return this; } diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 62c4b6fc9..5979c3ab0 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -130,9 +130,9 @@ describe('Command methods that should return this for chaining', () => { expect(result).toBe(program); }); - test('when call .helpOverrides() then returns this', () => { + test('when call .configureHelp() then returns this', () => { const program = new Command(); - const result = program.helpOverrides({ }); + const result = program.configureHelp({ }); expect(result).toBe(program); }); }); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 043b7fe86..32e2ff600 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -6,7 +6,7 @@ const commander = require('../'); test('when long option description then wrap and indent', () => { const program = new commander.Command(); program - .helpOverrides({ columns: 80 }) + .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 = @@ -25,7 +25,7 @@ Options: test('when long option description and default then wrap and indent', () => { const program = new commander.Command(); program - .helpOverrides({ columns: 80 }) + .configureHelp({ columns: 80 }) .option('-x --extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); const expectedOutput = @@ -43,7 +43,7 @@ Options: test('when long command description then wrap and indent', () => { const program = new commander.Command(); program - .helpOverrides({ columns: 80 }) + .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.'); @@ -68,7 +68,7 @@ test('when not enough room then help not wrapped', () => { 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 - .helpOverrides({ columns: 60 }) + .configureHelp({ columns: 60 }) .command('1234567801234567890x', commandDescription); const expectedOutput = @@ -90,7 +90,7 @@ test('when option descripton preformatted then only add small indent', () => { const optionSpec = '-t, --time '; const program = new commander.Command(); program - .helpOverrides({ columns: 80 }) + .configureHelp({ columns: 80 }) .option(optionSpec, `select time Time can also be specified using special values: diff --git a/typings/index.d.ts b/typings/index.d.ts index 20a3e5409..0a1d9ec53 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -99,7 +99,9 @@ declare namespace commander { formatHelp(cmd: Command, helper: Help): string; } type HelpConstructor = new () => Help; - type HelpOverridesConfiguration = Partial; + interface HelpConfiguration extends Partial { + [key: string]: any; // allow extra custom properties + } interface ParseOptions { from: 'node' | 'electron' | 'user'; @@ -201,8 +203,8 @@ declare namespace commander { */ exitOverride(callback?: (err: CommanderError) => never|void): this; - helpOverrides(): HelpOverridesConfiguration; - helpOverrides(overrides: HelpOverridesConfiguration | object): this; + configureHelp(configuration: HelpConfiguration): this; + configureHelp(): HelpConfiguration; /** * Register callback `fn` for the command. From d8ba2c038face3c01a6a9078cb12f5733438c0a5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 14:55:29 +1300 Subject: [PATCH 31/66] Combine optionalWrap and wrap --- index.js | 36 +++++++++++------------------------- typings/index.d.ts | 14 ++++++-------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/index.js b/index.js index aa1b77dab..144688e74 100644 --- a/index.js +++ b/index.js @@ -153,7 +153,7 @@ class Help { const descriptionWidth = columns - termWidth - itemIndentWidth - itemSeparatorWidth; function formatItem(term, description) { if (description) { - return term.padEnd(termWidth + itemSeparatorWidth) + helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); + return term.padEnd(termWidth + itemSeparatorWidth) + helper.wrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); } return term; }; @@ -204,28 +204,6 @@ class Help { ); }; - /** - * 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 - */ - wrap(str, width, indent) { - 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'); - } - /** * 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 @@ -237,7 +215,7 @@ class Help { * @return {string} * @api private */ - optionalWrap(str, width, indent, helper) { + 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; @@ -245,7 +223,15 @@ class Help { const minWidth = 40; if (width < minWidth) return str; - return helper.wrap(str, width, indent); + 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'); } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 0a1d9ec53..0899de7cb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -77,24 +77,22 @@ declare namespace commander { sortCommands: boolean; sortOptions: boolean; + commandTerm(cmd: Command): string; + commandUsage(cmd: Command): string; + commandDescription(cmd: Command): string; + optionTerm(option: Option): string; + optionDescription(option: Option): string; + visibleCommands(cmd: Command): Command[]; visibleOptions(cmd: Command): Option[]; visibleArguments(cmd: Command): Array<{ term: string; description: string}>; - commandTerm(cmd: Command): string; - optionTerm(option: Option): string; - largestCommandTermLength(cmd: Command, helper: Help): number; largestOptionTermLength(cmd: Command, helper: Help): number; largestArgTermLength(cmd: Command, helper: Help): number; padWidth(cmd: Command, helper: Help): number; - commandUsage(cmd: Command): string; - commandDescription(cmd: Command): string; - optionDescription(option: Option): string; - wrap(str: string, width: number, indent: number): string; - optionalWrap(str: string, width: number, indent: number, helper: Help): string; formatHelp(cmd: Command, helper: Help): string; } From d1da3f7dbf0cb023b23c5821de37bf2ed61a703c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 15:01:00 +1300 Subject: [PATCH 32/66] Add createHelp to TypeScript definition --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0899de7cb..ebb284b4f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -201,6 +201,7 @@ declare namespace commander { */ exitOverride(callback?: (err: CommanderError) => never|void): this; + createHelp(): Help; configureHelp(configuration: HelpConfiguration): this; configureHelp(): HelpConfiguration; From ee0a71b22cd6fef4b18397dd8352b0abaf5ee360 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 15:08:23 +1300 Subject: [PATCH 33/66] Add test-all script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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": [ From 2ea761932da0801dd2ec246e7389d30fa9d41e18 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 17:29:52 +1300 Subject: [PATCH 34/66] More carefully make concrete help option for displaying --- index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 144688e74..26644b032 100644 --- a/index.js +++ b/index.js @@ -21,9 +21,12 @@ class Help { visibleCommands(cmd) { const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); if (cmd._lazyHasImplicitHelpCommand()) { - const helpCommand = new Command(cmd._helpCommandnameAndArgs) + // Create a command matching the implicit help command. + const args = cmd._helpCommandnameAndArgs.split(/ +/); + const helpCommand = cmd.createCommand(args.shift()) .helpOption(false) .description(cmd._helpCommandDescription); + helpCommand._parseExpectedArgs(args); visibleCommands.push(helpCommand); } if (this.sortCommands) { @@ -71,11 +74,11 @@ class Help { /* WIP: */ commandTerm(cmd) { - // Why not just use usage?! + // 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]' : '') + // simple check for non-help option + (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option (args ? ' ' + args : ''); } From 36b3be7bfbade3c989488beecfad564ac5ebc707 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 17:30:33 +1300 Subject: [PATCH 35/66] Fix test with valid parameters for custom help --- tests/command.helpCommand.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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/); }); }); From 8ec534290395e2b3d7f17fc47f2445793a4961cc Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 17:30:44 +1300 Subject: [PATCH 36/66] Start adding Help tests --- tests/help.test.js | 124 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/help.test.js diff --git a/tests/help.test.js b/tests/help.test.js new file mode 100644 index 000000000..1a5e3ce78 --- /dev/null +++ b/tests/help.test.js @@ -0,0 +1,124 @@ +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']); + }); +}); + +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('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' }]); + }); +}); + +describe.skip('commandTerm', () => { + // Not happy with legacy behaviour which ignores possibility of hidden options or custom usage string or subcommands. +}); + +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)).toBe(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)).toBe(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)).toBe(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)).toBe(flags); + }); +}); From 5e895fcf2c3ab214890bbb2a9f733258b3ed2861 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 17:52:19 +1300 Subject: [PATCH 37/66] Add largestCommandTermLength tests --- tests/help.test.js | 56 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/tests/help.test.js b/tests/help.test.js index 1a5e3ce78..9e1e4c6bd 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -98,27 +98,75 @@ describe('optionTerm', () => { const flags = '-s'; const option = new commander.Option(flags); const helper = new commander.Help(); - expect(helper.optionTerm(option)).toBe(flags); + 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)).toBe(flags); + 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)).toBe(flags); + 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)).toBe(flags); + expect(helper.optionTerm(option)).toEqual(flags); + }); +}); + +describe('largestCommandTermLength', () => { + test('when no commands then returns zero', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.largestCommandTermLength(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.largestCommandTermLength(program, helper)).toEqual(helper.commandTerm(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.largestCommandTermLength(program, helper)).toEqual(longestCommandName.length); + }); + + test('when jsut help command then returns length of help term', () => { + const program = new commander.Command(); + program + .addHelpCommand(true); + const helper = new commander.Help(); + expect(helper.largestCommandTermLength(program, helper)).toEqual('help [command]'.length); }); }); From 71acb6906508103c35586d41faf6f18c82939297 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 18:22:50 +1300 Subject: [PATCH 38/66] Add more Help tests --- tests/help.test.js | 60 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/tests/help.test.js b/tests/help.test.js index 9e1e4c6bd..faf3a9fd1 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -150,8 +150,8 @@ describe('largestCommandTermLength', () => { expect(helper.largestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); }); - test('when multiple commands and no help then returns longest length', () => { - const longestCommandName = 'alphabet-soup'; + test('when multiple commands then returns longest length', () => { + const longestCommandName = 'alphabet-soup '; const program = new commander.Command(); program .addHelpCommand(false) @@ -162,7 +162,7 @@ describe('largestCommandTermLength', () => { expect(helper.largestCommandTermLength(program, helper)).toEqual(longestCommandName.length); }); - test('when jsut help command then returns length of help term', () => { + test('when just help command then returns length of help term', () => { const program = new commander.Command(); program .addHelpCommand(true); @@ -170,3 +170,57 @@ describe('largestCommandTermLength', () => { expect(helper.largestCommandTermLength(program, helper)).toEqual('help [command]'.length); }); }); + +describe('largestOptionTermLength', () => { + test('when no option then returns zero', () => { + const program = new commander.Command(); + program.helpOption(false); + const helper = new commander.Help(); + expect(helper.largestOptionTermLength(program, helper)).toEqual(0); + }); + + test('when implicit help option returns length of help flags', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.largestOptionTermLength(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.largestOptionTermLength(program, helper)).toEqual(longestOptionFlags.length); + }); +}); + +describe('largestArgTermLength', () => { + test('when no arguments then returns zero', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.largestArgTermLength(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.largestArgTermLength(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.largestArgTermLength(program, helper)).toEqual('longest'.length); + }); +}); From a946cda0f0ed13b4557d21f6aa1a21235b8ed261 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 18:37:48 +1300 Subject: [PATCH 39/66] Add commandUsage tests --- tests/help.test.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/help.test.js b/tests/help.test.js index faf3a9fd1..2e08c20dd 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -224,3 +224,48 @@ describe('largestArgTermLength', () => { expect(helper.largestArgTermLength(program, helper)).toEqual('longest'.length); }); }); + +describe('commandUsage', () => { + test('when single program then "Usage: program [options]"', () => { + const program = new commander.Command(); + program.name('program'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('Usage: program [options]'); + }); + + test('when multi program then "Usage: program [options] [command]"', () => { + const program = new commander.Command(); + program.name('program'); + program.command('sub'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('Usage: 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('Usage: 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('Usage: 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('Usage: program [options] '); + }); +}); From 8230fe5fb0a4125be54079e6b8c5cb2a7fec932f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 18:51:42 +1300 Subject: [PATCH 40/66] Add test for commandDescription --- tests/help.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/help.test.js b/tests/help.test.js index 2e08c20dd..7bc93bf4e 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -269,3 +269,19 @@ describe('commandUsage', () => { expect(helper.commandUsage(program)).toEqual('Usage: program [options] '); }); }); + +describe('commandDescription', () => { + test('when program has no description then undefined', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.commandDescription(program)).toBeUndefined(); + }); + + test('when program has description then return description', () => { + const description = 'womble'; + const program = new commander.Command(); + program.description(description); + const helper = new commander.Help(); + expect(helper.commandDescription(program)).toEqual(description); + }); +}); From 09c460e6371527b9a83baf18d743831a516bdb89 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 19:17:44 +1300 Subject: [PATCH 41/66] Add missing Command properties, and default description to empty string --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 26644b032..e0ae68d90 100644 --- a/index.js +++ b/index.js @@ -446,6 +446,8 @@ class Command extends EventEmitter { this._exitCallback = null; this._aliases = []; this._combineFlagAndOptionalValue = true; + this._description = ''; + this._argsDescription = undefined; this._hidden = false; this._hasHelpOption = true; From 1e60ab17d3462c71ca997375056ffc175224187b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 19:18:00 +1300 Subject: [PATCH 42/66] Add tests for optionDescription --- tests/help.test.js | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/help.test.js b/tests/help.test.js index 7bc93bf4e..5e5d09451 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -271,17 +271,55 @@ describe('commandUsage', () => { }); describe('commandDescription', () => { - test('when program has no description then undefined', () => { + test('when program has no description then empty string', () => { const program = new commander.Command(); const helper = new commander.Help(); - expect(helper.commandDescription(program)).toBeUndefined(); + expect(helper.commandDescription(program)).toEqual(''); }); test('when program has description then return description', () => { - const description = 'womble'; + const description = 'description'; const program = new commander.Command(); program.description(description); const helper = new commander.Help(); expect(helper.commandDescription(program)).toEqual(description); }); }); + +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")'); + }); +}); From 7e752354f4f48096cd13fd826dc2c937002c04dd Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 20:10:34 +1300 Subject: [PATCH 43/66] Add padWidth tests --- tests/help.test.js | 51 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/help.test.js b/tests/help.test.js index 5e5d09451..05d5bf1a0 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -179,7 +179,7 @@ describe('largestOptionTermLength', () => { expect(helper.largestOptionTermLength(program, helper)).toEqual(0); }); - test('when implicit help option returns length of help flags', () => { + test('when just implicit help option returns length of help flags', () => { const program = new commander.Command(); const helper = new commander.Help(); expect(helper.largestOptionTermLength(program, helper)).toEqual('-h, --help'.length); @@ -323,3 +323,52 @@ describe('optionDescription', () => { expect(helper.optionDescription(option)).toEqual('description (choices: "one", "two")'); }); }); + +describe.skip('formatHelp', () => { + // Might be happy with the Command tests? +}); + +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); + }); +}); + +describe.skip('wrap', () => { + // To Do +}); From b96c5283fa15021c9f83d2e155a0f50f862f46d4 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 20:27:53 +1300 Subject: [PATCH 44/66] Add sort tests --- tests/help.test.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/help.test.js b/tests/help.test.js index 05d5bf1a0..3f5f35284 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -3,6 +3,72 @@ 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.skip('columns', () => { + // To Do +}); + +describe('sortCommands', () => { + 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 sortCommands:true then commands sorted', () => { + const program = new commander.Command(); + program + .configureHelp({ sortCommands: 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']); + }); +}); + +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', () => { + const program = new commander.Command(); + program + .configureHelp({ sortOptions: true }) + .option('--zzz', 'desc') + .option('--aaa', 'desc') + .option('--bbb', 'desc'); + const helper = program.createHelp(); + const visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + }); + + test('when sortOptions:true then options sorted on name not flags', () => { + 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 visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + }); +}); + describe('visibleCommands', () => { test('when no subcommands then empty array', () => { const program = new commander.Command(); From e195b7803d413b98b88a3b8ebae66aa2f3ec7e36 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 21:53:35 +1300 Subject: [PATCH 45/66] Add columns and wrap tests --- tests/help.test.js | 64 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/tests/help.test.js b/tests/help.test.js index 3f5f35284..e7c0d8fbc 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -3,8 +3,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.skip('columns', () => { - // To Do +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); + }); }); describe('sortCommands', () => { @@ -435,6 +449,48 @@ describe('padWidth', () => { }); }); -describe.skip('wrap', () => { - // To Do +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); + }); }); From 74c2adfd96f9e95d59f7f77ac5b1f1c8fdaef8f0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Oct 2020 22:09:53 +1300 Subject: [PATCH 46/66] Add test for legacy commandTerm behaviour --- index.js | 3 --- tests/help.test.js | 45 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index e0ae68d90..bb229e42a 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,6 @@ class Help { return visibleCommands; } - /* WIP */ visibleOptions(cmd) { const visibleOptions = cmd.options.filter((option) => !option.hidden); // Implicit help @@ -62,7 +61,6 @@ class Help { return visibleOptions; } - /* WIP */ visibleArguments(cmd) { if (cmd._argsDescription && cmd._args.length) { return cmd._args.map((argument) => { @@ -72,7 +70,6 @@ class Help { return []; } - /* WIP: */ commandTerm(cmd) { // Legacy. Ignores custom usage string, and nested commands. const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); diff --git a/tests/help.test.js b/tests/help.test.js index e7c0d8fbc..8b659eba4 100644 --- a/tests/help.test.js +++ b/tests/help.test.js @@ -3,6 +3,8 @@ 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). +// Not testing formatHelp here, tested implicitly by help tests in other files. + describe('columns', () => { test('when default then columns from stdout', () => { const hold = process.stdout.columns; @@ -169,8 +171,43 @@ describe('visibleArguments', () => { }); }); -describe.skip('commandTerm', () => { - // Not happy with legacy behaviour which ignores possibility of hidden options or custom usage string or subcommands. +// commandTerm does not currently respect helpOption or ignore hidden options, so not testing those. +describe('commandTerm', () => { + test('when plain command then returns name', () => { + const command = new commander.Command('program'); + const helper = new commander.Help(); + expect(helper.commandTerm(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.commandTerm(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.commandTerm(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.commandTerm(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.commandTerm(command)).toEqual('program|alias [options] '); + }); }); describe('optionTerm', () => { @@ -404,10 +441,6 @@ describe('optionDescription', () => { }); }); -describe.skip('formatHelp', () => { - // Might be happy with the Command tests? -}); - describe('padWidth', () => { test('when argument term longest return argument length', () => { const longestThing = 'veryLongThingBiggerThanOthers'; From f654c9a0b8a10abcda48d24fdc95033014743955 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 4 Oct 2020 00:13:32 +1300 Subject: [PATCH 47/66] Add TypeScript usage tests for Help --- typings/commander-tests.ts | 38 ++++++++++++++++++++++++++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 39 insertions(+) diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 130c18994..15b11cc06 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({ + sortCommands: true, // override property + visibleCommands: (cmd: commander.Command) => [], // override method + customProperty: 'boo' +}); +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.sortCommands = true; +helper.sortOptions = false; + +const commandTermStr: string = helper.commandTerm(helperCommand); +const commandUsageStr: string = helper.commandUsage(helperCommand); +const commandDescriptionStr: string = helper.commandDescription(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.largestCommandTermLength(helperCommand, helper); +const widestOption: number = helper.largestOptionTermLength(helperCommand, helper); +const widestArgument: number = helper.largestArgTermLength(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 ebb284b4f..cd7b98a6a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -499,6 +499,7 @@ declare namespace commander { Command: CommandConstructor; Option: OptionConstructor; CommanderError: CommanderErrorConstructor; + Help: HelpConstructor; } } From ed68199c9e1532cd10b7f46ef61e8b045d7781d5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 4 Oct 2020 10:16:01 +1300 Subject: [PATCH 48/66] Refactor Help tests into separate files --- tests/help.columns.test.js | 22 + tests/help.commandDescription.test.js | 20 + tests/help.commandTerm.test.js | 43 ++ tests/help.commandUsage.test.js | 49 ++ tests/help.largestArgTermLength.test.js | 32 ++ tests/help.largestCommandTermLength.test.js | 52 ++ tests/help.largestOptionTermLength.test.js | 30 ++ tests/help.optionDescription.test.js | 42 ++ tests/help.optionTerm.test.js | 34 ++ tests/help.padWidth.test.js | 45 ++ tests/help.sortCommands.test.js | 29 ++ tests/help.sortOptions.test.js | 41 ++ tests/help.test.js | 529 -------------------- tests/help.visibleArgumnets.test.js | 27 + tests/help.visibleCommands.test.js | 33 ++ tests/help.visibleOptions.test.js | 38 ++ tests/help.wrap.test.js | 164 ++++++ tests/helpwrap.test.js | 115 ----- 18 files changed, 701 insertions(+), 644 deletions(-) create mode 100644 tests/help.columns.test.js create mode 100644 tests/help.commandDescription.test.js create mode 100644 tests/help.commandTerm.test.js create mode 100644 tests/help.commandUsage.test.js create mode 100644 tests/help.largestArgTermLength.test.js create mode 100644 tests/help.largestCommandTermLength.test.js create mode 100644 tests/help.largestOptionTermLength.test.js create mode 100644 tests/help.optionDescription.test.js create mode 100644 tests/help.optionTerm.test.js create mode 100644 tests/help.padWidth.test.js create mode 100644 tests/help.sortCommands.test.js create mode 100644 tests/help.sortOptions.test.js delete mode 100644 tests/help.test.js create mode 100644 tests/help.visibleArgumnets.test.js create mode 100644 tests/help.visibleCommands.test.js create mode 100644 tests/help.visibleOptions.test.js create mode 100644 tests/help.wrap.test.js delete mode 100644 tests/helpwrap.test.js 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..c03a1f515 --- /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('commandDescription', () => { + test('when program has no description then empty string', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.commandDescription(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.commandDescription(program)).toEqual(description); + }); +}); diff --git a/tests/help.commandTerm.test.js b/tests/help.commandTerm.test.js new file mode 100644 index 000000000..543685960 --- /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). + +// commandTerm does not currently respect helpOption or ignore hidden options, so not testing those. +describe('commandTerm', () => { + test('when plain command then returns name', () => { + const command = new commander.Command('program'); + const helper = new commander.Help(); + expect(helper.commandTerm(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.commandTerm(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.commandTerm(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.commandTerm(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.commandTerm(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..1d8fa3536 --- /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 "Usage: program [options]"', () => { + const program = new commander.Command(); + program.name('program'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('Usage: program [options]'); + }); + + test('when multi program then "Usage: program [options] [command]"', () => { + const program = new commander.Command(); + program.name('program'); + program.command('sub'); + const helper = new commander.Help(); + expect(helper.commandUsage(program)).toEqual('Usage: 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('Usage: 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('Usage: 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('Usage: program [options] '); + }); +}); diff --git a/tests/help.largestArgTermLength.test.js b/tests/help.largestArgTermLength.test.js new file mode 100644 index 000000000..0e172c899 --- /dev/null +++ b/tests/help.largestArgTermLength.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('largestArgTermLength', () => { + test('when no arguments then returns zero', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.largestArgTermLength(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.largestArgTermLength(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.largestArgTermLength(program, helper)).toEqual('longest'.length); + }); +}); diff --git a/tests/help.largestCommandTermLength.test.js b/tests/help.largestCommandTermLength.test.js new file mode 100644 index 000000000..f38c81c92 --- /dev/null +++ b/tests/help.largestCommandTermLength.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('largestCommandTermLength', () => { + test('when no commands then returns zero', () => { + const program = new commander.Command(); + const helper = new commander.Help(); + expect(helper.largestCommandTermLength(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.largestCommandTermLength(program, helper)).toEqual(helper.commandTerm(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.largestCommandTermLength(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.largestCommandTermLength(program, helper)).toEqual('help [command]'.length); + }); +}); diff --git a/tests/help.largestOptionTermLength.test.js b/tests/help.largestOptionTermLength.test.js new file mode 100644 index 000000000..256387cd8 --- /dev/null +++ b/tests/help.largestOptionTermLength.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('largestOptionTermLength', () => { + test('when no option then returns zero', () => { + const program = new commander.Command(); + program.helpOption(false); + const helper = new commander.Help(); + expect(helper.largestOptionTermLength(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.largestOptionTermLength(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.largestOptionTermLength(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..d28747b67 --- /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('sortCommands', () => { + 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 sortCommands:true then commands sorted', () => { + const program = new commander.Command(); + program + .configureHelp({ sortCommands: 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..1f4a9f9ad --- /dev/null +++ b/tests/help.sortOptions.test.js @@ -0,0 +1,41 @@ +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', () => { + const program = new commander.Command(); + program + .configureHelp({ sortOptions: true }) + .option('--zzz', 'desc') + .option('--aaa', 'desc') + .option('--bbb', 'desc'); + const helper = program.createHelp(); + const visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + }); + + test('when sortOptions:true then options sorted on name not flags', () => { + 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 visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + }); +}); diff --git a/tests/help.test.js b/tests/help.test.js deleted file mode 100644 index 8b659eba4..000000000 --- a/tests/help.test.js +++ /dev/null @@ -1,529 +0,0 @@ -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). - -// Not testing formatHelp here, tested implicitly by help tests in other files. - -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); - }); -}); - -describe('sortCommands', () => { - 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 sortCommands:true then commands sorted', () => { - const program = new commander.Command(); - program - .configureHelp({ sortCommands: 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']); - }); -}); - -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', () => { - const program = new commander.Command(); - program - .configureHelp({ sortOptions: true }) - .option('--zzz', 'desc') - .option('--aaa', 'desc') - .option('--bbb', 'desc'); - const helper = program.createHelp(); - const visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); - expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); - }); - - test('when sortOptions:true then options sorted on name not flags', () => { - 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 visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); - expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); - }); -}); - -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']); - }); -}); - -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('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' }]); - }); -}); - -// commandTerm does not currently respect helpOption or ignore hidden options, so not testing those. -describe('commandTerm', () => { - test('when plain command then returns name', () => { - const command = new commander.Command('program'); - const helper = new commander.Help(); - expect(helper.commandTerm(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.commandTerm(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.commandTerm(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.commandTerm(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.commandTerm(command)).toEqual('program|alias [options] '); - }); -}); - -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); - }); -}); - -describe('largestCommandTermLength', () => { - test('when no commands then returns zero', () => { - const program = new commander.Command(); - const helper = new commander.Help(); - expect(helper.largestCommandTermLength(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.largestCommandTermLength(program, helper)).toEqual(helper.commandTerm(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.largestCommandTermLength(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.largestCommandTermLength(program, helper)).toEqual('help [command]'.length); - }); -}); - -describe('largestOptionTermLength', () => { - test('when no option then returns zero', () => { - const program = new commander.Command(); - program.helpOption(false); - const helper = new commander.Help(); - expect(helper.largestOptionTermLength(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.largestOptionTermLength(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.largestOptionTermLength(program, helper)).toEqual(longestOptionFlags.length); - }); -}); - -describe('largestArgTermLength', () => { - test('when no arguments then returns zero', () => { - const program = new commander.Command(); - const helper = new commander.Help(); - expect(helper.largestArgTermLength(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.largestArgTermLength(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.largestArgTermLength(program, helper)).toEqual('longest'.length); - }); -}); - -describe('commandUsage', () => { - test('when single program then "Usage: program [options]"', () => { - const program = new commander.Command(); - program.name('program'); - const helper = new commander.Help(); - expect(helper.commandUsage(program)).toEqual('Usage: program [options]'); - }); - - test('when multi program then "Usage: program [options] [command]"', () => { - const program = new commander.Command(); - program.name('program'); - program.command('sub'); - const helper = new commander.Help(); - expect(helper.commandUsage(program)).toEqual('Usage: 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('Usage: 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('Usage: 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('Usage: program [options] '); - }); -}); - -describe('commandDescription', () => { - test('when program has no description then empty string', () => { - const program = new commander.Command(); - const helper = new commander.Help(); - expect(helper.commandDescription(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.commandDescription(program)).toEqual(description); - }); -}); - -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")'); - }); -}); - -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); - }); -}); - -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); - }); -}); 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..9ff0a0568 --- /dev/null +++ b/tests/help.visibleOptions.test.js @@ -0,0 +1,38 @@ +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']); + }); +}); 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 32e2ff600..000000000 --- a/tests/helpwrap.test.js +++ /dev/null @@ -1,115 +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 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); -}); - -// test for argsDescription passed to command ???? From c26f94320063b8d66aa5f71d41a1f7ebc679f96b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 4 Oct 2020 11:42:40 +1300 Subject: [PATCH 49/66] Add tests for createHelp and configureHelp --- tests/command.configureHelp.test.js | 31 +++++++++++++++++++++++++++++ tests/command.createHelp.test.js | 18 +++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/command.configureHelp.test.js create mode 100644 tests/command.createHelp.test.js 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'); +}); From 370781424471ceede9c87d3e3bfdfa7a0f68a2a3 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 4 Oct 2020 19:37:24 +1300 Subject: [PATCH 50/66] Add JSDoc for Help. Rename methods. Delete @api public as default. --- index.js | 169 ++++++++++++------ ...=> help.longestArgumentTermLength.test.js} | 8 +- ... => help.longestCommandTermLength.test.js} | 12 +- ...s => help.longestOptionTermLength.test.js} | 8 +- typings/commander-tests.ts | 6 +- typings/index.d.ts | 6 +- 6 files changed, 130 insertions(+), 79 deletions(-) rename tests/{help.largestArgTermLength.test.js => help.longestArgumentTermLength.test.js} (75%) rename tests/{help.largestCommandTermLength.test.js => help.longestCommandTermLength.test.js} (81%) rename tests/{help.largestOptionTermLength.test.js => help.longestOptionTermLength.test.js} (80%) diff --git a/index.js b/index.js index bb229e42a..97bfeac34 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,6 @@ const fs = require('fs'); // @ts-check // Although this is a class, methods are static in style to allow override using subclass or just functions. -// (Need to reconcile what is private when decide public/private methods????) class Help { constructor() { this.columns = process.stdout.columns || 80; @@ -18,14 +17,21 @@ class Help { 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._lazyHasImplicitHelpCommand()) { // Create a command matching the implicit help command. const args = cmd._helpCommandnameAndArgs.split(/ +/); const helpCommand = cmd.createCommand(args.shift()) - .helpOption(false) - .description(cmd._helpCommandDescription); + .helpOption(false); + helpCommand.description(cmd._helpCommandDescription); helpCommand._parseExpectedArgs(args); visibleCommands.push(helpCommand); } @@ -37,6 +43,13 @@ class Help { 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 @@ -61,6 +74,13 @@ class Help { 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) => { @@ -70,6 +90,13 @@ class Help { return []; } + /** + * Get the command term to show in the list of subcommands. + * + * @param {Command} cmd + * @returns {string} + */ + commandTerm(cmd) { // Legacy. Ignores custom usage string, and nested commands. const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); @@ -79,28 +106,66 @@ class Help { (args ? ' ' + args : ''); } + /** + * Get the option term to show in the list of options. + * + * @param {Option} option + * @returns {string} + */ + optionTerm(option) { return `${option.flags}`; } - largestCommandTermLength(cmd, helper) { + /** + * Get the longest command term length. + * + * @param {Command} cmd + * @param {Help} helper + * @returns {number} + */ + + longestCommandTermLength(cmd, helper) { return helper.visibleCommands(cmd).reduce((max, command) => { return Math.max(max, helper.commandTerm(command).length); }, 0); }; - largestOptionTermLength(cmd, helper) { + /** + * 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); }; - largestArgTermLength(cmd, helper) { + /** + * 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; @@ -114,15 +179,23 @@ class Help { return 'Usage: ' + parentCmdNames + cmdName + ' ' + cmd.usage(); } + /** + * Get the command description to show in the list of subcommands. + * + * @param {Command} cmd + * @returns {string} + */ + commandDescription(cmd) { + // @ts-ignore: overloaded return type return cmd.description(); } /** - * Calculate the full description, including defaultValue etc. + * Get the option description to show in the list of options. * + * @param {Option} option * @return {string} - * @api public */ optionDescription(option) { @@ -144,6 +217,14 @@ class Help { 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; @@ -153,7 +234,7 @@ class Help { const descriptionWidth = columns - termWidth - itemIndentWidth - itemSeparatorWidth; function formatItem(term, description) { if (description) { - return term.padEnd(termWidth + itemSeparatorWidth) + helper.wrap(description, descriptionWidth, termWidth + itemSeparatorWidth, helper); + return term.padEnd(termWidth + itemSeparatorWidth) + helper.wrap(description, descriptionWidth, termWidth + itemSeparatorWidth); } return term; }; @@ -166,6 +247,7 @@ class Help { // Description if (cmd.description()) { + // @ts-ignore: overloaded return type output = output.concat([cmd.description(), '']); } @@ -196,11 +278,19 @@ class Help { 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.largestOptionTermLength(cmd, helper), - helper.largestCommandTermLength(cmd, helper), - helper.largestArgTermLength(cmd, helper) + helper.longestOptionTermLength(cmd, helper), + helper.longestCommandTermLength(cmd, helper), + helper.longestArgumentTermLength(cmd, helper) ); }; @@ -213,8 +303,8 @@ class Help { * @param {number} width * @param {number} indent * @return {string} - * @api private */ + wrap(str, width, indent) { // Detect manually wrapped and indented strings by searching for line breaks // followed by multiple spaces/tabs. @@ -241,7 +331,6 @@ class Option { * * @param {string} flags * @param {string} [description] - * @api public */ constructor(flags, description) { @@ -273,7 +362,6 @@ class Option { * @param {any} value * @param {string} [description] * @return {Option} - * @api public */ default(value, description) { @@ -287,7 +375,6 @@ class Option { * * @param {Function} [fn] * @return {Option} - * @api public */ argParser(fn) { @@ -300,7 +387,6 @@ class Option { * * @param {boolean} [value] * @return {Option} - * @api public */ makeOptionMandatory(value) { @@ -313,7 +399,6 @@ class Option { * * @param {boolean} [value] * @return {Option} - * @api public */ hideHelp(value) { @@ -326,7 +411,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); @@ -337,7 +421,6 @@ class Option { * * @param {string[]} values * @return {Option} - * @api public */ choices(values) { @@ -355,7 +438,6 @@ class Option { * Return option name. * * @return {string} - * @api public */ name() { @@ -418,7 +500,6 @@ class Command extends EventEmitter { * Initialize a new `Command`. * * @param {string} [name] - * @api public */ constructor(name) { @@ -483,7 +564,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) { @@ -535,7 +615,6 @@ class Command extends EventEmitter { * * @param {string} [name] * @return {Command} new command - * @api public */ createCommand(name) { @@ -547,7 +626,6 @@ class Command extends EventEmitter { * or by supplying routines using configureHelp(). * * @return {Help} - * @api public */ createHelp() { @@ -569,7 +647,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) { @@ -598,8 +675,6 @@ class Command extends EventEmitter { /** * Define argument syntax for the command. - * - * @api public */ arguments(desc) { @@ -614,7 +689,6 @@ 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) { @@ -693,7 +767,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) { @@ -743,7 +816,6 @@ class Command extends EventEmitter { * * @param {Function} fn * @return {Command} `this` command for chaining - * @api public */ action(fn) { @@ -963,7 +1035,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) { @@ -981,7 +1052,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) { @@ -998,7 +1068,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; @@ -1010,7 +1079,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; @@ -1023,7 +1091,6 @@ Read more on https://git.io/JJc0W`); * * @param {boolean} value * @return {Command} `this` command for chaining - * @api public */ storeOptionsAsProperties(value) { @@ -1041,7 +1108,6 @@ Read more on https://git.io/JJc0W`); * * @param {boolean} value * @return {Command} `this` command for chaining - * @api public */ passCommandToAction(value) { @@ -1096,7 +1162,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) { @@ -1108,7 +1173,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'; } @@ -1124,7 +1189,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); @@ -1138,9 +1203,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; } @@ -1171,7 +1236,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) { @@ -1196,9 +1260,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; } @@ -1424,7 +1488,6 @@ Read more on https://git.io/JJc0W`); * * @param {String[]} argv * @return {{operands: String[], unknown: String[]}} - * @api public */ parseOptions(argv) { @@ -1520,7 +1583,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) { @@ -1627,7 +1689,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) { @@ -1648,12 +1709,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; @@ -1668,7 +1727,6 @@ Read more on https://git.io/JJc0W`); * * @param {string} [alias] * @return {string|Command} - * @api public */ alias(alias) { @@ -1693,7 +1751,6 @@ Read more on https://git.io/JJc0W`); * * @param {string[]} [aliases] * @return {string[]|Command} - * @api public */ aliases(aliases) { @@ -1709,7 +1766,6 @@ Read more on https://git.io/JJc0W`); * * @param {string} [str] * @return {String|Command} - * @api public */ usage(str) { @@ -1735,7 +1791,6 @@ Read more on https://git.io/JJc0W`); * * @param {string} [str] * @return {String|Command} - * @api public */ name(str) { @@ -1748,7 +1803,6 @@ Read more on https://git.io/JJc0W`); * Return program help documentation. * * @return {string} - * @api public */ helpInformation() { @@ -1780,7 +1834,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) { @@ -1823,7 +1876,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) { @@ -1847,7 +1899,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) { diff --git a/tests/help.largestArgTermLength.test.js b/tests/help.longestArgumentTermLength.test.js similarity index 75% rename from tests/help.largestArgTermLength.test.js rename to tests/help.longestArgumentTermLength.test.js index 0e172c899..4a0340d39 100644 --- a/tests/help.largestArgTermLength.test.js +++ b/tests/help.longestArgumentTermLength.test.js @@ -3,11 +3,11 @@ 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('largestArgTermLength', () => { +describe('longestArgumentTermLength', () => { test('when no arguments then returns zero', () => { const program = new commander.Command(); const helper = new commander.Help(); - expect(helper.largestArgTermLength(program, helper)).toEqual(0); + expect(helper.longestArgumentTermLength(program, helper)).toEqual(0); }); test('when has argument description then returns argument length', () => { @@ -15,7 +15,7 @@ describe('largestArgTermLength', () => { program.arguments(''); program.description('dummy', { wonder: 'wonder description' }); const helper = new commander.Help(); - expect(helper.largestArgTermLength(program, helper)).toEqual('wonder'.length); + expect(helper.longestArgumentTermLength(program, helper)).toEqual('wonder'.length); }); test('when has multiple argument descriptions then returns longest', () => { @@ -27,6 +27,6 @@ describe('largestArgTermLength', () => { beta: 'x' }); const helper = new commander.Help(); - expect(helper.largestArgTermLength(program, helper)).toEqual('longest'.length); + expect(helper.longestArgumentTermLength(program, helper)).toEqual('longest'.length); }); }); diff --git a/tests/help.largestCommandTermLength.test.js b/tests/help.longestCommandTermLength.test.js similarity index 81% rename from tests/help.largestCommandTermLength.test.js rename to tests/help.longestCommandTermLength.test.js index f38c81c92..8cd39013a 100644 --- a/tests/help.largestCommandTermLength.test.js +++ b/tests/help.longestCommandTermLength.test.js @@ -3,11 +3,11 @@ 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('largestCommandTermLength', () => { +describe('longestCommandTermLength', () => { test('when no commands then returns zero', () => { const program = new commander.Command(); const helper = new commander.Help(); - expect(helper.largestCommandTermLength(program, helper)).toEqual(0); + expect(helper.longestCommandTermLength(program, helper)).toEqual(0); }); test('when command and no help then returns length of term', () => { @@ -17,7 +17,7 @@ describe('largestCommandTermLength', () => { .addHelpCommand(false) .addCommand(sub); const helper = new commander.Help(); - expect(helper.largestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); + expect(helper.longestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); }); test('when command with arg and no help then returns length of term', () => { @@ -27,7 +27,7 @@ describe('largestCommandTermLength', () => { .addHelpCommand(false) .addCommand(sub); const helper = new commander.Help(); - expect(helper.largestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); + expect(helper.longestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); }); test('when multiple commands then returns longest length', () => { @@ -39,7 +39,7 @@ describe('largestCommandTermLength', () => { .command(longestCommandName, 'desc') .command('after', 'desc'); const helper = new commander.Help(); - expect(helper.largestCommandTermLength(program, helper)).toEqual(longestCommandName.length); + expect(helper.longestCommandTermLength(program, helper)).toEqual(longestCommandName.length); }); test('when just help command then returns length of help term', () => { @@ -47,6 +47,6 @@ describe('largestCommandTermLength', () => { program .addHelpCommand(true); const helper = new commander.Help(); - expect(helper.largestCommandTermLength(program, helper)).toEqual('help [command]'.length); + expect(helper.longestCommandTermLength(program, helper)).toEqual('help [command]'.length); }); }); diff --git a/tests/help.largestOptionTermLength.test.js b/tests/help.longestOptionTermLength.test.js similarity index 80% rename from tests/help.largestOptionTermLength.test.js rename to tests/help.longestOptionTermLength.test.js index 256387cd8..81731accd 100644 --- a/tests/help.largestOptionTermLength.test.js +++ b/tests/help.longestOptionTermLength.test.js @@ -3,18 +3,18 @@ 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('largestOptionTermLength', () => { +describe('longestOptionTermLength', () => { test('when no option then returns zero', () => { const program = new commander.Command(); program.helpOption(false); const helper = new commander.Help(); - expect(helper.largestOptionTermLength(program, helper)).toEqual(0); + 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.largestOptionTermLength(program, helper)).toEqual('-h, --help'.length); + expect(helper.longestOptionTermLength(program, helper)).toEqual('-h, --help'.length); }); test('when multiple option then returns longest length', () => { @@ -25,6 +25,6 @@ describe('largestOptionTermLength', () => { .option(longestOptionFlags) .option('--after'); const helper = new commander.Help(); - expect(helper.largestOptionTermLength(program, helper)).toEqual(longestOptionFlags.length); + expect(helper.longestOptionTermLength(program, helper)).toEqual(longestOptionFlags.length); }); }); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 15b11cc06..7b23bdedc 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -273,9 +273,9 @@ const visibleCommands: commander.Command[] = helper.visibleCommands(helperComman const visibleOptions: commander.Option[] = helper.visibleOptions(helperCommand); const visibleArguments: Array<{ term: string; description: string}> = helper.visibleArguments(helperCommand); -const widestCommand: number = helper.largestCommandTermLength(helperCommand, helper); -const widestOption: number = helper.largestOptionTermLength(helperCommand, helper); -const widestArgument: number = helper.largestArgTermLength(helperCommand, helper); +const widestCommand: number = helper.longestCommandTermLength(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); diff --git a/typings/index.d.ts b/typings/index.d.ts index cd7b98a6a..c30458ef1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -87,9 +87,9 @@ declare namespace commander { visibleOptions(cmd: Command): Option[]; visibleArguments(cmd: Command): Array<{ term: string; description: string}>; - largestCommandTermLength(cmd: Command, helper: Help): number; - largestOptionTermLength(cmd: Command, helper: Help): number; - largestArgTermLength(cmd: Command, helper: Help): number; + longestCommandTermLength(cmd: Command, helper: Help): number; + longestOptionTermLength(cmd: Command, helper: Help): number; + longestArgumentTermLength(cmd: Command, helper: Help): number; padWidth(cmd: Command, helper: Help): number; wrap(str: string, width: number, indent: number): string; From d99ec224b259a905da3a15afba86ffe6a22f6c76 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 4 Oct 2020 19:51:36 +1300 Subject: [PATCH 51/66] Add TSDoc for Help --- index.js | 2 +- typings/index.d.ts | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 97bfeac34..6245d3f5e 100644 --- a/index.js +++ b/index.js @@ -279,7 +279,7 @@ class Help { } /** - * Calculate the pad width from the maximum term length.. + * Calculate the pad width from the maximum term length. * * @param {Command} cmd * @param {Help} helper diff --git a/typings/index.d.ts b/typings/index.d.ts index c30458ef1..27efac026 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -73,27 +73,47 @@ declare namespace commander { type OptionConstructor = new (flags: string, description?: string) => Option; interface Help { + /** output columns, long lines are wrapped to fit */ columns: number; sortCommands: boolean; sortOptions: boolean; + /** Get the command term to show in the list of subcommands. */ commandTerm(cmd: Command): string; - commandUsage(cmd: Command): string; + /** Get the command description to show in the list of subcommands. */ commandDescription(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 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. */ longestCommandTermLength(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; From 9133df3379bbceff7d77b8be636c7b79a0c64fd8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 5 Oct 2020 00:02:59 +1300 Subject: [PATCH 52/66] Test special caes of implicit help flags --- tests/help.visibleOptions.test.js | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/help.visibleOptions.test.js b/tests/help.visibleOptions.test.js index 9ff0a0568..e34749460 100644 --- a/tests/help.visibleOptions.test.js +++ b/tests/help.visibleOptions.test.js @@ -36,3 +36,35 @@ describe('visibleOptions', () => { 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([]); + }); +}); From 82d43f56f8699620a609a3c9e91fd9dd8b480fa1 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 7 Oct 2020 21:17:14 +1300 Subject: [PATCH 53/66] Clarify method naming --- index.js | 30 ++++++++++++++------- tests/help.commandDescription.test.js | 6 ++--- tests/help.commandTerm.test.js | 14 +++++----- tests/help.longestCommandTermLength.test.js | 12 ++++----- typings/commander-tests.ts | 5 ++-- typings/index.d.ts | 7 ++--- 6 files changed, 44 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index 6245d3f5e..05ddaff32 100644 --- a/index.js +++ b/index.js @@ -97,7 +97,7 @@ class Help { * @returns {string} */ - commandTerm(cmd) { + subcommandTerm(cmd) { // Legacy. Ignores custom usage string, and nested commands. const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); return cmd._name + @@ -125,9 +125,9 @@ class Help { * @returns {number} */ - longestCommandTermLength(cmd, helper) { + longestSubcommandTermLength(cmd, helper) { return helper.visibleCommands(cmd).reduce((max, command) => { - return Math.max(max, helper.commandTerm(command).length); + return Math.max(max, helper.subcommandTerm(command).length); }, 0); }; @@ -180,7 +180,7 @@ class Help { } /** - * Get the command description to show in the list of subcommands. + * Get the description for the command. * * @param {Command} cmd * @returns {string} @@ -191,6 +191,18 @@ class Help { 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. * @@ -246,9 +258,9 @@ class Help { let output = [helper.commandUsage(cmd), '']; // Description - if (cmd.description()) { - // @ts-ignore: overloaded return type - output = output.concat([cmd.description(), '']); + const commandDescription = helper.commandDescription(cmd); + if (commandDescription.length > 0) { + output = output.concat([commandDescription, '']); } // Arguments @@ -269,7 +281,7 @@ class Help { // Commands const commandList = helper.visibleCommands(cmd).map((cmd) => { - return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd)); + return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd)); }); if (commandList.length > 0) { output = output.concat(['Commands:', formatList(commandList), '']); @@ -289,7 +301,7 @@ class Help { padWidth(cmd, helper) { return Math.max( helper.longestOptionTermLength(cmd, helper), - helper.longestCommandTermLength(cmd, helper), + helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper) ); }; diff --git a/tests/help.commandDescription.test.js b/tests/help.commandDescription.test.js index c03a1f515..49eb797cd 100644 --- a/tests/help.commandDescription.test.js +++ b/tests/help.commandDescription.test.js @@ -3,11 +3,11 @@ 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('commandDescription', () => { +describe('subcommandDescription', () => { test('when program has no description then empty string', () => { const program = new commander.Command(); const helper = new commander.Help(); - expect(helper.commandDescription(program)).toEqual(''); + expect(helper.subcommandDescription(program)).toEqual(''); }); test('when program has description then return description', () => { @@ -15,6 +15,6 @@ describe('commandDescription', () => { const program = new commander.Command(); program.description(description); const helper = new commander.Help(); - expect(helper.commandDescription(program)).toEqual(description); + expect(helper.subcommandDescription(program)).toEqual(description); }); }); diff --git a/tests/help.commandTerm.test.js b/tests/help.commandTerm.test.js index 543685960..cfb003a3e 100644 --- a/tests/help.commandTerm.test.js +++ b/tests/help.commandTerm.test.js @@ -3,33 +3,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). -// commandTerm does not currently respect helpOption or ignore hidden options, so not testing those. -describe('commandTerm', () => { +// 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.commandTerm(command)).toEqual('program'); + 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.commandTerm(command)).toEqual('program|alias'); + 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.commandTerm(command)).toEqual('program [options]'); + 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.commandTerm(command)).toEqual('program '); + expect(helper.subcommandTerm(command)).toEqual('program '); }); test('when command has everything then returns name|alias [options] ', () => { @@ -38,6 +38,6 @@ describe('commandTerm', () => { .option('-a,--all') .arguments(''); const helper = new commander.Help(); - expect(helper.commandTerm(command)).toEqual('program|alias [options] '); + expect(helper.subcommandTerm(command)).toEqual('program|alias [options] '); }); }); diff --git a/tests/help.longestCommandTermLength.test.js b/tests/help.longestCommandTermLength.test.js index 8cd39013a..b63601558 100644 --- a/tests/help.longestCommandTermLength.test.js +++ b/tests/help.longestCommandTermLength.test.js @@ -3,11 +3,11 @@ 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('longestCommandTermLength', () => { +describe('longestSubcommandTermLength', () => { test('when no commands then returns zero', () => { const program = new commander.Command(); const helper = new commander.Help(); - expect(helper.longestCommandTermLength(program, helper)).toEqual(0); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(0); }); test('when command and no help then returns length of term', () => { @@ -17,7 +17,7 @@ describe('longestCommandTermLength', () => { .addHelpCommand(false) .addCommand(sub); const helper = new commander.Help(); - expect(helper.longestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(helper.subcommandTerm(sub).length); }); test('when command with arg and no help then returns length of term', () => { @@ -27,7 +27,7 @@ describe('longestCommandTermLength', () => { .addHelpCommand(false) .addCommand(sub); const helper = new commander.Help(); - expect(helper.longestCommandTermLength(program, helper)).toEqual(helper.commandTerm(sub).length); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(helper.subcommandTerm(sub).length); }); test('when multiple commands then returns longest length', () => { @@ -39,7 +39,7 @@ describe('longestCommandTermLength', () => { .command(longestCommandName, 'desc') .command('after', 'desc'); const helper = new commander.Help(); - expect(helper.longestCommandTermLength(program, helper)).toEqual(longestCommandName.length); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual(longestCommandName.length); }); test('when just help command then returns length of help term', () => { @@ -47,6 +47,6 @@ describe('longestCommandTermLength', () => { program .addHelpCommand(true); const helper = new commander.Help(); - expect(helper.longestCommandTermLength(program, helper)).toEqual('help [command]'.length); + expect(helper.longestSubcommandTermLength(program, helper)).toEqual('help [command]'.length); }); }); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 7b23bdedc..62d3c97fb 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -263,9 +263,10 @@ helper.columns = 3; helper.sortCommands = true; helper.sortOptions = false; -const commandTermStr: string = helper.commandTerm(helperCommand); +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); @@ -273,7 +274,7 @@ const visibleCommands: commander.Command[] = helper.visibleCommands(helperComman const visibleOptions: commander.Option[] = helper.visibleOptions(helperCommand); const visibleArguments: Array<{ term: string; description: string}> = helper.visibleArguments(helperCommand); -const widestCommand: number = helper.longestCommandTermLength(helperCommand, helper); +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); diff --git a/typings/index.d.ts b/typings/index.d.ts index 27efac026..a7d65379e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -79,9 +79,9 @@ declare namespace commander { sortOptions: boolean; /** Get the command term to show in the list of subcommands. */ - commandTerm(cmd: Command): string; + subcommandTerm(cmd: Command): string; /** Get the command description to show in the list of subcommands. */ - commandDescription(cmd: Command): string; + 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. */ @@ -89,6 +89,7 @@ declare namespace commander { /** Get the command usage to be displayed at the top of the built-in help. */ commandUsage(cmd: Command): string; + 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[]; @@ -98,7 +99,7 @@ declare namespace commander { visibleArguments(cmd: Command): Array<{ term: string; description: string}>; /** Get the longest command term length. */ - longestCommandTermLength(cmd: Command, helper: Help): number; + longestSubcommandTermLength(cmd: Command, helper: Help): number; /** Get the longest option term length. */ longestOptionTermLength(cmd: Command, helper: Help): number; /** Get the longest argument term length. */ From 8c0ae2e4bc5296360e3aeba1c0aad2906efab878 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 9 Oct 2020 20:40:14 +1300 Subject: [PATCH 54/66] Shift the Usage prefix into formatHelp --- index.js | 4 ++-- tests/help.commandUsage.test.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 05ddaff32..cf6aa7f1e 100644 --- a/index.js +++ b/index.js @@ -176,7 +176,7 @@ class Help { for (let parentCmd = cmd.parent; parentCmd; parentCmd = parentCmd.parent) { parentCmdNames = parentCmd.name() + ' ' + parentCmdNames; } - return 'Usage: ' + parentCmdNames + cmdName + ' ' + cmd.usage(); + return parentCmdNames + cmdName + ' ' + cmd.usage(); } /** @@ -255,7 +255,7 @@ class Help { } // Usage - let output = [helper.commandUsage(cmd), '']; + let output = [`Usage: ${helper.commandUsage(cmd)}`, '']; // Description const commandDescription = helper.commandDescription(cmd); diff --git a/tests/help.commandUsage.test.js b/tests/help.commandUsage.test.js index 1d8fa3536..abeaceba0 100644 --- a/tests/help.commandUsage.test.js +++ b/tests/help.commandUsage.test.js @@ -4,19 +4,19 @@ const commander = require('../'); // There is some overlap with the higher level Command tests (which predate Help). describe('commandUsage', () => { - test('when single program then "Usage: program [options]"', () => { + 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('Usage: program [options]'); + expect(helper.commandUsage(program)).toEqual('program [options]'); }); - test('when multi program then "Usage: program [options] [command]"', () => { + 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('Usage: program [options] [command]'); + expect(helper.commandUsage(program)).toEqual('program [options] [command]'); }); test('when program has alias then usage includes alias', () => { @@ -25,7 +25,7 @@ describe('commandUsage', () => { .name('program') .alias('alias'); const helper = new commander.Help(); - expect(helper.commandUsage(program)).toEqual('Usage: program|alias [options]'); + expect(helper.commandUsage(program)).toEqual('program|alias [options]'); }); test('when help for subcommand then usage includes hierarchy', () => { @@ -35,7 +35,7 @@ describe('commandUsage', () => { const sub = program.command('sub') .name('sub'); const helper = new commander.Help(); - expect(helper.commandUsage(sub)).toEqual('Usage: program sub [options]'); + expect(helper.commandUsage(sub)).toEqual('program sub [options]'); }); test('when program has argument then usage includes argument', () => { @@ -44,6 +44,6 @@ describe('commandUsage', () => { .name('program') .arguments(''); const helper = new commander.Help(); - expect(helper.commandUsage(program)).toEqual('Usage: program [options] '); + expect(helper.commandUsage(program)).toEqual('program [options] '); }); }); From c6e7f583208af6769af18b6febf30bf7f68ee903 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 11 Oct 2020 19:43:59 +1300 Subject: [PATCH 55/66] Add Help class mention and reorder help --- Readme.md | 199 +++++++++++++++++++++++++++++------------------------- 1 file changed, 107 insertions(+), 92 deletions(-) diff --git a/Readme.md b/Readme.md index 04f38a080..54fdf1602 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,26 @@ 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 +- `sortCommands`: 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. + +``` +program.configureHelp({ + sortCommands: 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. From cc3a322f65d208c7ddc3299c075151b8ea75ddfa Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 14 Oct 2020 20:15:37 +1300 Subject: [PATCH 56/66] Add simple configure-help example --- examples/configure-help.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 examples/configure-help.js diff --git a/examples/configure-help.js b/examples/configure-help.js new file mode 100644 index 000000000..b05d8ee3f --- /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 (in part). + +program.configureHelp({ + sortCommands: 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 From 142bcaa9d8e3b261e301f620d79865f813074318 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 17 Oct 2020 11:51:20 +1300 Subject: [PATCH 57/66] Add example file to README --- Readme.md | 2 ++ examples/configure-help.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 54fdf1602..b39531e4b 100644 --- a/Readme.md +++ b/Readme.md @@ -638,6 +638,8 @@ The data properties are: 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({ sortCommands: true, diff --git a/examples/configure-help.js b/examples/configure-help.js index b05d8ee3f..aa8716820 100644 --- a/examples/configure-help.js +++ b/examples/configure-help.js @@ -4,7 +4,7 @@ const commander = require('../'); // include commander in git clone of commander const program = new commander.Command(); // This example shows a simple use of configureHelp. -// This is used as an example in the README (in part). +// This is used as an example in the README. program.configureHelp({ sortCommands: true, From ec77a66750f2add5e7da01a3b1d3727641d00eca Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 17 Oct 2020 12:05:27 +1300 Subject: [PATCH 58/66] Rename to sortSubcommands to match other naming --- Readme.md | 4 ++-- examples/configure-help.js | 2 +- index.js | 4 ++-- tests/help.sortCommands.test.js | 6 +++--- typings/commander-tests.ts | 4 ++-- typings/index.d.ts | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Readme.md b/Readme.md index b39531e4b..892a2852a 100644 --- a/Readme.md +++ b/Readme.md @@ -633,7 +633,7 @@ You can configure the Help behaviour by modifying data properties and methods us The data properties are: - `columns`: specify the wrap width, useful for unit tests -- `sortCommands`: sort the subcommands alphabetically +- `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. @@ -642,7 +642,7 @@ Example file: [configure-help.js](./examples/configure-help.js) ``` program.configureHelp({ - sortCommands: true, + sortSubcommands: true, subcommandTerm: (cmd) => cmd.name() // Just show the name, instead of short usage. }); ``` diff --git a/examples/configure-help.js b/examples/configure-help.js index aa8716820..d1b7b0d29 100644 --- a/examples/configure-help.js +++ b/examples/configure-help.js @@ -7,7 +7,7 @@ const program = new commander.Command(); // This is used as an example in the README. program.configureHelp({ - sortCommands: true, + sortSubcommands: true, subcommandTerm: (cmd) => cmd.name() // Just show the name, instead of short usage. }); diff --git a/index.js b/index.js index cf6aa7f1e..a0b8b149b 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ const fs = require('fs'); class Help { constructor() { this.columns = process.stdout.columns || 80; - this.sortCommands = false; + this.sortSubcommands = false; this.sortOptions = false; } @@ -35,7 +35,7 @@ class Help { helpCommand._parseExpectedArgs(args); visibleCommands.push(helpCommand); } - if (this.sortCommands) { + if (this.sortSubcommands) { visibleCommands.sort((a, b) => { return a.name().localeCompare(b.name()); }); diff --git a/tests/help.sortCommands.test.js b/tests/help.sortCommands.test.js index d28747b67..947490d14 100644 --- a/tests/help.sortCommands.test.js +++ b/tests/help.sortCommands.test.js @@ -3,7 +3,7 @@ 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('sortCommands', () => { +describe('sortSubcommands', () => { test('when unsorted then commands in order added', () => { const program = new commander.Command(); program @@ -15,10 +15,10 @@ describe('sortCommands', () => { expect(visibleCommandNames).toEqual(['ccc', 'aaa', 'bbb', 'help']); }); - test('when sortCommands:true then commands sorted', () => { + test('when sortSubcommands:true then commands sorted', () => { const program = new commander.Command(); program - .configureHelp({ sortCommands: true }) + .configureHelp({ sortSubcommands: true }) .command('ccc', 'desc') .command('aaa', 'desc') .command('bbb', 'desc'); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 62d3c97fb..6d8afee64 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -248,7 +248,7 @@ mySub.myFunction(); const createHelpInstance: commander.Help = program.createHelp(); const configureHelpThis: commander.Command = program.configureHelp({ - sortCommands: true, // override property + sortSubcommands: true, // override property visibleCommands: (cmd: commander.Command) => [], // override method customProperty: 'boo' }); @@ -260,7 +260,7 @@ const helperCommand = new commander.Command(); const helperOption = new commander.Option('-a, --all'); helper.columns = 3; -helper.sortCommands = true; +helper.sortSubcommands = true; helper.sortOptions = false; const subcommandTermStr: string = helper.subcommandTerm(helperCommand); diff --git a/typings/index.d.ts b/typings/index.d.ts index a7d65379e..aa66ea408 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -75,7 +75,7 @@ declare namespace commander { interface Help { /** output columns, long lines are wrapped to fit */ columns: number; - sortCommands: boolean; + sortSubcommands: boolean; sortOptions: boolean; /** Get the command term to show in the list of subcommands. */ From 5fd56e088e65825e845954ae84cb7d8ad9c3142b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 18 Oct 2020 14:51:04 +1300 Subject: [PATCH 59/66] Do not weaken configuration type, user can extend as required --- typings/commander-tests.ts | 3 +-- typings/index.d.ts | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 6d8afee64..a410f402f 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -249,8 +249,7 @@ mySub.myFunction(); const createHelpInstance: commander.Help = program.createHelp(); const configureHelpThis: commander.Command = program.configureHelp({ sortSubcommands: true, // override property - visibleCommands: (cmd: commander.Command) => [], // override method - customProperty: 'boo' + visibleCommands: (cmd: commander.Command) => [] // override method }); const helpConfiguration: commander.HelpConfiguration = program.configureHelp(); diff --git a/typings/index.d.ts b/typings/index.d.ts index aa66ea408..462a7bbd4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -118,9 +118,7 @@ declare namespace commander { formatHelp(cmd: Command, helper: Help): string; } type HelpConstructor = new () => Help; - interface HelpConfiguration extends Partial { - [key: string]: any; // allow extra custom properties - } + type HelpConfiguration = Partial; interface ParseOptions { from: 'node' | 'electron' | 'user'; From b0850b06d9319f0f797b680b996e4cf19b815875 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 18 Oct 2020 15:05:51 +1300 Subject: [PATCH 60/66] Do not cache implicit help command calculation so safer (no need, not being thrashed) --- index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index a0b8b149b..4d983cf41 100644 --- a/index.js +++ b/index.js @@ -26,7 +26,7 @@ class Help { visibleCommands(cmd) { const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden); - if (cmd._lazyHasImplicitHelpCommand()) { + if (cmd._hasImplicitHelpCommand()) { // Create a command matching the implicit help command. const args = cmd._helpCommandnameAndArgs.split(/ +/); const helpCommand = cmd.createCommand(args.shift()) @@ -545,7 +545,7 @@ 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'; @@ -705,9 +705,9 @@ class Command extends EventEmitter { 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; @@ -722,11 +722,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; }; /** @@ -1396,7 +1396,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 { From b6503f6dc555864231068ace9b2aed1b5cade00f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 18 Oct 2020 15:16:38 +1300 Subject: [PATCH 61/66] Add JSDoc for configureHelp --- index.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 4d983cf41..388fd509a 100644 --- a/index.js +++ b/index.js @@ -634,8 +634,8 @@ class Command extends EventEmitter { }; /** - * You can customise the help with either a subclass by overriding createHelp, - * or by supplying routines using configureHelp(). + * You can customise the help with a subclass of Help by overriding createHelp, + * or by overriding Help properties using configureHelp(). * * @return {Help} */ @@ -644,6 +644,14 @@ class Command extends EventEmitter { 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; @@ -1802,7 +1810,7 @@ Read more on https://git.io/JJc0W`); * Get or set the name of the command * * @param {string} [str] - * @return {String|Command} + * @return {string|Command} */ name(str) { From ea676a80424235d4e35c66166a20ded2ac3f681b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 18 Oct 2020 15:21:11 +1300 Subject: [PATCH 62/66] Add TSDoc --- index.js | 2 +- typings/index.d.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 388fd509a..71e8d2926 100644 --- a/index.js +++ b/index.js @@ -646,7 +646,7 @@ class Command extends EventEmitter { /** * You can customise the help by overriding Help properties using configureHelp(), - * or with a subclass of Help by overriding createHelp, + * or with a subclass of Help by overriding createHelp(). * * @param {Object} [configuration] - configuration options * @return {Command|Object} `this` command for chaining, or stored configuration diff --git a/typings/index.d.ts b/typings/index.d.ts index 462a7bbd4..542fbb64a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -220,8 +220,18 @@ 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; /** From 634828b5673920b31991f1141454223e007824de Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 18 Oct 2020 15:24:23 +1300 Subject: [PATCH 63/66] Add missing TSDoc --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 542fbb64a..5dea0b333 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -89,6 +89,7 @@ declare namespace commander { /** 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. */ From ee48a67423acd9f45f4863b24774b6d2cde553b0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 21 Oct 2020 22:51:16 +1300 Subject: [PATCH 64/66] Switch option sort to use attributeName, with negative after positive --- index.js | 6 +++++- tests/help.sortOptions.test.js | 38 ++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 71e8d2926..9c83d3635 100644 --- a/index.js +++ b/index.js @@ -68,7 +68,11 @@ class Help { } if (this.sortOptions) { visibleOptions.sort((a, b) => { - return a.name().localeCompare(b.name()); + const compare = a.attributeName().localeCompare(b.attributeName()); + if (compare === 0) { + return (a.negate) ? +1 : -1; + } + return compare; }); } return visibleOptions; diff --git a/tests/help.sortOptions.test.js b/tests/help.sortOptions.test.js index 1f4a9f9ad..69a502abb 100644 --- a/tests/help.sortOptions.test.js +++ b/tests/help.sortOptions.test.js @@ -15,7 +15,7 @@ describe('sortOptions', () => { expect(visibleOptionNames).toEqual(['zzz', 'aaa', 'bbb', 'help']); }); - test('when sortOptions:true then options sorted', () => { + test('when sortOptions:true then options sorted alphabetically', () => { const program = new commander.Command(); program .configureHelp({ sortOptions: true }) @@ -23,11 +23,11 @@ describe('sortOptions', () => { .option('--aaa', 'desc') .option('--bbb', 'desc'); const helper = program.createHelp(); - const visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); - expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + const visibleOptionNames = helper.visibleOptions(program).map(cmd => cmd.name()); + expect(visibleOptionNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); }); - test('when sortOptions:true then options sorted on name not flags', () => { + test('when short and long flags then sort on long flag (name)', () => { const program = new commander.Command(); program .configureHelp({ sortOptions: true }) @@ -35,7 +35,33 @@ describe('sortOptions', () => { .option('-n,--aaa', 'desc') .option('-o,--bbb', 'desc'); const helper = program.createHelp(); - const visibleCommandNames = helper.visibleOptions(program).map(cmd => cmd.name()); - expect(visibleCommandNames).toEqual(['aaa', 'bbb', 'help', 'zzz']); + 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']); }); }); From 67ea43cdfab0402b80913cd31292182433a24b20 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 22 Oct 2020 08:23:40 +1300 Subject: [PATCH 65/66] No need for string template literal --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 9c83d3635..9c11e0304 100644 --- a/index.js +++ b/index.js @@ -230,7 +230,7 @@ class Help { if (extraInfo.length > 0) { return `${option.description} (${extraInfo.join(', ')})`; } - return `${option.description}`; + return option.description; }; /** From 7266d89fc9ca318a878c0522efab21f7cb3c2753 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 22 Oct 2020 17:35:42 +1300 Subject: [PATCH 66/66] No need for string template literal --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 9c11e0304..1aad60969 100644 --- a/index.js +++ b/index.js @@ -118,7 +118,7 @@ class Help { */ optionTerm(option) { - return `${option.flags}`; + return option.flags; } /**