Skip to content

Commit

Permalink
Add custom output streams to commands
Browse files Browse the repository at this point in the history
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 tj#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 tj#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.
  • Loading branch information
Melissa0x1f992 committed Oct 13, 2020
1 parent 9c7cfc0 commit 7df1dc7
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 5 deletions.
26 changes: 21 additions & 5 deletions index.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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

This comment has been minimized.

Copy link
@Melissa0x1f992

Melissa0x1f992 Oct 13, 2020

Author Owner

Should be stream.Writeable

* @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).
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 [
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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);
};

Expand Down
14 changes: 14 additions & 0 deletions 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);
});
13 changes: 13 additions & 0 deletions typings/index.d.ts
@@ -1,6 +1,8 @@
// Type definitions for commander
// Original definitions by: Alan Agius <https://github.com/alan-agius4>, Marcelo Dezem <https://github.com/mdezem>, vvakame <https://github.com/vvakame>, Jules Randolph <https://github.com/sveinburne>

import * as stream from 'stream';

declare namespace commander {

interface CommanderError extends Error {
Expand Down Expand Up @@ -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.
*
Expand Down

0 comments on commit 7df1dc7

Please sign in to comment.