Skip to content

Commit

Permalink
Merge pull request #1051 from shadowspawn/feature/pretty-help
Browse files Browse the repository at this point in the history
Wrap and indent help descriptions for options and commands
  • Loading branch information
shadowspawn committed Sep 22, 2019
2 parents 85d4c2b + 97b5fee commit 55386f4
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 4 deletions.
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
* @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 ????

0 comments on commit 55386f4

Please sign in to comment.