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

Expand help customisation with .addHelpText #1296

Merged
merged 39 commits into from Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
84d47d5
Add new help events
shadowspawn Jun 17, 2020
901b25e
Add context for new help events
shadowspawn Jun 19, 2020
243eae2
Rename and use help context
shadowspawn Jun 19, 2020
ab863a2
Suppress help output
shadowspawn Jun 19, 2020
c8440f7
Slight tidy of legacy callback handling
shadowspawn Jun 21, 2020
aee58dc
Shift help contextOptions up to public API
shadowspawn Jun 21, 2020
040bc4f
Fix some spelling errors
shadowspawn Jun 21, 2020
dc8d01d
Start adding tests. Fix groupHelp.
shadowspawn Jun 21, 2020
d118efb
Test help event order
shadowspawn Jun 21, 2020
6583da9
Add tests on context
shadowspawn Jun 21, 2020
9f39b76
Add missing semicolon
shadowspawn Jun 22, 2020
2fefb40
Add context.error tests
shadowspawn Jun 24, 2020
bc41fdc
Merge branch 'develop' into feature/on-help
shadowspawn Aug 2, 2020
e7126e7
First cut at README adding events and removing callbacks
shadowspawn Aug 2, 2020
772bf75
Add typings and parameter descriptions
shadowspawn Aug 2, 2020
38e32bb
Change from --help to postHelp event
shadowspawn Aug 2, 2020
cda067c
Update and add custom help examples
shadowspawn Aug 4, 2020
48a3067
Make help listener example more realistic
shadowspawn Aug 8, 2020
4efb432
Update example
shadowspawn Aug 8, 2020
b8f2f5b
Merge remote-tracking branch 'upstream/release/7.x' into feature/on-help
shadowspawn Aug 23, 2020
fa574df
Call the old callback, deprecated
shadowspawn Aug 24, 2020
88a59c4
First cut at .addHelp()
shadowspawn Aug 31, 2020
001c7e1
Change name to addHelpText
shadowspawn Sep 1, 2020
fea1ef5
First round of tests for addHelpText
shadowspawn Sep 1, 2020
e440a47
Simplify help event context, remove log
shadowspawn Sep 1, 2020
ed553e4
Assign write directly
shadowspawn Sep 1, 2020
f49d2e3
Add end-to-end and context checks for addHelpText
shadowspawn Sep 1, 2020
0606acb
Put back write wrapper to fix unit test
shadowspawn Sep 1, 2020
4bc95d0
Fix write mocks for help-as-error output
shadowspawn Sep 1, 2020
9ea0de1
Ignore falsy values for AddHelpText
shadowspawn Sep 1, 2020
47a0c10
Remove the help override from addHelpText as not good fit
shadowspawn Sep 2, 2020
034a687
Convert example to addHelpText
shadowspawn Sep 2, 2020
f020a12
Update README
shadowspawn Sep 2, 2020
d027d52
Add info about new .help param
shadowspawn Sep 2, 2020
06c3b62
Remove excess space
shadowspawn Sep 2, 2020
e9f4f6f
Update more examples
shadowspawn Sep 2, 2020
6b31ef6
Remove references to override
shadowspawn Sep 4, 2020
a78aff4
Update docs with .addHelpText
shadowspawn Sep 4, 2020
e255110
Merge branch 'develop' into feature/on-help
shadowspawn Sep 4, 2020
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
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