diff --git a/Readme.md b/Readme.md index 1c696aca5..e8807f9cd 100644 --- a/Readme.md +++ b/Readme.md @@ -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) @@ -497,7 +497,7 @@ 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) @@ -505,12 +505,10 @@ Example file: [custom-help](./examples/custom-help) 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: @@ -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 @@ -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) @@ -749,13 +759,11 @@ program .option("-e, --exec_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); ``` diff --git a/examples/custom-help b/examples/custom-help index e47a0c376..66ebdb2dc 100755 --- a/examples/custom-help +++ b/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 @@ -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 diff --git a/examples/custom-help-text.js b/examples/custom-help-text.js new file mode 100644 index 000000000..c83414eda --- /dev/null +++ b/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 diff --git a/examples/deploy b/examples/deploy index 5db5ce298..3e9c3909f 100755 --- a/examples/deploy +++ b/examples/deploy @@ -27,12 +27,10 @@ program .option('-e, --exec_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); diff --git a/examples/storeOptionsAsProperties-action.js b/examples/storeOptionsAsProperties-action.js index af3db8a69..8e9670ae2 100755 --- a/examples/storeOptionsAsProperties-action.js +++ b/examples/storeOptionsAsProperties-action.js @@ -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: diff --git a/index.js b/index.js index c1b67b7b2..b367dadaf 100644 --- a/index.js +++ b/index.js @@ -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)); @@ -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); @@ -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() } @@ -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)); }; /** @@ -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; + } }; /** diff --git a/tests/command.addHelp.test.js b/tests/command.addHelp.test.js new file mode 100644 index 000000000..1fc7ca42e --- /dev/null +++ b/tests/command.addHelp.test.js @@ -0,0 +1,231 @@ +const commander = require('../'); + +// Using outputHelp to simplify testing. + +describe('program calls to addHelpText', () => { + let writeSpy; + + beforeAll(() => { + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + }); + + afterEach(() => { + writeSpy.mockClear(); + }); + + afterAll(() => { + writeSpy.mockRestore(); + }); + + test('when "before" string then string before built-in help', () => { + const program = new commander.Command(); + program.addHelpText('before', 'text'); + program.outputHelp(); + expect(writeSpy).toHaveBeenNthCalledWith(1, 'text\n'); + expect(writeSpy).toHaveBeenNthCalledWith(2, program.helpInformation()); + }); + + test('when "before" function then function result before built-in help', () => { + const program = new commander.Command(); + program.addHelpText('before', () => 'text'); + program.outputHelp(); + expect(writeSpy).toHaveBeenNthCalledWith(1, 'text\n'); + expect(writeSpy).toHaveBeenNthCalledWith(2, program.helpInformation()); + }); + + test('when "beforeAll" string then string before built-in help', () => { + const program = new commander.Command(); + program.addHelpText('beforeAll', 'text'); + program.outputHelp(); + expect(writeSpy).toHaveBeenNthCalledWith(1, 'text\n'); + expect(writeSpy).toHaveBeenNthCalledWith(2, program.helpInformation()); + }); + + test('when "after" string then string after built-in help', () => { + const program = new commander.Command(); + program.addHelpText('after', 'text'); + program.outputHelp(); + expect(writeSpy).toHaveBeenNthCalledWith(1, program.helpInformation()); + expect(writeSpy).toHaveBeenNthCalledWith(2, 'text\n'); + }); + + test('when "afterAll" string then string after built-in help', () => { + const program = new commander.Command(); + program.addHelpText('afterAll', 'text'); + program.outputHelp(); + expect(writeSpy).toHaveBeenNthCalledWith(1, program.helpInformation()); + expect(writeSpy).toHaveBeenNthCalledWith(2, 'text\n'); + }); + + test('when all the simple positions then strings in order', () => { + const program = new commander.Command(); + program.addHelpText('before', 'before'); + program.addHelpText('after', 'after'); + program.addHelpText('beforeAll', 'beforeAll'); + program.addHelpText('afterAll', 'afterAll'); + program.outputHelp(); + expect(writeSpy).toHaveBeenNthCalledWith(1, 'beforeAll\n'); + expect(writeSpy).toHaveBeenNthCalledWith(2, 'before\n'); + expect(writeSpy).toHaveBeenNthCalledWith(3, program.helpInformation()); + expect(writeSpy).toHaveBeenNthCalledWith(4, 'after\n'); + expect(writeSpy).toHaveBeenNthCalledWith(5, 'afterAll\n'); + }); +}); + +describe('program and subcommand calls to addHelpText', () => { + let writeSpy; + + beforeAll(() => { + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + }); + + afterEach(() => { + writeSpy.mockClear(); + }); + + afterAll(() => { + writeSpy.mockRestore(); + }); + + test('when "before" on program then not called on subcommand', () => { + const program = new commander.Command(); + const sub = program.command('sub'); + const testMock = jest.fn(); + program.addHelpText('before', testMock); + sub.outputHelp(); + expect(testMock).not.toHaveBeenCalled(); + }); + + test('when "beforeAll" on program then is called on subcommand', () => { + const program = new commander.Command(); + const sub = program.command('sub'); + const testMock = jest.fn(); + program.addHelpText('beforeAll', testMock); + sub.outputHelp(); + expect(testMock).toHaveBeenCalled(); + }); + + test('when "after" on program then not called on subcommand', () => { + const program = new commander.Command(); + const sub = program.command('sub'); + const testMock = jest.fn(); + program.addHelpText('after', testMock); + sub.outputHelp(); + expect(testMock).not.toHaveBeenCalled(); + }); + + test('when "afterAll" on program then is called on subcommand', () => { + const program = new commander.Command(); + const sub = program.command('sub'); + const testMock = jest.fn(); + program.addHelpText('afterAll', testMock); + sub.outputHelp(); + expect(testMock).toHaveBeenCalled(); + }); +}); + +describe('context checks with full parse', () => { + let stdoutSpy; + let stderrSpy; + + beforeAll(() => { + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + }); + + afterEach(() => { + stdoutSpy.mockClear(); + stderrSpy.mockClear(); + }); + + afterAll(() => { + stdoutSpy.mockRestore(); + stderrSpy.mockRestore(); + }); + + test('when help requested then text is on stdout', () => { + const program = new commander.Command(); + program + .exitOverride() + .addHelpText('before', 'text'); + expect(() => { + program.parse(['--help'], { from: 'user' }); + }).toThrow(); + expect(stdoutSpy).toHaveBeenCalledWith('text\n'); + }); + + test('when help for error then text is on stderr', () => { + const program = new commander.Command(); + program + .exitOverride() + .addHelpText('before', 'text') + .command('sub'); + expect(() => { + program.parse([], { from: 'user' }); + }).toThrow(); + expect(stderrSpy).toHaveBeenCalledWith('text\n'); + }); + + test('when help requested then context.error is false', () => { + let context = {}; + const program = new commander.Command(); + program + .exitOverride() + .addHelpText('before', (contextParam) => { context = contextParam; }); + expect(() => { + program.parse(['--help'], { from: 'user' }); + }).toThrow(); + expect(context.error).toBe(false); + }); + + test('when help for error then context.error is true', () => { + let context = {}; + const program = new commander.Command(); + program + .exitOverride() + .addHelpText('before', (contextParam) => { context = contextParam; }) + .command('sub'); + expect(() => { + program.parse([], { from: 'user' }); + }).toThrow(); + expect(context.error).toBe(true); + }); + + test('when help on program then context.command is program', () => { + let context = {}; + const program = new commander.Command(); + program + .exitOverride() + .addHelpText('before', (contextParam) => { context = contextParam; }); + expect(() => { + program.parse(['--help'], { from: 'user' }); + }).toThrow(); + expect(context.command).toBe(program); + }); + + test('when help on subcommand and "before" subcommand then context.command is subcommand', () => { + let context = {}; + const program = new commander.Command(); + program + .exitOverride(); + const sub = program.command('sub') + .addHelpText('before', (contextParam) => { context = contextParam; }); + expect(() => { + program.parse(['sub', '--help'], { from: 'user' }); + }).toThrow(); + expect(context.command).toBe(sub); + }); + + test('when help on subcommand and "beforeAll" on program then context.command is subcommand', () => { + let context = {}; + const program = new commander.Command(); + program + .exitOverride() + .addHelpText('beforeAll', (contextParam) => { context = contextParam; }); + const sub = program.command('sub'); + expect(() => { + program.parse(['sub', '--help'], { from: 'user' }); + }).toThrow(); + expect(context.command).toBe(sub); + }); +}); diff --git a/tests/command.asterisk.test.js b/tests/command.asterisk.test.js index aff9495e0..14711b2d4 100644 --- a/tests/command.asterisk.test.js +++ b/tests/command.asterisk.test.js @@ -11,18 +11,8 @@ const commander = require('../'); // Historical: the event 'command:*' used to also be shared by the action handler on the program. describe(".command('*')", () => { - // Use internal knowledge to suppress output to keep test output clean. - let writeMock; - - beforeAll(() => { - writeMock = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); - }); - - afterAll(() => { - writeMock.mockRestore(); - }); - test('when no arguments then asterisk action not called', () => { + const writeMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const mockAction = jest.fn(); const program = new commander.Command(); program @@ -35,6 +25,7 @@ describe(".command('*')", () => { ; } expect(mockAction).not.toHaveBeenCalled(); + writeMock.mockRestore(); }); test('when unrecognised argument then asterisk action called', () => { diff --git a/tests/command.executableSubcommand.test.js b/tests/command.executableSubcommand.test.js index 8c6076cb0..538a35f34 100644 --- a/tests/command.executableSubcommand.test.js +++ b/tests/command.executableSubcommand.test.js @@ -3,9 +3,9 @@ const commander = require('../'); // Executable subcommand tests that didn't fit in elsewhere. // This is the default behaviour when no default command and no action handlers -test('when no command missing then display help', () => { +test('when no command specified and executable then display help', () => { // Optional. Suppress normal output to keep test output clean. - const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const program = new commander.Command(); program .exitOverride((err) => { throw err; }) diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 481b85e16..62bfdf6e4 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -18,21 +18,17 @@ function expectCommanderError(err, exitCode, code, message) { describe('.exitOverride and error details', () => { // Use internal knowledge to suppress output to keep test output clean. let consoleErrorSpy; - let writeSpy; // used for help [sic] and version beforeAll(() => { consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); - writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); }); afterEach(() => { consoleErrorSpy.mockClear(); - writeSpy.mockClear(); }); afterAll(() => { consoleErrorSpy.mockRestore(); - writeSpy.mockRestore(); }); test('when specify unknown program option then throw CommanderError', () => { @@ -125,6 +121,7 @@ describe('.exitOverride and error details', () => { }); test('when specify --help then throw CommanderError', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); const program = new commander.Command(); program .exitOverride(); @@ -136,11 +133,12 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(writeSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 0, 'commander.helpDisplayed', '(outputHelp)'); + writeSpy.mockRestore(); }); test('when executable subcommand and no command specified then throw CommanderError', () => { + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const program = new commander.Command(); program .exitOverride() @@ -153,11 +151,12 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(writeSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 1, 'commander.help', '(outputHelp)'); + writeSpy.mockRestore(); }); test('when specify --version then throw CommanderError', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); const myVersion = '1.2.3'; const program = new commander.Command(); program @@ -171,8 +170,8 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(writeSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 0, 'commander.version', myVersion); + writeSpy.mockRestore(); }); test('when executableSubcommand succeeds then call exitOverride', async() => { diff --git a/tests/command.help.test.js b/tests/command.help.test.js index c2ed34b3c..965886fbf 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -131,6 +131,30 @@ test('when both help flags masked then not displayed in helpInformation', () => expect(helpInformation).not.toMatch('display help'); }); +test('when call .help then output on stdout', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const program = new commander.Command(); + program + .exitOverride(); + expect(() => { + program.help(); + }).toThrow('(outputHelp)'); + expect(writeSpy).toHaveBeenCalledWith(program.helpInformation()); + writeSpy.mockClear(); +}); + +test('when call .help with { error: true } then output on stderr', () => { + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + const program = new commander.Command(); + program + .exitOverride(); + expect(() => { + program.help({ error: true }); + }).toThrow('(outputHelp)'); + expect(writeSpy).toHaveBeenCalledWith(program.helpInformation()); + writeSpy.mockClear(); +}); + test('when no options then Options not includes in helpInformation', () => { const program = new commander.Command(); // No custom options, no version option, no help option diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index d7914dd73..16abd3dfa 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -187,11 +187,13 @@ const nameValue: string = program.name(); // outputHelp program.outputHelp(); -program.outputHelp((str: string) => { return str; }); +program.outputHelp((str: string) => { return str; }); // deprecated +program.outputHelp({ error: true }); // help program.help(); -program.help((str: string) => { return str; }); +program.help((str: string) => { return str; }); // Deprecated +program.help({ error: true }); // helpInformation const helpInformnationValue: string = program.helpInformation(); @@ -202,8 +204,19 @@ const helpOptionThis2: commander.Command = program.helpOption('-h,--help', 'cust const helpOptionThis3: commander.Command = program.helpOption(undefined, 'custom description'); const helpOptionThis4: commander.Command = program.helpOption(false); +// addHelpText +const addHelpTextThis1: commander.Command = program.addHelpText('after', 'text'); +const addHelpTextThis2: commander.Command = program.addHelpText('afterAll', 'text'); +const addHelpTextThis3: commander.Command = program.addHelpText('before', () => 'before'); +const addHelpTextThis4: commander.Command = program.addHelpText('beforeAll', (context: commander.AddHelpTextContext) => { + if (context.error) { + return; // Can return nothing to skip display + } + return context.command.name(); +}); + // on -const onThis: commander.Command = program.on('--help', () => { +const onThis: commander.Command = program.on('command:foo', () => { // do nothing. }); diff --git a/typings/index.d.ts b/typings/index.d.ts index e404ae88d..700c51cdf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -26,6 +26,15 @@ declare namespace commander { interface ParseOptions { from: 'node' | 'electron' | 'user'; } + interface HelpContext { // optional parameter for .help() and .outputHelp() + error: boolean; + } + interface AddHelpTextContext { // passed to text function used with .addHelpText() + error: boolean; + command: Command; + } + + type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll'; interface Command { [key: string]: any; // options as properties @@ -335,10 +344,11 @@ declare namespace commander { /** * 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()`. + * */ - outputHelp(cb?: (str: string) => string): void; + outputHelp(context?: HelpContext): void; + outputHelp(cb?: (str: string) => string): void; // callback deprecated /** * Return command help documentation. @@ -354,17 +364,23 @@ declare namespace commander { /** * Output help information and exit. + * + * Outputs built-in help, and custom text added using `.addHelpText()`. */ - help(cb?: (str: string) => string): never; + help(context?: HelpContext): never; + help(cb?: (str: string) => string): never; // callback deprecated /** - * Add a listener (callback) for when events occur. (Implemented using EventEmitter.) + * Add additional text to be displayed with the built-in help. * - * @example - * program - * .on('--help', () -> { - * console.log('See web site for more information.'); - * }); + * Position is 'before' or 'after' to affect just this command, + * and 'beforeAll' or 'afterAll' to affect this command and all its subcommands. + */ + addHelpText(position: AddHelpTextPosition, text: string): this; + addHelpText(position: AddHelpTextPosition, text: (context: AddHelpTextContext) => string | void): this; + + /** + * Add a listener (callback) for when events occur. (Implemented using EventEmitter.) */ on(event: string | symbol, listener: (...args: any[]) => void): this; }