From 7df1dc7478468d63b07fc3a83bc25a0894d538f2 Mon Sep 17 00:00:00 2001 From: Isacc Davis Date: Tue, 13 Oct 2020 07:01:15 -0500 Subject: [PATCH] Add custom output streams to commands stdout isn't available to my user. I have a custom method of displaying text output to the user. I still want to make use of the full Commander help suite. Disabling the built-in help commands and writing wrappers becomes an increasingly complicated task when you want to make use of the entire help suite. This PR will solve that problem by removing the assumption that all output is to `process.stdout`. A user can instead pass a writeable stream that will replace the default stdout stream. In this way, anyone can output command results in any way that works for their use case by defining their own writeable stream. This is a more elegant solution to the problem in #1370. Instead of using `helpOption(false)` and writing my own help option, I can use Commander's help option, and handle the output with my own writeable stream. This is a less invasive solution than #779, as it leaves the implementation of the writeable stream to the user. Suggested changelog: "allow overriding the output from default (`process.stdout`) to any `stream.Writeable`" Things to note: * I've intentionally not updated the README or the examples/, as this is a WIP PR, and I'd like to know if this idea will be accepted before documenting it that far. * This adds a dependency on `stream.Writeable`, but that's built into Node, so I think that's fine * `process.stdout` is a `stream.Writeable`, but it's also a `tty.WriteStream`. Only the latter has the `.columns` field in its API. All of Commander's uses of that field are protected by a default if `.columns` is Falsey. So for that reason I've kept it simple with the understanding that if someone wants a non-default column width in their custom stream, they can specify that field themself. An alternative would be to create a type that's just a `stream.Writeable` + `.columns`. I think this alternative adds unneeded complexity, so I didn't go for it. However, it has the pro of not relying on the existing `.columns || 80` in the code to prevent undefined behavior. --- index.js | 26 +++++++++++++++++++++----- tests/command.outputStream.test.js | 14 ++++++++++++++ typings/index.d.ts | 13 +++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 tests/command.outputStream.test.js diff --git a/index.js b/index.js index c1b67b7b2..610bc9de2 100644 --- a/index.js +++ b/index.js @@ -128,6 +128,7 @@ class Command extends EventEmitter { this._exitCallback = null; this._aliases = []; this._combineFlagAndOptionalValue = true; + this._outputStream = process.stdout; this._hidden = false; this._hasHelpOption = true; @@ -198,6 +199,7 @@ class Command extends EventEmitter { cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue; + cmd._outputStream = this._outputStream; cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor this.commands.push(cmd); @@ -687,6 +689,20 @@ Read more on https://git.io/JJc0W`); return this; }; + /** + * The Stream to which this command writes + * normal output (help, usage, etc.). + * + * @param {tty.WriteStream} writeStream + * @returns {Command} `this` command for chaining + * @api public + */ + outputStream(writeStream) { + if (writeStream === undefined) return this._outputStream; + this._outputStream = writeStream; + return this; + }; + /** * Whether to pass command to action handler, * or just the options (specify false). @@ -1287,7 +1303,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._outputStream.write(str + '\n'); this._exit(0, 'commander.version', str); }); return this; @@ -1500,7 +1516,7 @@ Read more on https://git.io/JJc0W`); optionHelp() { const width = this.padWidth(); - const columns = process.stdout.columns || 80; + const columns = this._outputStream.columns || 80; const descriptionWidth = columns - width - 4; function padOptionDetails(flags, description) { return pad(flags, width) + ' ' + optionalWrap(description, descriptionWidth, width + 2); @@ -1542,7 +1558,7 @@ Read more on https://git.io/JJc0W`); const commands = this.prepareCommands(); const width = this.padWidth(); - const columns = process.stdout.columns || 80; + const columns = this._outputStream.columns || 80; const descriptionWidth = columns - width - 4; return [ @@ -1573,7 +1589,7 @@ Read more on https://git.io/JJc0W`); const argsDescription = this._argsDescription; if (argsDescription && this._args.length) { const width = this.padWidth(); - const columns = process.stdout.columns || 80; + const columns = this._outputStream.columns || 80; const descriptionWidth = columns - width - 5; desc.push('Arguments:'); desc.push(''); @@ -1636,7 +1652,7 @@ Read more on https://git.io/JJc0W`); if (typeof cbOutput !== 'string' && !Buffer.isBuffer(cbOutput)) { throw new Error('outputHelp callback must return a string or a Buffer'); } - process.stdout.write(cbOutput); + this._outputStream.write(cbOutput); this.emit(this._helpLongFlag); }; diff --git a/tests/command.outputStream.test.js b/tests/command.outputStream.test.js new file mode 100644 index 000000000..431ecb5b1 --- /dev/null +++ b/tests/command.outputStream.test.js @@ -0,0 +1,14 @@ +const commander = require('../'); +const stream = require('stream'); + +test('when command output stream is not set then default is process.stdout', () => { + const program = new commander.Command(); + expect(program.outputStream()).toBe(process.stdout); +}); + +test('when set command output stream then command output stream is set', () => { + const program = new commander.Command(); + const customStream = new stream.Writable(); + program.outputStream(customStream); + expect(program.outputStream()).toBe(customStream); +}); diff --git a/typings/index.d.ts b/typings/index.d.ts index e404ae88d..5ad9d9129 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,6 +1,8 @@ // Type definitions for commander // Original definitions by: Alan Agius , Marcelo Dezem , vvakame , Jules Randolph +import * as stream from 'stream'; + declare namespace commander { interface CommanderError extends Error { @@ -221,6 +223,17 @@ declare namespace commander { */ allowUnknownOption(arg?: boolean): this; + /** + * Set the Stream to which this command writes normal output (help, usage, etc.). + * + * @returns `this` command for chaining + */ + outputStream(writeStream: stream.Writable): this; + /** + * Get the Stream to which this command writes normal output (help, usage, etc.). + */ + outputStream(): stream.Writable; + /** * Parse `argv`, setting options and invoking commands when defined. *