From 725ebd1f7b7d5e513951f07e005738b63d27efa5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 21 Sep 2019 22:42:23 +1200 Subject: [PATCH 1/3] Wrap and indent help descriptions for options and commands Co-authored-by: Ephigenia --- index.js | 61 ++++++++++++++++++-- tests/helpwrap.test.js | 123 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 tests/helpwrap.test.js diff --git a/index.js b/index.js index 45863093a..a53f6000e 100644 --- a/index.js +++ b/index.js @@ -1153,11 +1153,16 @@ Command.prototype.padWidth = function() { Command.prototype.optionHelp = function() { var width = this.padWidth(); + var columns = process.stdout.columns || 80; + var descriptionWidth = columns - width - 4; + var minWidth = 40; + // Append the help information return this.options.map(function(option) { - return pad(option.flags, width) + ' ' + option.description + + const fullDesc = option.description + ((!option.negate && option.defaultValue !== undefined) ? ' (default: ' + JSON.stringify(option.defaultValue) + ')' : ''); - }).concat([pad(this._helpFlags, width) + ' ' + this._helpDescription]) + return pad(option.flags, width) + ' ' + optionalWrap(fullDesc, descriptionWidth, width + 2, minWidth); + }).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2, minWidth)]) .join('\n'); }; @@ -1174,11 +1179,15 @@ Command.prototype.commandHelp = function() { var commands = this.prepareCommands(); var width = this.padWidth(); + var columns = process.stdout.columns || 80; + var descriptionWidth = columns - width - 4; + var minWidth = 40; + return [ 'Commands:', commands.map(function(cmd) { var desc = cmd[1] ? ' ' + cmd[1] : ''; - return (desc ? pad(cmd[0], width) : cmd[0]) + desc; + return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2, minWidth); }).join('\n').replace(/^/gm, ' '), '' ].join('\n'); @@ -1202,10 +1211,12 @@ Command.prototype.helpInformation = function() { var argsDescription = this._argsDescription; if (argsDescription && this._args.length) { var width = this.padWidth(); + var columns = process.stdout.columns || 80; + var descriptionWidth = columns - width - 5; desc.push('Arguments:'); desc.push(''); this._args.forEach(function(arg) { - desc.push(' ' + pad(arg.name, width) + ' ' + argsDescription[arg.name]); + desc.push(' ' + pad(arg.name, width) + ' ' + wrap(argsDescription[arg.name], descriptionWidth, width + 4)); }); desc.push(''); } @@ -1329,6 +1340,48 @@ function pad(str, width) { 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) { + var regex = new RegExp('.{1,' + (width - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g'); + var lines = str.match(regex) || []; + var result = lines.map(function(line, i) { + if (line.slice(-1) === '\n') { + line = line.slice(0, line.length - 1); + } + return ((i > 0 && indent) ? Array(indent + 1).join(' ') : '') + line; + }).join('\n'); + return result; +} + +/** + * Optionally wrap the given str to a max width of width characters per line + * while indenting with indent spaces. Do not wrap the text when the minWidth + * is not hit. + * + * @param {String} str + * @param {Number} width + * @param {Number} indent + * @return {String} + * @api private + */ +function optionalWrap(str, width, indent, minWidth) { + // detected manually wrapped and indented strings by searching for line breaks + // followed by multiple spaces/tabs + if (str.match(/[\n]\s+/)) return str; + // check if minimum width is reached + if (width < minWidth) return str; + return wrap(str, width, indent); +} + /** * Output help information if necessary * diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js new file mode 100644 index 000000000..dd0f2cb07 --- /dev/null +++ b/tests/helpwrap.test.js @@ -0,0 +1,123 @@ +const commander = require('../'); + +// Test auto wrap and indent with some manual strings. + +test('when long option description then wrap and indent', () => { + const oldColumns = process.stdout.columns; + process.stdout.columns = 80; + const program = new commander.Command(); + program + .option('-x -extra-long-option-switch', 'kjsahdkajshkahd kajhsd akhds kashd kajhs dkha dkh aksd ka dkha kdh kasd ka kahs dkh sdkh askdh aksd kashdk ahsd kahs dkha skdh'); + + const expectedOutput = +`Usage: [options] + +Options: + -x -extra-long-option-switch kjsahdkajshkahd kajhsd akhds kashd kajhs dkha + dkh aksd ka dkha kdh kasd ka kahs dkh sdkh + askdh aksd kashdk ahsd kahs dkha skdh + -h, --help output usage information +`; + + expect(program.helpInformation()).toBe(expectedOutput); + process.stdout.columns = oldColumns; +}); + +test('when long option description and default then wrap and indent', () => { + const oldColumns = process.stdout.columns; + process.stdout.columns = 80; + const program = new commander.Command(); + program + .option('-x -extra-long-option ', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg'); + + const expectedOutput = +`Usage: [options] + +Options: + -x -extra-long-option kjsahdkajshkahd kajhsd akhds (default: "aaa + bbb ccc ddd eee fff ggg") + -h, --help output usage information +`; + + expect(program.helpInformation()).toBe(expectedOutput); + process.stdout.columns = oldColumns; +}); + +test('when long command description then wrap and indent', () => { + const oldColumns = process.stdout.columns; + process.stdout.columns = 80; + const program = new commander.Command(); + program + .option('-x -extra-long-option-switch', 'x') + .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); + + const expectedOutput = +`Usage: [options] [command] + +Options: + -x -extra-long-option-switch x + -h, --help output usage information + +Commands: + alpha Lorem mollit quis dolor ex do eu quis ad insa + a commodo esse. +`; + + expect(program.helpInformation()).toBe(expectedOutput); + process.stdout.columns = oldColumns; +}); + +test('when not enough room then help not wrapped', () => { + // Not wrapping if less than 40 columns available for wrapping. + const oldColumns = process.stdout.columns; + process.stdout.columns = 60; + const program = new commander.Command(); + const commandDescription = 'description text of very long command which should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.'; + program + .command('1234567801234567890x', commandDescription); + + const expectedOutput = +`Usage: [options] [command] + +Options: + -h, --help output usage information + +Commands: + 1234567801234567890x ${commandDescription} +`; + + expect(program.helpInformation()).toBe(expectedOutput); + process.stdout.columns = oldColumns; +}); + +test('when option descripton preformatted then only add small indent', () => { + const oldColumns = process.stdout.columns; + process.stdout.columns = 80; + // #396: leave custom format alone, apart from space-space indent + const optionSpec = '-t, --time '; + const program = new commander.Command(); + program + .option(optionSpec, +`select time + +Time can also be specified using special values: + "dawn" - From night to sunrise. +`); + + const expectedOutput = +`Usage: [options] + +Options: + ${optionSpec} select time + + Time can also be specified using special values: + "dawn" - From night to sunrise. + + -h, --help output usage information +`; + + expect(program.helpInformation()).toBe(expectedOutput); + process.stdout.columns = oldColumns; +}); + +// test for argsDescription passed to command ???? From 8d7d08016d7ce0af09e51c7e5ac4d17b5dd41851 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 22 Sep 2019 10:05:36 +1200 Subject: [PATCH 2/3] Code tidy from review suggestions --- index.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index a53f6000e..9f89d0535 100644 --- a/index.js +++ b/index.js @@ -1155,14 +1155,13 @@ Command.prototype.optionHelp = function() { var columns = process.stdout.columns || 80; var descriptionWidth = columns - width - 4; - var minWidth = 40; // Append the help information return this.options.map(function(option) { const fullDesc = option.description + ((!option.negate && option.defaultValue !== undefined) ? ' (default: ' + JSON.stringify(option.defaultValue) + ')' : ''); - return pad(option.flags, width) + ' ' + optionalWrap(fullDesc, descriptionWidth, width + 2, minWidth); - }).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2, minWidth)]) + return pad(option.flags, width) + ' ' + optionalWrap(fullDesc, descriptionWidth, width + 2); + }).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2)]) .join('\n'); }; @@ -1181,13 +1180,12 @@ Command.prototype.commandHelp = function() { var columns = process.stdout.columns || 80; var descriptionWidth = columns - width - 4; - var minWidth = 40; return [ 'Commands:', commands.map(function(cmd) { var desc = cmd[1] ? ' ' + cmd[1] : ''; - return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2, minWidth); + return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2); }).join('\n').replace(/^/gm, ' '), '' ].join('\n'); @@ -1353,19 +1351,18 @@ function pad(str, width) { function wrap(str, width, indent) { var regex = new RegExp('.{1,' + (width - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g'); var lines = str.match(regex) || []; - var result = lines.map(function(line, i) { + return lines.map(function(line, i) { if (line.slice(-1) === '\n') { line = line.slice(0, line.length - 1); } return ((i > 0 && indent) ? Array(indent + 1).join(' ') : '') + line; }).join('\n'); - return result; } /** * Optionally wrap the given str to a max width of width characters per line - * while indenting with indent spaces. Do not wrap the text when the minWidth - * is not hit. + * while indenting with indent spaces. Do not wrap if insufficient width or + * string is manually formatted. * * @param {String} str * @param {Number} width @@ -1373,12 +1370,14 @@ function wrap(str, width, indent) { * @return {String} * @api private */ -function optionalWrap(str, width, indent, minWidth) { - // detected manually wrapped and indented strings by searching for line breaks - // followed by multiple spaces/tabs +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; - // check if minimum width is reached + // 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); } From 97b5fee6f1418280a56c050f01f09a981a237bf6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 22 Sep 2019 11:10:18 +1200 Subject: [PATCH 3/3] Fix a (false positive) eslint report from command line that was not showing in editor --- tests/helpwrap.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index dd0f2cb07..9eb9dd6aa 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -97,8 +97,7 @@ test('when option descripton preformatted then only add small indent', () => { const optionSpec = '-t, --time '; const program = new commander.Command(); program - .option(optionSpec, -`select time + .option(optionSpec, `select time Time can also be specified using special values: "dawn" - From night to sunrise.