diff --git a/index.js b/index.js index 3efb86ab2..9d86ce1e5 100644 --- a/index.js +++ b/index.js @@ -1064,11 +1064,15 @@ 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 + + return pad(option.flags, width) + ' ' + optionalWrap(option.description, descriptionWidth, width + 2, minWidth) + ((!option.negate && option.defaultValue !== undefined) ? ' (default: ' + JSON.stringify(option.defaultValue) + ')' : ''); - }).concat([pad(this._helpFlags, width) + ' ' + this._helpDescription]) + }).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2, minWidth)]) .join('\n'); }; @@ -1085,11 +1089,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'); @@ -1113,10 +1121,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(''); } @@ -1238,6 +1248,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/test/test.command.helpWrap.js b/test/test.command.helpWrap.js new file mode 100644 index 000000000..15fff4ef3 --- /dev/null +++ b/test/test.command.helpWrap.js @@ -0,0 +1,34 @@ +var program = require('../') + , sinon = require('sinon').createSandbox() + , should = require('should'); + +process.stdout.columns = 80; + +// test should make sure that the description text of commands and options +// is wrapped at the columns width and indented correctly +program + .description('description of the command') + .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 ') + .option('-s', 'some other shorter description') + .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.') + .command('beta-gamma-delta', 'Consectetur tempor eiusmod occaecat veniam veniam Lorem anim reprehenderit ipsum amet.'); + +var expectedOutput = `Usage: [options] [command] + +description of the command + +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 + -s some other shorter description + -h, --help output usage information + +Commands: + alpha Lorem mollit quis dolor ex do eu quis ad insa + a commodo esse. + beta-gamma-delta Consectetur tempor eiusmod occaecat veniam + veniam Lorem anim reprehenderit ipsum amet. +`; + +program.helpInformation().should.equal(expectedOutput); diff --git a/test/test.command.helpWrapSkipped.js b/test/test.command.helpWrapSkipped.js new file mode 100644 index 000000000..e1870513c --- /dev/null +++ b/test/test.command.helpWrapSkipped.js @@ -0,0 +1,26 @@ +var program = require('../') + , sinon = require('sinon').createSandbox() + , should = require('should'); + +// test should make sure that the description is not wrapped when there’s +// insufficient space for the wrapped description and every line would only +// contains some words. +program + .command('alpha', `This text should also not be wrapped manually. Reprehenderit velit nulla nisi excepteur dolore cillum nisi reprehenderit.`) + .command( + 'betabetabteabteabetabetabteabteabetabetabteabteabetabasdsasdfsdf', + 'description text of very long command should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.' + ); + +var expectedOutput = `Usage: [options] [command] + +Options: + -h, --help output usage information + +Commands: + alpha This text should also not be wrapped manually. Reprehenderit velit nulla nisi excepteur dolore cillum nisi reprehenderit. + betabetabteabteabetabetabteabteabetabetabteabteabetabasdsasdfsdf description text of very long command should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu. +`; + +process.stdout.columns = 80; +program.helpInformation().should.equal(expectedOutput); diff --git a/test/test.command.helpWrappedManually.js b/test/test.command.helpWrappedManually.js new file mode 100644 index 000000000..8ad02fc7d --- /dev/null +++ b/test/test.command.helpWrappedManually.js @@ -0,0 +1,40 @@ +var program = require('../') + , sinon = require('sinon').createSandbox() + , should = require('should'); + +// make sure descriptions which are manually wrapped and indented are not +// changed by in the output to maintain backwards compatibility to <3.0 +program + .option( + '-t, --time ', + `select time + + Time can also be specified using special values: + + "dawn" - From night to sunrise. + "sunrise" - Time around sunrise. + "morning" - From sunrise to noon. + ` + ) + .option( + '-d, --date ', + `select date` + ); + +var expectedOutput = `Usage: [options] + +Options: + -t, --time select time + + Time can also be specified using special values: + + "dawn" - From night to sunrise. + "sunrise" - Time around sunrise. + "morning" - From sunrise to noon. + + -d, --date select date + -h, --help output usage information +`; + +process.stdout.columns = 80; +program.helpInformation().should.equal(expectedOutput);