From ebf5334f9b54f18530b1b7d50ec54ba4d875e266 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 27 Oct 2020 21:53:24 +1300 Subject: [PATCH 01/23] Add output configuration and use for version and errors --- index.js | 52 ++++++++++++++++++++++-------- tests/command.exitOverride.test.js | 4 +-- tests/options.version.test.js | 10 +++--- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 518faab67..8d26ff7a8 100644 --- a/index.js +++ b/index.js @@ -543,6 +543,13 @@ class Command extends EventEmitter { this._description = ''; this._argsDescription = undefined; + this._outputConfiguration = { + log: (...args) => console.log(...args), + error: (...args) => console.error(...args), + logColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, + errorColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined + }; + this._hidden = false; this._hasHelpOption = true; this._helpFlags = '-h, --help'; @@ -663,6 +670,23 @@ class Command extends EventEmitter { return this; } + /** + * WIP + * + * @param {Object} [configuration] - configuration options + * @return {Command|Object} `this` command for chaining, or stored configuration + */ + + configureOutput(configuration) { + // Do we need a getter? Could be useful for users building custom outputs, + // see if add other bottlenecks making less likely???? + if (configuration === undefined) return this._outputConfiguration; + + Object.assign(this._outputConfiguration, configuration); + // add type checking on properties and throw if incorrect???? + return this; + } + /** * Add a prepared subcommand. * @@ -968,8 +992,7 @@ Read more on https://git.io/JJc0W`); val = option.parseArg(val, oldValue === undefined ? defaultValue : oldValue); } catch (err) { if (err.code === 'commander.optionArgumentRejected') { - console.error(err.message); - this._exit(err.exitCode, err.code, err.message); + this._displayError(err.exitCode, err.code, err.message); } throw err; } @@ -1639,6 +1662,14 @@ Read more on https://git.io/JJc0W`); return this._optionValues; }; + /** + * WIP + */ + _displayError(exitCode, code, message) { + this._outputConfiguration.error(message); + this._exit(exitCode, code, message); + } + /** * Argument `name` is missing. * @@ -1648,8 +1679,7 @@ Read more on https://git.io/JJc0W`); missingArgument(name) { const message = `error: missing required argument '${name}'`; - console.error(message); - this._exit(1, 'commander.missingArgument', message); + this._displayError(1, 'commander.missingArgument', message); }; /** @@ -1667,8 +1697,7 @@ Read more on https://git.io/JJc0W`); } else { message = `error: option '${option.flags}' argument missing`; } - console.error(message); - this._exit(1, 'commander.optionMissingArgument', message); + this._displayError(1, 'commander.optionMissingArgument', message); }; /** @@ -1680,8 +1709,7 @@ Read more on https://git.io/JJc0W`); missingMandatoryOptionValue(option) { const message = `error: required option '${option.flags}' not specified`; - console.error(message); - this._exit(1, 'commander.missingMandatoryOptionValue', message); + this._displayError(1, 'commander.missingMandatoryOptionValue', message); }; /** @@ -1694,8 +1722,7 @@ Read more on https://git.io/JJc0W`); unknownOption(flag) { if (this._allowUnknownOption) return; const message = `error: unknown option '${flag}'`; - console.error(message); - this._exit(1, 'commander.unknownOption', message); + this._displayError(1, 'commander.unknownOption', message); }; /** @@ -1712,8 +1739,7 @@ Read more on https://git.io/JJc0W`); const fullCommand = partCommands.join(' '); const message = `error: unknown command '${this.args[0]}'.` + (this._hasHelpOption ? ` See '${fullCommand} ${this._helpLongFlag}'.` : ''); - console.error(message); - this._exit(1, 'commander.unknownCommand', message); + this._displayError(1, 'commander.unknownCommand', message); }; /** @@ -1739,7 +1765,7 @@ Read more on https://git.io/JJc0W`); this._versionOptionName = versionOption.attributeName(); this.options.push(versionOption); this.on('option:' + versionOption.name(), () => { - process.stdout.write(str + '\n'); + this._outputConfiguration.log(str); this._exit(0, 'commander.version', str); }); return this; diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 962384a71..5f82d4c37 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -156,7 +156,7 @@ describe('.exitOverride and error details', () => { }); test('when specify --version then throw CommanderError', () => { - const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { }); const myVersion = '1.2.3'; const program = new commander.Command(); program @@ -171,7 +171,7 @@ describe('.exitOverride and error details', () => { } expectCommanderError(caughtErr, 0, 'commander.version', myVersion); - writeSpy.mockRestore(); + logSpy.mockRestore(); }); test('when executableSubcommand succeeds then call exitOverride', async() => { diff --git a/tests/options.version.test.js b/tests/options.version.test.js index 863184e44..77d1daa89 100644 --- a/tests/options.version.test.js +++ b/tests/options.version.test.js @@ -4,18 +4,18 @@ const commander = require('../'); describe('.version', () => { // Optional. Suppress normal output to keep test output clean. - let writeSpy; + let logSpy; beforeAll(() => { - writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + logSpy = jest.spyOn(console, 'log').mockImplementation(() => { }); }); afterEach(() => { - writeSpy.mockClear(); + logSpy.mockClear(); }); afterAll(() => { - writeSpy.mockRestore(); + logSpy.mockRestore(); }); test('when no .version and specify --version then unknown option error', () => { @@ -50,7 +50,7 @@ describe('.version', () => { }).toThrow(myVersion); // Test output once as well, rest of tests just check the thrown message. - expect(writeSpy).toHaveBeenCalledWith(`${myVersion}\n`); + expect(logSpy).toHaveBeenCalledWith(myVersion); }); test('when specify default long flag then display version', () => { From 849bbbed259680175564f6c8ecc7b57f4eb478ef Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 12:08:43 +1300 Subject: [PATCH 02/23] Tests passing using write/writeErr --- index.js | 20 +++++++++++--------- tests/command.exitOverride.test.js | 22 ++++++++++------------ tests/options.version.test.js | 10 +++++----- xxx | 15 +++++++++++++++ 4 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 xxx diff --git a/index.js b/index.js index 8d26ff7a8..6cc4ef3e3 100644 --- a/index.js +++ b/index.js @@ -544,10 +544,13 @@ class Command extends EventEmitter { this._argsDescription = undefined; this._outputConfiguration = { - log: (...args) => console.log(...args), - error: (...args) => console.error(...args), - logColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, - errorColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined + // "write" and "error" are choosing backwards compatibility over consistency, for now! + // write is used for output to stdout, and error for output to stderr. + // Commander only passes simple strings and does not use implicit formatting in either case. + write: (arg) => process.stdout.write(arg), + writeError: (arg) => process.stderr.write(arg), + columns: () => process.stdout.isTTY ? process.stdout.columns : undefined, + columnsError: () => process.stderr.isTTY ? process.stderr.columns : undefined }; this._hidden = false; @@ -683,7 +686,6 @@ class Command extends EventEmitter { if (configuration === undefined) return this._outputConfiguration; Object.assign(this._outputConfiguration, configuration); - // add type checking on properties and throw if incorrect???? return this; } @@ -1666,7 +1668,7 @@ Read more on https://git.io/JJc0W`); * WIP */ _displayError(exitCode, code, message) { - this._outputConfiguration.error(message); + this._outputConfiguration.writeError(`${message}\n`); this._exit(exitCode, code, message); } @@ -1765,7 +1767,7 @@ Read more on https://git.io/JJc0W`); this._versionOptionName = versionOption.attributeName(); this.options.push(versionOption); this.on('option:' + versionOption.name(), () => { - this._outputConfiguration.log(str); + this._outputConfiguration.write(`${str}\n`); this._exit(0, 'commander.version', str); }); return this; @@ -1884,9 +1886,9 @@ Read more on https://git.io/JJc0W`); const context = { error: !!contextOptions.error }; let write; if (context.error) { - write = (arg, ...args) => process.stderr.write(arg, ...args); + write = (...arg) => this._outputConfiguration.writeError(...arg); } else { - write = (arg, ...args) => process.stdout.write(arg, ...args); + write = (...arg) => this._outputConfiguration.write(...arg); } context.write = contextOptions.write || write; context.command = this; diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 5f82d4c37..f542a5d81 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -17,18 +17,18 @@ 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 stderrSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + stderrSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + stderrSpy.mockRestore(); }); test('when specify unknown program option then throw CommanderError', () => { @@ -43,7 +43,7 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(stderrSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m'"); }); @@ -61,7 +61,7 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(stderrSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unknown command 'oops'. See 'prog --help'."); }); @@ -98,7 +98,7 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(stderrSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 1, 'commander.optionMissingArgument', `error: option '${optionFlags}' argument missing`); }); @@ -116,7 +116,7 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(stderrSpy).toHaveBeenCalled(); expectCommanderError(caughtErr, 1, 'commander.missingArgument', "error: missing required argument 'arg-name'"); }); @@ -138,7 +138,6 @@ describe('.exitOverride and error details', () => { }); 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() @@ -152,11 +151,10 @@ describe('.exitOverride and error details', () => { } expectCommanderError(caughtErr, 1, 'commander.help', '(outputHelp)'); - writeSpy.mockRestore(); }); test('when specify --version then throw CommanderError', () => { - const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { }); + const stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); const myVersion = '1.2.3'; const program = new commander.Command(); program @@ -171,7 +169,7 @@ describe('.exitOverride and error details', () => { } expectCommanderError(caughtErr, 0, 'commander.version', myVersion); - logSpy.mockRestore(); + stdoutSpy.mockRestore(); }); test('when executableSubcommand succeeds then call exitOverride', async() => { diff --git a/tests/options.version.test.js b/tests/options.version.test.js index 77d1daa89..863184e44 100644 --- a/tests/options.version.test.js +++ b/tests/options.version.test.js @@ -4,18 +4,18 @@ const commander = require('../'); describe('.version', () => { // Optional. Suppress normal output to keep test output clean. - let logSpy; + let writeSpy; beforeAll(() => { - logSpy = jest.spyOn(console, 'log').mockImplementation(() => { }); + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); }); afterEach(() => { - logSpy.mockClear(); + writeSpy.mockClear(); }); afterAll(() => { - logSpy.mockRestore(); + writeSpy.mockRestore(); }); test('when no .version and specify --version then unknown option error', () => { @@ -50,7 +50,7 @@ describe('.version', () => { }).toThrow(myVersion); // Test output once as well, rest of tests just check the thrown message. - expect(logSpy).toHaveBeenCalledWith(myVersion); + expect(writeSpy).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when specify default long flag then display version', () => { diff --git a/xxx b/xxx new file mode 100644 index 000000000..13f285804 --- /dev/null +++ b/xxx @@ -0,0 +1,15 @@ + ● program calls to addHelpText › when "before" function returns nothing then no effect + + expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) + + n: 1 + Expected: "Usage: [options]· + Options: + -h, --help display help for command + " + Received: "Usage: [options]· + Options: + -h, --help display help for command· + " + + From c295eda6282adde3d993976a855cde0ccc6360e7 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 12:21:21 +1300 Subject: [PATCH 03/23] Suppress test output on stderr using spyon --- tests/args.variadic.test.js | 15 --------------- tests/command.action.test.js | 4 +--- tests/command.allowUnknownOption.test.js | 8 ++++---- tests/command.helpCommand.test.js | 8 ++++---- tests/command.helpOption.test.js | 8 ++++---- tests/command.unknownCommand.test.js | 8 ++++---- tests/command.unknownOption.test.js | 8 ++++---- tests/options.mandatory.test.js | 16 ++++++++-------- tests/options.variadic.test.js | 4 ++-- 9 files changed, 31 insertions(+), 48 deletions(-) diff --git a/tests/args.variadic.test.js b/tests/args.variadic.test.js index 83c3dae82..750880175 100644 --- a/tests/args.variadic.test.js +++ b/tests/args.variadic.test.js @@ -3,21 +3,6 @@ const commander = require('../'); // Testing variadic arguments. Testing all the action arguments, but could test just variadicArg. describe('variadic argument', () => { - // Optional. Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; - - beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); - }); - - afterEach(() => { - consoleErrorSpy.mockClear(); - }); - - afterAll(() => { - consoleErrorSpy.mockRestore(); - }); - test('when no extra arguments specified for program then variadic arg is empty array', () => { const actionMock = jest.fn(); const program = new commander.Command(); diff --git a/tests/command.action.test.js b/tests/command.action.test.js index 447028933..d3b51f714 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -46,19 +46,17 @@ test('when .action on program with required argument and argument supplied then }); test('when .action on program with required argument and argument not supplied then action not called', () => { - // Optional. Use internal knowledge to suppress output to keep test output clean. - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); const actionMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ writeError: () => {} }) .arguments('') .action(actionMock); expect(() => { program.parse(['node', 'test']); }).toThrow(); expect(actionMock).not.toHaveBeenCalled(); - consoleErrorSpy.mockRestore(); }); // Changes made in #729 to call program action handler diff --git a/tests/command.allowUnknownOption.test.js b/tests/command.allowUnknownOption.test.js index cd0bca001..4425802dd 100644 --- a/tests/command.allowUnknownOption.test.js +++ b/tests/command.allowUnknownOption.test.js @@ -4,18 +4,18 @@ const commander = require('../'); describe('allowUnknownOption', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; + let writeErrorSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when specify unknown program option then error', () => { diff --git a/tests/command.helpCommand.test.js b/tests/command.helpCommand.test.js index f1f769845..a42048be8 100644 --- a/tests/command.helpCommand.test.js +++ b/tests/command.helpCommand.test.js @@ -39,21 +39,21 @@ describe('help command listed in helpInformation', () => { describe('help command processed on correct command', () => { // Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; + let writeErrorSpy; let writeSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); writeSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); writeSpy.mockRestore(); }); diff --git a/tests/command.helpOption.test.js b/tests/command.helpOption.test.js index 35cff54ee..5b5a25526 100644 --- a/tests/command.helpOption.test.js +++ b/tests/command.helpOption.test.js @@ -2,22 +2,22 @@ const commander = require('../'); describe('helpOption', () => { let writeSpy; - let consoleErrorSpy; + let writeErrorSpy; beforeAll(() => { // Optional. Suppress expected output to keep test output clean. writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { writeSpy.mockClear(); - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); }); afterAll(() => { writeSpy.mockRestore(); - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when helpOption has custom flags then custom short flag invokes help', () => { diff --git a/tests/command.unknownCommand.test.js b/tests/command.unknownCommand.test.js index 405625429..183d7afb0 100644 --- a/tests/command.unknownCommand.test.js +++ b/tests/command.unknownCommand.test.js @@ -2,18 +2,18 @@ const commander = require('../'); describe('unknownOption', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; + let writeErrorSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when unknown argument in simple program then no error', () => { diff --git a/tests/command.unknownOption.test.js b/tests/command.unknownOption.test.js index 18dbb6896..d4b62e59e 100644 --- a/tests/command.unknownOption.test.js +++ b/tests/command.unknownOption.test.js @@ -4,18 +4,18 @@ const commander = require('../'); describe('unknownOption', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; + let writeErrorSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when specify unknown option with subcommand and action handler then error', () => { diff --git a/tests/options.mandatory.test.js b/tests/options.mandatory.test.js index eaed78c71..4abb019f3 100644 --- a/tests/options.mandatory.test.js +++ b/tests/options.mandatory.test.js @@ -103,18 +103,18 @@ describe('required program option with mandatory value specified', () => { describe('required program option with mandatory value not specified', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; + let writeErrorSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when program has required option not specified then error', () => { @@ -185,18 +185,18 @@ describe('required command option with mandatory value specified', () => { describe('required command option with mandatory value not specified', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. - let consoleErrorSpy; + let writeErrorSpy; beforeAll(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); }); afterEach(() => { - consoleErrorSpy.mockClear(); + writeErrorSpy.mockClear(); }); afterAll(() => { - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when command has required value not specified then error', () => { diff --git a/tests/options.variadic.test.js b/tests/options.variadic.test.js index f06002ec1..c414cdb1f 100644 --- a/tests/options.variadic.test.js +++ b/tests/options.variadic.test.js @@ -3,7 +3,7 @@ const commander = require('../'); describe('variadic option with required value', () => { test('when variadic with value missing then error', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + const writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const program = new commander.Command(); program @@ -14,7 +14,7 @@ describe('variadic option with required value', () => { program.parse(['--required'], { from: 'user' }); }).toThrow(); - consoleErrorSpy.mockRestore(); + writeErrorSpy.mockRestore(); }); test('when variadic with one value then set in array', () => { From 0d82720666f90f477f6ef06e54ce3b10f3063967 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 13:13:24 +1300 Subject: [PATCH 04/23] Accurately set help columns for stdout/stderr --- index.js | 16 ++++++++++------ tests/help.columns.test.js | 22 ---------------------- typings/index.d.ts | 2 +- 3 files changed, 11 insertions(+), 29 deletions(-) delete mode 100644 tests/help.columns.test.js diff --git a/index.js b/index.js index 6cc4ef3e3..c18e0914b 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,7 @@ const fs = require('fs'); // Although this is a class, methods are static in style to allow override using subclass or just functions. class Help { constructor() { - this.columns = process.stdout.columns || 80; + this.columns = undefined; this.sortSubcommands = false; this.sortOptions = false; } @@ -243,7 +243,7 @@ class Help { formatHelp(cmd, helper) { const termWidth = helper.padWidth(cmd, helper); - const columns = helper.columns; + const columns = helper.columns || 80; const itemIndentWidth = 2; const itemSeparatorWidth = 2; // itemIndent term itemSeparator description @@ -549,8 +549,8 @@ class Command extends EventEmitter { // Commander only passes simple strings and does not use implicit formatting in either case. write: (arg) => process.stdout.write(arg), writeError: (arg) => process.stderr.write(arg), - columns: () => process.stdout.isTTY ? process.stdout.columns : undefined, - columnsError: () => process.stderr.isTTY ? process.stderr.columns : undefined + getColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, + getErrorColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined }; this._hidden = false; @@ -1869,11 +1869,15 @@ Read more on https://git.io/JJc0W`); /** * Return program help documentation. * + * @param {{ error: boolean }} [contextOptions] - pass {error:true} to wrap for stderr instead of stdout * @return {string} */ - helpInformation() { + helpInformation(contextOptions) { const helper = this.createHelp(); + if (helper.columns === undefined) { + helper.columns = (contextOptions && contextOptions.error) ? this._outputConfiguration.getErrorColumns() : this._outputConfiguration.getColumns(); + } return helper.formatHelp(this, helper); }; @@ -1921,7 +1925,7 @@ Read more on https://git.io/JJc0W`); groupListeners.slice().reverse().forEach(command => command.emit('beforeAllHelp', context)); this.emit('beforeHelp', context); - let helpInformation = this.helpInformation(); + let helpInformation = this.helpInformation(context); if (deprecatedCallback) { helpInformation = deprecatedCallback(helpInformation); if (typeof helpInformation !== 'string' && !Buffer.isBuffer(helpInformation)) { diff --git a/tests/help.columns.test.js b/tests/help.columns.test.js deleted file mode 100644 index cecaa4186..000000000 --- a/tests/help.columns.test.js +++ /dev/null @@ -1,22 +0,0 @@ -const commander = require('../'); - -// These are tests of the Help class, not of the Command help. -// There is some overlap with the higher level Command tests (which predate Help). - -describe('columns', () => { - test('when default then columns from stdout', () => { - const hold = process.stdout.columns; - process.stdout.columns = 123; - const program = new commander.Command(); - const helper = program.createHelp(); - expect(helper.columns).toEqual(123); - process.stdout.columns = hold; - }); - - test('when configure columns then value from user', () => { - const program = new commander.Command(); - program.configureHelp({ columns: 321 }); - const helper = program.createHelp(); - expect(helper.columns).toEqual(321); - }); -}); diff --git a/typings/index.d.ts b/typings/index.d.ts index 78a695da1..e7ef9f6e8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -77,7 +77,7 @@ declare namespace commander { interface Help { /** output columns, long lines are wrapped to fit */ - columns: number; + columns?: number; sortSubcommands: boolean; sortOptions: boolean; From ab1e0ceeea2ed28e994ac4d2c0ec9c16e131c20a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 13:21:03 +1300 Subject: [PATCH 05/23] Remove bogus file --- xxx | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 xxx diff --git a/xxx b/xxx deleted file mode 100644 index 13f285804..000000000 --- a/xxx +++ /dev/null @@ -1,15 +0,0 @@ - ● program calls to addHelpText › when "before" function returns nothing then no effect - - expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) - - n: 1 - Expected: "Usage: [options]· - Options: - -h, --help display help for command - " - Received: "Usage: [options]· - Options: - -h, --help display help for command· - " - - From 95c3153e14907ccd41c77942efab0524aea3fd64 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 13:25:07 +1300 Subject: [PATCH 06/23] Tidy comments --- index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/index.js b/index.js index c18e0914b..7a939ca0f 100644 --- a/index.js +++ b/index.js @@ -544,11 +544,9 @@ class Command extends EventEmitter { this._argsDescription = undefined; this._outputConfiguration = { - // "write" and "error" are choosing backwards compatibility over consistency, for now! - // write is used for output to stdout, and error for output to stderr. - // Commander only passes simple strings and does not use implicit formatting in either case. write: (arg) => process.stdout.write(arg), writeError: (arg) => process.stderr.write(arg), + // columns is used for wrapping the help getColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, getErrorColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined }; @@ -681,8 +679,6 @@ class Command extends EventEmitter { */ configureOutput(configuration) { - // Do we need a getter? Could be useful for users building custom outputs, - // see if add other bottlenecks making less likely???? if (configuration === undefined) return this._outputConfiguration; Object.assign(this._outputConfiguration, configuration); From 9f3efe2f8ae255299b985b235cc6070d49a816f6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 15:01:28 +1300 Subject: [PATCH 07/23] Only using single argument to write, simplify declaration to match --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 7a939ca0f..6e1c20f81 100644 --- a/index.js +++ b/index.js @@ -1886,9 +1886,9 @@ Read more on https://git.io/JJc0W`); const context = { error: !!contextOptions.error }; let write; if (context.error) { - write = (...arg) => this._outputConfiguration.writeError(...arg); + write = (arg) => this._outputConfiguration.writeError(arg); } else { - write = (...arg) => this._outputConfiguration.write(...arg); + write = (arg) => this._outputConfiguration.write(arg); } context.write = contextOptions.write || write; context.command = this; From 6a3eadfa440e0460e1cbdc5edace8aa9a50fb19a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 18:40:18 +1300 Subject: [PATCH 08/23] Add tests for configureOutput write and writeError --- tests/command.configureOutput.test.js | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/command.configureOutput.test.js diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js new file mode 100644 index 000000000..013d9acdf --- /dev/null +++ b/tests/command.configureOutput.test.js @@ -0,0 +1,108 @@ +const commander = require('../'); + +test('when default writeError() then error on stderr', () => { + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + const program = new commander.Command(); + program.exitOverride(); + + try { + program.parse(['--unknown'], { from: 'user' }); + } catch (err) { + } + + expect(writeSpy).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when custom writeError() then error on custom output', () => { + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + const customWrite = jest.fn(); + const program = new commander.Command(); + program + .exitOverride() + .configureOutput({ writeError: customWrite }); + + try { + program.parse(['--unknown'], { from: 'user' }); + } catch (err) { + } + + expect(writeSpy).toHaveBeenCalledTimes(0); + expect(customWrite).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when default write() then version on stdout', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const program = new commander.Command(); + program + .exitOverride() + .version('1.2.3'); + + expect(() => { + program.parse(['--version'], { from: 'user' }); + }).toThrow(); + + expect(writeSpy).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when custom write() then version on custom output', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const customWrite = jest.fn(); + const program = new commander.Command(); + program + .exitOverride() + .version('1.2.3') + .configureOutput({ write: customWrite }); + + expect(() => { + program.parse(['--version'], { from: 'user' }); + }).toThrow(); + + expect(writeSpy).toHaveBeenCalledTimes(0); + expect(customWrite).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when default write() then help on stdout', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const program = new commander.Command(); + program.outputHelp(); + + expect(writeSpy).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when custom write() then help error on stdout', () => { + const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + const customWrite = jest.fn(); + const program = new commander.Command(); + program.configureOutput({ write: customWrite }); + program.outputHelp(); + + expect(writeSpy).toHaveBeenCalledTimes(0); + expect(customWrite).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when default writeError then help error on stderr', () => { + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + const program = new commander.Command(); + program.outputHelp({ error: true }); + + expect(writeSpy).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); + +test('when custom writeError then help error on stderr', () => { + const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + const customWrite = jest.fn(); + const program = new commander.Command(); + program.configureOutput({ writeError: customWrite }); + program.outputHelp({ error: true }); + + expect(writeSpy).toHaveBeenCalledTimes(0); + expect(customWrite).toHaveBeenCalledTimes(1); + writeSpy.mockRestore(); +}); From b191033f2a16b680b8361a27b072de66818e09cd Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 19:42:27 +1300 Subject: [PATCH 09/23] Add tests for configureOutput getColumns and getErrorColumns --- tests/command.configureOutput.test.js | 109 +++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index 013d9acdf..95dfb53fd 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -74,7 +74,7 @@ test('when default write() then help on stdout', () => { writeSpy.mockRestore(); }); -test('when custom write() then help error on stdout', () => { +test('when custom write() then help error on custom output', () => { const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); const customWrite = jest.fn(); const program = new commander.Command(); @@ -95,7 +95,7 @@ test('when default writeError then help error on stderr', () => { writeSpy.mockRestore(); }); -test('when custom writeError then help error on stderr', () => { +test('when custom writeError then help error on custom output', () => { const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const customWrite = jest.fn(); const program = new commander.Command(); @@ -106,3 +106,108 @@ test('when custom writeError then help error on stderr', () => { expect(customWrite).toHaveBeenCalledTimes(1); writeSpy.mockRestore(); }); + +test('when default getColumns then help columns from stdout', () => { + const expectedColumns = 123; + const holdIsTTY = process.stdout.isTTY; + const holdColumns = process.stdout.columns; + let helpColumns; + + process.stderr.isTTY = true; + process.stdout.columns = expectedColumns; + process.stdout.isTTY = true; + const program = new commander.Command(); + program + .configureHelp({ + formatHelp: (cmd, helper) => { + helpColumns = helper.columns; + return ''; + } + }); + program.outputHelp(); + + expect(helpColumns).toBe(expectedColumns); + process.stdout.columns = holdColumns; + process.stdout.isTTY = holdIsTTY; +}); + +test('when custom getColumns then help columns custom', () => { + const expectedColumns = 123; + let helpColumns; + + const program = new commander.Command(); + program + .configureHelp({ + formatHelp: (cmd, helper) => { + helpColumns = helper.columns; + return ''; + } + }).configureOutput({ + getColumns: () => expectedColumns + }); + program.outputHelp(); + + expect(helpColumns).toBe(expectedColumns); +}); + +test('when default getErrorColumns then help error columns from stderr', () => { + const expectedColumns = 123; + const holdIsTTY = process.stderr.isTTY; + const holdColumns = process.stderr.columns; + let helpColumns; + + process.stderr.isTTY = true; + process.stderr.columns = expectedColumns; + const program = new commander.Command(); + program + .configureHelp({ + formatHelp: (cmd, helper) => { + helpColumns = helper.columns; + return ''; + } + }); + program.outputHelp({ error: true }); + + expect(helpColumns).toBe(expectedColumns); + process.stderr.isTTY = holdIsTTY; + process.stderr.columns = holdColumns; +}); + +test('when custom getErrorColumns then help error columns custom', () => { + const expectedColumns = 123; + let helpColumns; + + const program = new commander.Command(); + program + .configureHelp({ + formatHelp: (cmd, helper) => { + helpColumns = helper.columns; + return ''; + } + }).configureOutput({ + getErrorColumns: () => expectedColumns + }); + program.outputHelp({ error: true }); + + expect(helpColumns).toBe(expectedColumns); +}); + +test('when custom getColumns and configureHelp:columns then help columns from configureHelp', () => { + const expectedColumns = 123; + let helpColumns; + + const program = new commander.Command(); + program + .configureHelp({ + formatHelp: (cmd, helper) => { + helpColumns = helper.columns; + return ''; + }, + columns: expectedColumns + }).configureOutput({ + getColumns: () => 999 + }); + program.outputHelp(); + + expect(helpColumns).toBe(expectedColumns); +}); From 627f31045aa2f42da3f4354b626a203a16d7bfc5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 19:58:08 +1300 Subject: [PATCH 10/23] Add error case too --- tests/command.configureOutput.test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index 95dfb53fd..1502f15ca 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -211,3 +211,23 @@ test('when custom getColumns and configureHelp:columns then help columns from co expect(helpColumns).toBe(expectedColumns); }); + +test('when custom getErrorColumns and configureHelp:columns then help error columns from configureHelp', () => { + const expectedColumns = 123; + let helpColumns; + + const program = new commander.Command(); + program + .configureHelp({ + formatHelp: (cmd, helper) => { + helpColumns = helper.columns; + return ''; + }, + columns: expectedColumns + }).configureOutput({ + getErrorColumns: () => 999 + }); + program.outputHelp({ error: true }); + + expect(helpColumns).toBe(expectedColumns); +}); From 02f5b44f85dd0d703055d53103272c99fafe0b98 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 21:58:28 +1300 Subject: [PATCH 11/23] Use configureOutput instead of jest.spyon for some tests --- tests/options.variadic.test.js | 6 +----- tests/options.version.test.js | 39 ++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/tests/options.variadic.test.js b/tests/options.variadic.test.js index c414cdb1f..d531c6648 100644 --- a/tests/options.variadic.test.js +++ b/tests/options.variadic.test.js @@ -2,19 +2,15 @@ const commander = require('../'); describe('variadic option with required value', () => { test('when variadic with value missing then error', () => { - // Optional. Use internal knowledge to suppress output to keep test output clean. - const writeErrorSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); - const program = new commander.Command(); program .exitOverride() + .configureOutput({ writeError: jest.fn() }) .option('-r,--required '); expect(() => { program.parse(['--required'], { from: 'user' }); }).toThrow(); - - writeErrorSpy.mockRestore(); }); test('when variadic with one value then set in array', () => { diff --git a/tests/options.version.test.js b/tests/options.version.test.js index 863184e44..1036ff984 100644 --- a/tests/options.version.test.js +++ b/tests/options.version.test.js @@ -3,21 +3,6 @@ const commander = require('../'); // Test .version. Using exitOverride to check behaviour (instead of mocking process.exit). describe('.version', () => { - // Optional. Suppress normal output to keep test output clean. - let writeSpy; - - beforeAll(() => { - writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); - }); - - afterEach(() => { - writeSpy.mockClear(); - }); - - afterAll(() => { - writeSpy.mockRestore(); - }); - test('when no .version and specify --version then unknown option error', () => { const errorMessage = 'unknownOption'; const program = new commander.Command(); @@ -40,29 +25,32 @@ describe('.version', () => { test('when specify default short flag then display version', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion); expect(() => { program.parse(['node', 'test', '-V']); }).toThrow(myVersion); - - // Test output once as well, rest of tests just check the thrown message. - expect(writeSpy).toHaveBeenCalledWith(`${myVersion}\n`); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when specify default long flag then display version', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion); expect(() => { program.parse(['node', 'test', '--version']); }).toThrow(myVersion); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when default .version then helpInformation includes default version help', () => { @@ -79,50 +67,62 @@ describe('.version', () => { test('when specify custom short flag then display version', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion, '-r, --revision'); expect(() => { program.parse(['node', 'test', '-r']); }).toThrow(myVersion); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when specify just custom short flag then display version', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion, '-r'); expect(() => { program.parse(['node', 'test', '-r']); }).toThrow(myVersion); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when specify custom long flag then display version', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion, '-r, --revision'); expect(() => { program.parse(['node', 'test', '--revision']); }).toThrow(myVersion); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when specify just custom long flag then display version', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion, '--revision'); expect(() => { program.parse(['node', 'test', '--revision']); }).toThrow(myVersion); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when custom .version then helpInformation includes custom version help', () => { @@ -154,9 +154,11 @@ describe('.version', () => { test('when have .version+version and specify --version then version displayed', () => { const myVersion = '1.2.3'; + const writeMock = jest.fn(); const program = new commander.Command(); program .exitOverride() + .configureOutput({ write: writeMock }) .version(myVersion) .command('version') .action(() => {}); @@ -164,6 +166,7 @@ describe('.version', () => { expect(() => { program.parse(['node', 'test', '--version']); }).toThrow(myVersion); + expect(writeMock).toHaveBeenCalledWith(`${myVersion}\n`); }); test('when specify version then can get version', () => { From d89c91874730dbc3841457659fb826382912d9f5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 21:59:44 +1300 Subject: [PATCH 12/23] Add configureOutput to chain tests --- tests/command.chain.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 5979c3ab0..9918dae0c 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -135,4 +135,10 @@ describe('Command methods that should return this for chaining', () => { const result = program.configureHelp({ }); expect(result).toBe(program); }); + + test('when call .configureOutput() then returns this', () => { + const program = new Command(); + const result = program.configureOutput({ }); + expect(result).toBe(program); + }); }); From 3477e7e28675598479525a983b7a9edb322213e2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 31 Oct 2020 22:15:53 +1300 Subject: [PATCH 13/23] Add set/get test for configureOutput --- tests/command.configureOutput.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index 1502f15ca..7de9fa4c3 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -231,3 +231,15 @@ test('when custom getErrorColumns and configureHelp:columns then help error colu expect(helpColumns).toBe(expectedColumns); }); + +test('when set configureOutput then get configureOutput', () => { + const outputOptions = { + write: jest.fn(), + writeError: jest.fn(), + getColumns: jest.fn(), + getErrorColumns: jest.fn() + }; + const program = new commander.Command(); + program.configureOutput(outputOptions); + expect(program.configureOutput()).toEqual(outputOptions); +}); From a0b1db7b00e5cd04073dc353aed7e9199efb368d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 7 Nov 2020 16:36:52 +1300 Subject: [PATCH 14/23] Rename routines with symmetrical out/err --- index.js | 18 +++++------ tests/command.action.test.js | 2 +- tests/command.configureOutput.test.js | 44 +++++++++++++-------------- tests/options.variadic.test.js | 2 +- tests/options.version.test.js | 14 ++++----- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/index.js b/index.js index 6e1c20f81..94e256469 100644 --- a/index.js +++ b/index.js @@ -544,11 +544,11 @@ class Command extends EventEmitter { this._argsDescription = undefined; this._outputConfiguration = { - write: (arg) => process.stdout.write(arg), - writeError: (arg) => process.stderr.write(arg), + writeOut: (arg) => process.stdout.write(arg), + writeErr: (arg) => process.stderr.write(arg), // columns is used for wrapping the help - getColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, - getErrorColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined + getOutColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, + getErrColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined }; this._hidden = false; @@ -1664,7 +1664,7 @@ Read more on https://git.io/JJc0W`); * WIP */ _displayError(exitCode, code, message) { - this._outputConfiguration.writeError(`${message}\n`); + this._outputConfiguration.writeErr(`${message}\n`); this._exit(exitCode, code, message); } @@ -1763,7 +1763,7 @@ Read more on https://git.io/JJc0W`); this._versionOptionName = versionOption.attributeName(); this.options.push(versionOption); this.on('option:' + versionOption.name(), () => { - this._outputConfiguration.write(`${str}\n`); + this._outputConfiguration.writeOut(`${str}\n`); this._exit(0, 'commander.version', str); }); return this; @@ -1872,7 +1872,7 @@ Read more on https://git.io/JJc0W`); helpInformation(contextOptions) { const helper = this.createHelp(); if (helper.columns === undefined) { - helper.columns = (contextOptions && contextOptions.error) ? this._outputConfiguration.getErrorColumns() : this._outputConfiguration.getColumns(); + helper.columns = (contextOptions && contextOptions.error) ? this._outputConfiguration.getErrColumns() : this._outputConfiguration.getOutColumns(); } return helper.formatHelp(this, helper); }; @@ -1886,9 +1886,9 @@ Read more on https://git.io/JJc0W`); const context = { error: !!contextOptions.error }; let write; if (context.error) { - write = (arg) => this._outputConfiguration.writeError(arg); + write = (arg) => this._outputConfiguration.writeErr(arg); } else { - write = (arg) => this._outputConfiguration.write(arg); + write = (arg) => this._outputConfiguration.writeOut(arg); } context.write = contextOptions.write || write; context.command = this; diff --git a/tests/command.action.test.js b/tests/command.action.test.js index d3b51f714..cef18bee8 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -50,7 +50,7 @@ test('when .action on program with required argument and argument not supplied t const program = new commander.Command(); program .exitOverride() - .configureOutput({ writeError: () => {} }) + .configureOutput({ writeErr: () => {} }) .arguments('') .action(actionMock); expect(() => { diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index 7de9fa4c3..865b155f7 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -1,6 +1,6 @@ const commander = require('../'); -test('when default writeError() then error on stderr', () => { +test('when default writeErr() then error on stderr', () => { const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const program = new commander.Command(); program.exitOverride(); @@ -14,13 +14,13 @@ test('when default writeError() then error on stderr', () => { writeSpy.mockRestore(); }); -test('when custom writeError() then error on custom output', () => { +test('when custom writeErr() then error on custom output', () => { const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const customWrite = jest.fn(); const program = new commander.Command(); program .exitOverride() - .configureOutput({ writeError: customWrite }); + .configureOutput({ writeErr: customWrite }); try { program.parse(['--unknown'], { from: 'user' }); @@ -54,7 +54,7 @@ test('when custom write() then version on custom output', () => { program .exitOverride() .version('1.2.3') - .configureOutput({ write: customWrite }); + .configureOutput({ writeOut: customWrite }); expect(() => { program.parse(['--version'], { from: 'user' }); @@ -78,7 +78,7 @@ test('when custom write() then help error on custom output', () => { const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); const customWrite = jest.fn(); const program = new commander.Command(); - program.configureOutput({ write: customWrite }); + program.configureOutput({ writeOut: customWrite }); program.outputHelp(); expect(writeSpy).toHaveBeenCalledTimes(0); @@ -86,7 +86,7 @@ test('when custom write() then help error on custom output', () => { writeSpy.mockRestore(); }); -test('when default writeError then help error on stderr', () => { +test('when default writeErr then help error on stderr', () => { const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const program = new commander.Command(); program.outputHelp({ error: true }); @@ -95,11 +95,11 @@ test('when default writeError then help error on stderr', () => { writeSpy.mockRestore(); }); -test('when custom writeError then help error on custom output', () => { +test('when custom writeErr then help error on custom output', () => { const writeSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); const customWrite = jest.fn(); const program = new commander.Command(); - program.configureOutput({ writeError: customWrite }); + program.configureOutput({ writeErr: customWrite }); program.outputHelp({ error: true }); expect(writeSpy).toHaveBeenCalledTimes(0); @@ -107,7 +107,7 @@ test('when custom writeError then help error on custom output', () => { writeSpy.mockRestore(); }); -test('when default getColumns then help columns from stdout', () => { +test('when default getOutColumns then help columns from stdout', () => { const expectedColumns = 123; const holdIsTTY = process.stdout.isTTY; const holdColumns = process.stdout.columns; @@ -131,7 +131,7 @@ test('when default getColumns then help columns from stdout', () => { process.stdout.isTTY = holdIsTTY; }); -test('when custom getColumns then help columns custom', () => { +test('when custom getOutColumns then help columns custom', () => { const expectedColumns = 123; let helpColumns; @@ -143,14 +143,14 @@ test('when custom getColumns then help columns custom', () => { return ''; } }).configureOutput({ - getColumns: () => expectedColumns + getOutColumns: () => expectedColumns }); program.outputHelp(); expect(helpColumns).toBe(expectedColumns); }); -test('when default getErrorColumns then help error columns from stderr', () => { +test('when default getErrColumns then help error columns from stderr', () => { const expectedColumns = 123; const holdIsTTY = process.stderr.isTTY; const holdColumns = process.stderr.columns; @@ -173,7 +173,7 @@ test('when default getErrorColumns then help error columns from stderr', () => { process.stderr.columns = holdColumns; }); -test('when custom getErrorColumns then help error columns custom', () => { +test('when custom getErrColumns then help error columns custom', () => { const expectedColumns = 123; let helpColumns; @@ -185,14 +185,14 @@ test('when custom getErrorColumns then help error columns custom', () => { return ''; } }).configureOutput({ - getErrorColumns: () => expectedColumns + getErrColumns: () => expectedColumns }); program.outputHelp({ error: true }); expect(helpColumns).toBe(expectedColumns); }); -test('when custom getColumns and configureHelp:columns then help columns from configureHelp', () => { +test('when custom getOutColumns and configureHelp:columns then help columns from configureHelp', () => { const expectedColumns = 123; let helpColumns; @@ -205,14 +205,14 @@ test('when custom getColumns and configureHelp:columns then help columns from co }, columns: expectedColumns }).configureOutput({ - getColumns: () => 999 + getOutColumns: () => 999 }); program.outputHelp(); expect(helpColumns).toBe(expectedColumns); }); -test('when custom getErrorColumns and configureHelp:columns then help error columns from configureHelp', () => { +test('when custom getErrColumns and configureHelp:columns then help error columns from configureHelp', () => { const expectedColumns = 123; let helpColumns; @@ -225,7 +225,7 @@ test('when custom getErrorColumns and configureHelp:columns then help error colu }, columns: expectedColumns }).configureOutput({ - getErrorColumns: () => 999 + getErrColumns: () => 999 }); program.outputHelp({ error: true }); @@ -234,10 +234,10 @@ test('when custom getErrorColumns and configureHelp:columns then help error colu test('when set configureOutput then get configureOutput', () => { const outputOptions = { - write: jest.fn(), - writeError: jest.fn(), - getColumns: jest.fn(), - getErrorColumns: jest.fn() + writeOut: jest.fn(), + writeErr: jest.fn(), + getOutColumns: jest.fn(), + getErrColumns: jest.fn() }; const program = new commander.Command(); program.configureOutput(outputOptions); diff --git a/tests/options.variadic.test.js b/tests/options.variadic.test.js index d531c6648..1c32e21cc 100644 --- a/tests/options.variadic.test.js +++ b/tests/options.variadic.test.js @@ -5,7 +5,7 @@ describe('variadic option with required value', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ writeError: jest.fn() }) + .configureOutput({ writeErr: jest.fn() }) .option('-r,--required '); expect(() => { diff --git a/tests/options.version.test.js b/tests/options.version.test.js index 1036ff984..5efa5e3c4 100644 --- a/tests/options.version.test.js +++ b/tests/options.version.test.js @@ -29,7 +29,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion); expect(() => { @@ -44,7 +44,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion); expect(() => { @@ -71,7 +71,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion, '-r, --revision'); expect(() => { @@ -86,7 +86,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion, '-r'); expect(() => { @@ -101,7 +101,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion, '-r, --revision'); expect(() => { @@ -116,7 +116,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion, '--revision'); expect(() => { @@ -158,7 +158,7 @@ describe('.version', () => { const program = new commander.Command(); program .exitOverride() - .configureOutput({ write: writeMock }) + .configureOutput({ writeOut: writeMock }) .version(myVersion) .command('version') .action(() => {}); From 015895e9ebec51297fdebe620a701ffa01fb09b9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 7 Nov 2020 17:09:52 +1300 Subject: [PATCH 15/23] Add outputError simple code --- index.js | 11 +++++++---- tests/command.configureOutput.test.js | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 94e256469..4a229d18f 100644 --- a/index.js +++ b/index.js @@ -544,11 +544,14 @@ class Command extends EventEmitter { this._argsDescription = undefined; this._outputConfiguration = { - writeOut: (arg) => process.stdout.write(arg), - writeErr: (arg) => process.stderr.write(arg), + // Routines for where output is going, stdout or stderr + writeOut: (str) => process.stdout.write(str), + writeErr: (str) => process.stderr.write(str), // columns is used for wrapping the help getOutColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, - getErrColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined + getErrColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined, + // routines for what is being written out + outputError: (str, write) => write(str) // used for displaying errors, and not used for displaying help }; this._hidden = false; @@ -1664,7 +1667,7 @@ Read more on https://git.io/JJc0W`); * WIP */ _displayError(exitCode, code, message) { - this._outputConfiguration.writeErr(`${message}\n`); + this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr); this._exit(exitCode, code, message); } diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index 865b155f7..e6efadcf5 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -237,7 +237,8 @@ test('when set configureOutput then get configureOutput', () => { writeOut: jest.fn(), writeErr: jest.fn(), getOutColumns: jest.fn(), - getErrColumns: jest.fn() + getErrColumns: jest.fn(), + outputError: jest.fn() }; const program = new commander.Command(); program.configureOutput(outputOptions); From 2727d13f27d6e73607f212fe6094be2098726718 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 10 Nov 2020 22:23:17 +1300 Subject: [PATCH 16/23] Add tests for outputError --- tests/command.configureOutput.test.js | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index e6efadcf5..3e9a7b80a 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -244,3 +244,32 @@ test('when set configureOutput then get configureOutput', () => { program.configureOutput(outputOptions); expect(program.configureOutput()).toEqual(outputOptions); }); + +test('when custom outputErr and error then outputErr called', () => { + const outputError = jest.fn(); + const program = new commander.Command(); + program + .exitOverride() + .configureOutput({ + outputError + }); + + expect(() => { + program.parse(['--unknownOption'], { from: 'user' }); + }).toThrow(); + expect(outputError).toHaveBeenCalledWith("error: unknown option '--unknownOption'\n", program._outputConfiguration.writeErr); +}); + +test('when custom outputErr and writeErr and error then outputErr passed writeErr', () => { + const writeErr = () => jest.fn(); + const outputError = jest.fn(); + const program = new commander.Command(); + program + .exitOverride() + .configureOutput({ writeErr, outputError }); + + expect(() => { + program.parse(['--unknownOption'], { from: 'user' }); + }).toThrow(); + expect(outputError).toHaveBeenCalledWith("error: unknown option '--unknownOption'\n", writeErr); +}); From bab81d04f22524bc882191ff67e98e8d033521d0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 10 Nov 2020 22:39:48 +1300 Subject: [PATCH 17/23] Add JSDoc --- index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 4a229d18f..f773fc3b7 100644 --- a/index.js +++ b/index.js @@ -675,7 +675,18 @@ class Command extends EventEmitter { } /** - * WIP + * The default output goes to stdout and stderr. You can customise this for special + * applications. You can also customise the display of errors by overriding outputError. + * + * The function signatures are: + * + * writeOut(str) + * writeErr(str) + * // columns is used for wrapping the help + * getOutColumns() + * getErrColumns() + * // routines for what is being written out + * outputError(str, write) // used for displaying errors, and not used for displaying help * * @param {Object} [configuration] - configuration options * @return {Command|Object} `this` command for chaining, or stored configuration @@ -1664,7 +1675,9 @@ Read more on https://git.io/JJc0W`); }; /** - * WIP + * Internal bottleneck for handling of parsing errors. + * + * @api private */ _displayError(exitCode, code, message) { this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr); From 8a680607c7e7068df904fd4d503ffb7d5cac2c98 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 10 Nov 2020 23:09:12 +1300 Subject: [PATCH 18/23] Tweak wording --- index.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index f773fc3b7..5817acaac 100644 --- a/index.js +++ b/index.js @@ -543,15 +543,13 @@ class Command extends EventEmitter { this._description = ''; this._argsDescription = undefined; + // see .configureOutput() for docs this._outputConfiguration = { - // Routines for where output is going, stdout or stderr writeOut: (str) => process.stdout.write(str), writeErr: (str) => process.stderr.write(str), - // columns is used for wrapping the help getOutColumns: () => process.stdout.isTTY ? process.stdout.columns : undefined, getErrColumns: () => process.stderr.isTTY ? process.stderr.columns : undefined, - // routines for what is being written out - outputError: (str, write) => write(str) // used for displaying errors, and not used for displaying help + outputError: (str, write) => write(str) }; this._hidden = false; @@ -678,14 +676,15 @@ class Command extends EventEmitter { * The default output goes to stdout and stderr. You can customise this for special * applications. You can also customise the display of errors by overriding outputError. * - * The function signatures are: + * The configuration properties are all functions: * + * // functions to change where being written, stdout and stderr * writeOut(str) * writeErr(str) - * // columns is used for wrapping the help + * // matching functions to specify columns for wrapping help * getOutColumns() * getErrColumns() - * // routines for what is being written out + * // functions based on what is being written out * outputError(str, write) // used for displaying errors, and not used for displaying help * * @param {Object} [configuration] - configuration options From 2a8d1cba58907abbbc5b80bccf015da2ff8be6ba Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 10 Nov 2020 23:21:10 +1300 Subject: [PATCH 19/23] First cut at TypeScript --- typings/index.d.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index e7ef9f6e8..e921ffbb1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -134,6 +134,14 @@ declare namespace commander { error: boolean; command: Command; } + interface OutputConfiguration { + writeOut?(str: string): void; + writeErr?(str: string): void; + getOutColumns?(): number; + getErrColumns?(): number; + outputError?(str: string, write: (str: string) => void): void; + + } type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll'; @@ -249,6 +257,25 @@ declare namespace commander { /** Get configuration */ configureHelp(): HelpConfiguration; + /** + * The default output goes to stdout and stderr. You can customise this for special + * applications. You can also customise the display of errors by overriding outputError. + * + * The configuration properties are all functions: + * + * // functions to change where being written, stdout and stderr + * writeOut(str) + * writeErr(str) + * // matching functions to specify columns for wrapping help + * getOutColumns() + * getErrColumns() + * // functions based on what is being written out + * outputError(str, write) // used for displaying errors, and not used for displaying help + */ + configureOutput(configuration: OutputConfiguration): this; + /** Get configuration */ + configureOutput(): OutputConfiguration; + /** * Register callback `fn` for the command. * From 1100c473789a2f5b0f707894cad17b9752a80ba1 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 10 Nov 2020 23:32:31 +1300 Subject: [PATCH 20/23] Add TypeScript sanity check for configureOutput --- typings/commander-tests.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 6854c3e40..d90474179 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -263,6 +263,18 @@ const configureHelpThis: commander.Command = program.configureHelp({ }); const helpConfiguration: commander.HelpConfiguration = program.configureHelp(); +// configureOutput +const configureOutputThis: commander.Command = program.configureOutput({ }); +const configureOutputConfig: commander.OutputConfiguration = program.configureOutput(); + +program.configureOutput({ + writeOut: (str: string) => { }, + writeErr: (str: string) => { }, + getOutColumns: () => 80, + getErrColumns: () => 80, + outputError: (str: string, write: (str: string) => void) => { } +}); + // Help const helper = new commander.Help(); const helperCommand = new commander.Command(); From 7c882f67910ddf052315bd1435237699e96c843a Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 11 Nov 2020 20:52:41 +1300 Subject: [PATCH 21/23] Add example for configureOutput --- examples/configure-output.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 examples/configure-output.js diff --git a/examples/configure-output.js b/examples/configure-output.js new file mode 100644 index 000000000..207a38f06 --- /dev/null +++ b/examples/configure-output.js @@ -0,0 +1,30 @@ +// const commander = require('commander'); // (normal include) +const commander = require('../'); // include commander in git clone of commander repo + +const program = new commander.Command(); + +function red(str) { + return `\x1b[31m${str}\x1b[0m`; +} + +program + .configureOutput({ + // Visibly override write routines for example! + writeOut: (str) => process.stdout.write(`[OUT] ${str}`), + writeErr: (str) => process.stdout.write(`[ERR] ${str}`), + // Output errors in red. + outputError: (str, write) => write(red(str)) + }); + +program + .version('1.2.3') + .option('-c, --compress') + .command('sub-command'); + +program.parse(); + +// Try the following: +// node configure-output.js --version +// node configure-output.js --unknown +// node configure-output.js --help +// node configure-output.js From cc4230979207eac0f676e45c21b628cdcf5f2648 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 14 Nov 2020 18:04:24 +1300 Subject: [PATCH 22/23] Add configureOutput to README --- Readme.md | 21 +++++++++++++++++++-- examples/configure-output.js | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index c72403c01..c852480ad 100644 --- a/Readme.md +++ b/Readme.md @@ -41,7 +41,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Import into ECMAScript Module](#import-into-ecmascript-module) - [Node options such as `--harmony`](#node-options-such-as---harmony) - [Debugging stand-alone executable subcommands](#debugging-stand-alone-executable-subcommands) - - [Override exit handling](#override-exit-handling) + - [Override exit and output handling](#override-exit-and-output-handling) - [Additional documentation](#additional-documentation) - [Examples](#examples) - [Support](#support) @@ -772,7 +772,7 @@ the inspector port is incremented by 1 for the spawned subcommand. If you are using VSCode to debug executable subcommands you need to set the `"autoAttachChildProcesses": true` flag in your launch.json configuration. -### Override exit handling +### Override exit and output handling By default Commander calls `process.exit` when it detects errors, or after displaying the help or version. You can override this behaviour and optionally supply a callback. The default override throws a `CommanderError`. @@ -790,6 +790,23 @@ try { } ``` +By default Commander is configured for a command-line application and writes to stdout and stderr. +You can modify this behaviour for custom applications. In addition, you can modify the display of error messages. + +Example file: [configure-output.js](./examples/configure-output.js) + + +```js +program + .configureOutput({ + // Visibly override write routines as example! + writeOut: (str) => process.stdout.write(`[OUT] ${str}`), + writeErr: (str) => process.stdout.write(`[ERR] ${str}`), + // Output errors in red. + outputError: (str, write) => write(red(str)) + }); +``` + ### Additional documentation There is more information available about: diff --git a/examples/configure-output.js b/examples/configure-output.js index 207a38f06..f3145a4d2 100644 --- a/examples/configure-output.js +++ b/examples/configure-output.js @@ -9,7 +9,7 @@ function red(str) { program .configureOutput({ - // Visibly override write routines for example! + // Visibly override write routines as example! writeOut: (str) => process.stdout.write(`[OUT] ${str}`), writeErr: (str) => process.stdout.write(`[ERR] ${str}`), // Output errors in red. From 0ae16fba632bd1fc3d47e35e1a0fd6fd0771c067 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 16 Nov 2020 21:01:03 +1300 Subject: [PATCH 23/23] Make example in README a little clearer --- Readme.md | 9 +++++++-- examples/configure-output.js | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Readme.md b/Readme.md index c852480ad..4c5f5ea8f 100644 --- a/Readme.md +++ b/Readme.md @@ -797,13 +797,18 @@ Example file: [configure-output.js](./examples/configure-output.js) ```js +function errorColor(str) { + // Add ANSI escape codes to display text in red. + return `\x1b[31m${str}\x1b[0m`; +} + program .configureOutput({ // Visibly override write routines as example! writeOut: (str) => process.stdout.write(`[OUT] ${str}`), writeErr: (str) => process.stdout.write(`[ERR] ${str}`), - // Output errors in red. - outputError: (str, write) => write(red(str)) + // Highlight errors in color. + outputError: (str, write) => write(errorColor(str)) }); ``` diff --git a/examples/configure-output.js b/examples/configure-output.js index f3145a4d2..490a322aa 100644 --- a/examples/configure-output.js +++ b/examples/configure-output.js @@ -3,7 +3,8 @@ const commander = require('../'); // include commander in git clone of commander const program = new commander.Command(); -function red(str) { +function errorColor(str) { + // Add ANSI escape codes to display text in red. return `\x1b[31m${str}\x1b[0m`; } @@ -13,7 +14,7 @@ program writeOut: (str) => process.stdout.write(`[OUT] ${str}`), writeErr: (str) => process.stdout.write(`[ERR] ${str}`), // Output errors in red. - outputError: (str, write) => write(red(str)) + outputError: (str, write) => write(errorColor(str)) }); program