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

Wrap and indent help descriptions for options and commands #1051

Merged
merged 3 commits into from Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 56 additions & 4 deletions index.js
Expand Up @@ -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');
};

Expand All @@ -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');
Expand All @@ -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('');
}
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

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

The document parameter is missing minWidth.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Prompted by this, I moved minWidth to an internal detail.

* @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
*
Expand Down
122 changes: 122 additions & 0 deletions 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 <value>', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg');

const expectedOutput =
`Usage: [options]

Options:
-x -extra-long-option <value> 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 <HH:MM>';
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 ????