Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix wrapped & indented option, command and argument descriptions #956

Closed
wants to merge 12 commits into from
Closed
60 changes: 56 additions & 4 deletions index.js
Expand Up @@ -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) + ')' : '');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value should be added to the description for optionalWrap, rather than added afterwards. I suggest a temporary variable to help with readability.

e.g.

var desc = option.description +
       ((!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');
};

Expand All @@ -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');
Expand All @@ -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('');
}
Expand Down Expand Up @@ -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
*
Expand Down
34 changes: 34 additions & 0 deletions 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);
26 changes: 26 additions & 0 deletions 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);
40 changes: 40 additions & 0 deletions 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 <HH:MM>',
`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 <YYYY-MM-DD>',
`select date`
);

var expectedOutput = `Usage: [options]

Options:
-t, --time <HH:MM> 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 <YYYY-MM-DD> select date
-h, --help output usage information
`;

process.stdout.columns = 80;
program.helpInformation().should.equal(expectedOutput);