Skip to content

Commit

Permalink
Wrap and indent help descriptions for options and commands
Browse files Browse the repository at this point in the history
Co-authored-by: Ephigenia <love@ephigenia.de>
  • Loading branch information
shadowspawn and Ephigenia committed Sep 21, 2019
1 parent 1bed2ff commit 725ebd1
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 4 deletions.
61 changes: 57 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1153,11 +1153,16 @@ 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 +
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, minWidth);
}).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2, minWidth)])
.join('\n');
};

Expand All @@ -1174,11 +1179,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 @@ -1202,10 +1211,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 +1340,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
123 changes: 123 additions & 0 deletions tests/helpwrap.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 725ebd1

Please sign in to comment.