Skip to content

Commit

Permalink
Expand help customisation with .addHelpText (#1296)
Browse files Browse the repository at this point in the history
* Add new help events

* Add context for new help events

* Rename and use help context

* Suppress help output

* Slight tidy of legacy callback handling

* Shift help contextOptions up to public API

* Fix some spelling errors

* Start adding tests. Fix groupHelp.

* Test help event order

* Add tests on context

* Add missing semicolon

* Add context.error tests

* First cut at README adding events and removing callbacks

* Add typings and parameter descriptions

* Change from --help to postHelp event

* Update and add custom help examples

* Make help listener example more realistic

* Update example

* Call the old callback, deprecated

* First cut at .addHelp()

* Change name to addHelpText

* First round of tests for addHelpText

* Simplify help event context, remove log

* Assign write directly

* Add end-to-end and context checks for addHelpText

* Put back write wrapper to fix unit test

* Fix write mocks for help-as-error output

* Ignore falsy values for AddHelpText

* Remove the help override from addHelpText as not good fit

* Convert example to addHelpText

* Update README

* Add info about new .help param

* Remove excess space

* Update more examples

* Remove references to override

* Update docs with .addHelpText
  • Loading branch information
shadowspawn committed Sep 9, 2020
1 parent 693a40f commit d26a26d
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 100 deletions.
54 changes: 31 additions & 23 deletions Readme.md
Expand Up @@ -27,8 +27,8 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [Automated help](#automated-help)
- [Custom help](#custom-help)
- [.usage and .name](#usage-and-name)
- [.help(cb)](#helpcb)
- [.outputHelp(cb)](#outputhelpcb)
- [.help()](#help)
- [.outputHelp()](#outputhelp)
- [.helpInformation()](#helpinformation)
- [.helpOption(flags, description)](#helpoptionflags-description)
- [.addHelpCommand()](#addhelpcommand)
Expand Down Expand Up @@ -497,20 +497,18 @@ shell spawn --help
### Custom help
You can display extra information by listening for "--help".
You can add extra text to be displayed along with the built-in help.
Example file: [custom-help](./examples/custom-help)
```js
program
.option('-f, --foo', 'enable some foo');
// must be before .parse()
program.on('--help', () => {
console.log('');
console.log('Example call:');
console.log(' $ custom-help --help');
});
program.addHelpText('after', `
Example call:
$ custom-help --help`);
```
Yields the following help output:
Expand All @@ -526,6 +524,20 @@ Example call:
$ custom-help --help
```
The positions in order displayed are:
- `beforeAll`: add to the program for a global banner or header
- `before`: display extra information before built-in help
- `after`: display extra information after built-in help
- `afterAll`: add to the program for a global footer (epilog)
The positions "beforeAll" and "afterAll" apply to the command and all its subcommands.
The second parameter can be a string, or a function returning a string. The function is passed a context object for your convenience. The properties are:
- error: a boolean for whether the help is being displayed due to a usage error
- command: the Command which is displaying the help
### .usage and .name
These allow you to customise the usage description in the first line of the help. The name is otherwise
Expand All @@ -543,19 +555,17 @@ The help will start with:
Usage: my-command [global options] command
```
### .help(cb)
### .help()
Output help information and exit immediately. Optional callback cb allows post-processing of help text before it is displayed.
Output help information and exit immediately. You can optionally pass `{ error: true }` to display on stderr and exit with an error status.
### .outputHelp(cb)
### .outputHelp()
Output help information without exiting.
Optional callback cb allows post-processing of help text before it is displayed.
Output help information without exiting. You can optionally pass `{ error: true }` to display on stderr.
### .helpInformation()
Get the command help information as a string for processing or displaying yourself. (The text does not include the custom help
from `--help` listeners.)
Get the built-in command help information as a string for processing or displaying yourself.
### .helpOption(flags, description)
Expand Down Expand Up @@ -749,13 +759,11 @@ program
.option("-e, --exec_mode <mode>", "Which exec mode to use")
.action(function(cmd, options){
console.log('exec "%s" using %s mode', cmd, options.exec_mode);
}).on('--help', function() {
console.log('');
console.log('Examples:');
console.log('');
console.log(' $ deploy exec sequential');
console.log(' $ deploy exec async');
});
}).addHelpText('after', `
Examples:
$ deploy exec sequential
$ deploy exec async`
);
program.parse(process.argv);
```
Expand Down
16 changes: 9 additions & 7 deletions examples/custom-help
@@ -1,6 +1,7 @@
#!/usr/bin/env node

// This example displays some custom help text after the built-in help.
// This example shows a simple use of addHelpText.
// This is used as an example in the README.

// const { Command } = require('commander'); // (normal include)
const { Command } = require('../'); // include commander in git clone of commander repo
Expand All @@ -9,11 +10,12 @@ const program = new Command();
program
.option('-f, --foo', 'enable some foo');

// must be before .parse()
program.on('--help', () => {
console.log('');
console.log('Example call:');
console.log(' $ custom-help --help');
});
program.addHelpText('after', `
Example call:
$ custom-help --help`);

program.parse(process.argv);

// Try the following:
// node custom-help --help
33 changes: 33 additions & 0 deletions examples/custom-help-text.js
@@ -0,0 +1,33 @@
#!/usr/bin/env node

// This example shows using addHelpText.

// const { Command } = require('commander'); // (normal include)
const { Command } = require('../'); // include commander in git clone of commander repo
const program = new Command();

program.name('awesome');

program
.addHelpText('beforeAll', 'A W E S O M E\n')
.addHelpText('afterAll', (context) => {
if (context.error) {
return '\nHelp being displayed for an error';
}
return '\nSee web site for further help';
});

program
.command('extra')
.addHelpText('before', 'Note: the extra command does not do anything')
.addHelpText('after', `
Examples:
awesome extra --help
awesome help extra`);

program.parse();

// Try the following:
// node custom-help-text.js --help
// node custom-help-text.js extra --help
// node custom-help-text.js
14 changes: 6 additions & 8 deletions examples/deploy
Expand Up @@ -27,12 +27,10 @@ program
.option('-e, --exec_mode <mode>', 'Which exec mode to use')
.action(function(cmd, options) {
console.log('exec "%s" using %s mode', cmd, options.exec_mode);
}).on('--help', function() {
console.log(' Examples:');
console.log();
console.log(' $ deploy exec sequential');
console.log(' $ deploy exec async');
console.log();
});

}).addHelpText('after', `
Examples:
$ deploy exec sequential
$ deploy exec async`
);

program.parse(process.argv);
2 changes: 1 addition & 1 deletion examples/storeOptionsAsProperties-action.js
Expand Up @@ -3,7 +3,7 @@
// To avoid possible name clashes, you can change the default behaviour
// of storing the options values as properties on the command object.
// In addition, you can pass just the options to the action handler
// instead of a commmand object. This allows simpler code, and is more consistent
// instead of a command object. This allows simpler code, and is more consistent
// with the previous behaviour so less code changes from old code.
//
// Example output:
Expand Down
117 changes: 89 additions & 28 deletions index.js
Expand Up @@ -945,7 +945,7 @@ Read more on https://git.io/JJc0W`);
*/
_dispatchSubcommand(commandName, operands, unknown) {
const subCommand = this._findCommand(commandName);
if (!subCommand) this._helpAndError();
if (!subCommand) this.help({ error: true });

if (subCommand._executableHandler) {
this._executeSubCommand(subCommand, operands.concat(unknown));
Expand Down Expand Up @@ -980,7 +980,7 @@ Read more on https://git.io/JJc0W`);
} else {
if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) {
// probably missing subcommand and no handler, user needs help
this._helpAndError();
this.help({ error: true });
}

outputHelpIfRequested(this, parsed.unknown);
Expand Down Expand Up @@ -1011,7 +1011,7 @@ Read more on https://git.io/JJc0W`);
}
} else if (this.commands.length) {
// This command has subcommands and nothing hooked up at this level, so display help.
this._helpAndError();
this.help({ error: true });
} else {
// fall through for caller to handle after calling .parse()
}
Expand Down Expand Up @@ -1617,27 +1617,63 @@ Read more on https://git.io/JJc0W`);
.join('\n');
};

/**
* @api private
*/

_getHelpContext(contextOptions) {
contextOptions = contextOptions || {};
const context = { error: !!contextOptions.error };
let write;
if (context.error) {
write = (...args) => process.stderr.write(...args);
} else {
write = (...args) => process.stdout.write(...args);
}
context.write = contextOptions.write || write;
context.command = this;
return context;
}

/**
* Output help information for this command.
*
* When listener(s) are available for the helpLongFlag
* those callbacks are invoked.
* Outputs built-in help, and custom text added using `.addHelpText()`.
*
* @api public
* @param {Object} [contextOptions] - Can optionally pass in `{ error: true }` to write to stderr
*/

outputHelp(cb) {
if (!cb) {
cb = (passthru) => {
return passthru;
};
outputHelp(contextOptions) {
let deprecatedCallback;
if (typeof contextOptions === 'function') {
deprecatedCallback = contextOptions;
contextOptions = {};
}
const context = this._getHelpContext(contextOptions);

const groupListeners = [];
let command = this;
while (command) {
groupListeners.push(command); // ordered from current command to root
command = command.parent;
}
const cbOutput = cb(this.helpInformation());
if (typeof cbOutput !== 'string' && !Buffer.isBuffer(cbOutput)) {
throw new Error('outputHelp callback must return a string or a Buffer');

groupListeners.slice().reverse().forEach(command => command.emit('beforeAllHelp', context));
this.emit('beforeHelp', context);

let helpInformation = this.helpInformation();
if (deprecatedCallback) {
helpInformation = deprecatedCallback(helpInformation);
if (typeof helpInformation !== 'string' && !Buffer.isBuffer(helpInformation)) {
throw new Error('outputHelp callback must return a string or a Buffer');
}
}
process.stdout.write(cbOutput);
this.emit(this._helpLongFlag);
context.write(helpInformation);

this.emit(this._helpLongFlag); // deprecated
this.emit('afterHelp', context);
groupListeners.forEach(command => command.emit('afterAllHelp', context));
};

/**
Expand Down Expand Up @@ -1669,28 +1705,53 @@ Read more on https://git.io/JJc0W`);
/**
* Output help information and exit.
*
* @param {Function} [cb]
* Outputs built-in help, and custom text added using `.addHelpText()`.
*
* @param {Object} [contextOptions] - optionally pass in `{ error: true }` to write to stderr
* @api public
*/

help(cb) {
this.outputHelp(cb);
// exitCode: preserving original behaviour which was calling process.exit()
help(contextOptions) {
this.outputHelp(contextOptions);
let exitCode = process.exitCode || 0;
if (exitCode === 0 && contextOptions && typeof contextOptions !== 'function' && contextOptions.error) {
exitCode = 1;
}
// message: do not have all displayed text available so only passing placeholder.
this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)');
this._exit(exitCode, 'commander.help', '(outputHelp)');
};

/**
* Output help information and exit. Display for error situations.
* Add additional text to be displayed with the built-in help.
*
* @api private
* Position is 'before' or 'after' to affect just this command,
* and 'beforeAll' or 'afterAll' to affect this command and all its subcommands.
*
* @param {string} position - before or after built-in help
* @param {string | Function} text - string to add, or a function returning a string
* @return {Command} `this` command for chaining
*/

_helpAndError() {
this.outputHelp();
// message: do not have all displayed text available so only passing placeholder.
this._exit(1, 'commander.help', '(outputHelp)');
};
addHelpText(position, text) {
const allowedValues = ['beforeAll', 'before', 'after', 'afterAll'];
if (!allowedValues.includes(position)) {
throw new Error(`Unexpected value for position to addHelpText.
Expecting one of '${allowedValues.join("', '")}'`);
}
const helpEvent = `${position}Help`;
this.on(helpEvent, (context) => {
let helpStr;
if (typeof text === 'function') {
helpStr = text({ error: context.error, command: context.command });
} else {
helpStr = text;
}
// Ignore falsy value when nothing to output.
if (helpStr) {
context.write(`${helpStr}\n`);
}
});
return this;
}
};

/**
Expand Down

0 comments on commit d26a26d

Please sign in to comment.