diff --git a/index.js b/index.js index 4bd6a14b7..ff132660e 100644 --- a/index.js +++ b/index.js @@ -1153,11 +1153,15 @@ Command.prototype.padWidth = function() { Command.prototype.optionHelp = function() { var width = this.padWidth(); + var columns = process.stdout.columns || 80; + var descriptionWidth = columns - width - 4; + // 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); + }).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2)]) .join('\n'); }; @@ -1174,11 +1178,14 @@ Command.prototype.commandHelp = function() { var commands = this.prepareCommands(); var width = this.padWidth(); + var columns = process.stdout.columns || 80; + var descriptionWidth = columns - width - 4; + 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); }).join('\n').replace(/^/gm, ' '), '' ].join('\n'); @@ -1202,10 +1209,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 +1338,49 @@ 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) || []; + 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'); +} + +/** + * 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 necessary * diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js new file mode 100644 index 000000000..9eb9dd6aa --- /dev/null +++ b/tests/helpwrap.test.js @@ -0,0 +1,122 @@ +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 ????