From 7f8d911153c475d75da0925adf2db40cbe13a98a Mon Sep 17 00:00:00 2001 From: Tommaso Sotte Date: Sun, 10 Mar 2019 18:04:11 +0100 Subject: [PATCH] Add `.all` property with interleaved stdout and stderr (#171) Fixes #1 Co-authored-by: Sindre Sorhus --- fixtures/noop-132 | 9 +++++++++ index.js | 34 +++++++++++++++++++++++++++++++++- package.json | 1 + readme.md | 10 +++++++++- test.js | 14 +++++++++++--- 5 files changed, 63 insertions(+), 5 deletions(-) create mode 100755 fixtures/noop-132 diff --git a/fixtures/noop-132 b/fixtures/noop-132 new file mode 100755 index 000000000..277e817db --- /dev/null +++ b/fixtures/noop-132 @@ -0,0 +1,9 @@ +#!/usr/bin/env node +'use strict'; + +process.stdout.write('1'); +process.stderr.write('3'); + +setTimeout(() => { + process.stdout.write('2'); +}, 1000); diff --git a/index.js b/index.js index fa42babf9..297a24d46 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const stripFinalNewline = require('strip-final-newline'); const npmRunPath = require('npm-run-path'); const isStream = require('is-stream'); const _getStream = require('get-stream'); +const mergeStream = require('merge-stream'); const pFinally = require('p-finally'); const onExit = require('signal-exit'); const errname = require('./lib/errname'); @@ -91,6 +92,24 @@ function handleShell(fn, command, options) { return fn(command, {...options, shell: true}); } +function makeAllStream(spawned) { + if (!spawned.stdout && !spawned.stderr) { + return null; + } + + const mixed = mergeStream(); + + if (spawned.stdout) { + mixed.add(spawned.stdout); + } + + if (spawned.stderr) { + mixed.add(spawned.stderr); + } + + return mixed; +} + function getStream(process, stream, {encoding, buffer, maxBuffer}) { if (!process[stream]) { return null; @@ -146,6 +165,10 @@ function makeError(result, options) { error.cmd = joinedCommand; error.timedOut = Boolean(timedOut); + if ('all' in result) { + error.all = result.all; + } + return error; } @@ -263,17 +286,23 @@ module.exports = (command, args, options) => { if (spawned.stderr) { spawned.stderr.destroy(); } + + if (spawned.all) { + spawned.all.destroy(); + } } // TODO: Use native "finally" syntax when targeting Node.js 10 const handlePromise = () => pFinally(Promise.all([ processDone, getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}), - getStream(spawned, 'stderr', {encoding, buffer, maxBuffer}) + getStream(spawned, 'stderr', {encoding, buffer, maxBuffer}), + getStream(spawned, 'all', {encoding, buffer, maxBuffer: maxBuffer * 2}) ]).then(results => { // eslint-disable-line promise/prefer-await-to-then const result = results[0]; result.stdout = results[1]; result.stderr = results[2]; + result.all = results[3]; if (result.error || result.code !== 0 || result.signal !== null) { const error = makeError(result, { @@ -297,6 +326,7 @@ module.exports = (command, args, options) => { return { stdout: handleOutput(parsed.options, result.stdout), stderr: handleOutput(parsed.options, result.stderr), + all: handleOutput(parsed.options, result.all), code: 0, exitCode: 0, exitCodeName: 'SUCCESS', @@ -312,6 +342,8 @@ module.exports = (command, args, options) => { handleInput(spawned, parsed.options.input); + spawned.all = makeAllStream(spawned); + // eslint-disable-next-line promise/prefer-await-to-then spawned.then = (onFulfilled, onRejected) => handlePromise().then(onFulfilled, onRejected); spawned.catch = onRejected => handlePromise().catch(onRejected); diff --git a/package.json b/package.json index d6a84e558..d4355ee0d 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", + "merge-stream": "1.0.1", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", diff --git a/readme.md b/readme.md index 6542e973c..c6d9d83ca 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,7 @@ - Higher max buffer. 10 MB instead of 200 KB. - [Executes locally installed binaries by name.](#preferlocal) - [Cleans up spawned processes when the parent process dies.](#cleanup) +- [Adds an `.all` property](#execafile-arguments-options) with interleaved output from `stdout` and `stderr`, similar to what the terminal sees. [*(Async only)*](#execasyncfile-arguments-options) ## Install @@ -65,6 +66,7 @@ const execa = require('execa'); exitCodeName: 'ESRCH', stdout: '', stderr: '', + all: '', failed: true, signal: null, cmd: 'exit 3', @@ -106,7 +108,11 @@ Execute a file. Think of this as a mix of `child_process.execFile` and `child_process.spawn`. -Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout` and `stderr` properties. +Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which is enhanced to be a `Promise`. + +It exposes an additional `.all` stream, with `stdout` and `stderr` interleaved. + +The promise result is an `Object` with `stdout`, `stderr` and `all` properties. ### execa.stdout(file, [arguments], [options]) @@ -130,6 +136,8 @@ Execute a file synchronously. Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). +It does not have the `.all` property that `execa()` has because the [underlying synchronous implementation](https://nodejs.org/api/child_process.html#child_process_child_process_execfilesync_file_args_options) only returns `stdout` and `stderr` at the end of the execution, so they cannot be interleaved. + This method throws an `Error` if the command fails. ### execa.shellSync(file, [options]) diff --git a/test.js b/test.js index 3e72c60d5..9c043a472 100644 --- a/test.js +++ b/test.js @@ -46,10 +46,16 @@ test('execa.stderr()', async t => { t.is(stderr, 'foo'); }); -test('stdout/stderr available on errors', async t => { +test.serial('result.all shows both `stdout` and `stderr` intermixed', async t => { + const result = await execa('noop-132'); + t.is(result.all, '132'); +}); + +test('stdout/stderr/all available on errors', async t => { const err = await t.throwsAsync(execa('exit', ['2']), {message: getExitRegExp('2')}); t.is(typeof err.stdout, 'string'); t.is(typeof err.stderr, 'string'); + t.is(typeof err.all, 'string'); }); test('include stdout and stderr in errors for improved debugging', async t => { @@ -234,7 +240,8 @@ test('do not buffer stdout when `buffer` set to `false`', async t => { const promise = execa('max-buffer', ['stdout', '10'], {buffer: false}); const [result, stdout] = await Promise.all([ promise, - getStream(promise.stdout) + getStream(promise.stdout), + getStream(promise.all) ]); t.is(result.stdout, undefined); @@ -245,7 +252,8 @@ test('do not buffer stderr when `buffer` set to `false`', async t => { const promise = execa('max-buffer', ['stderr', '10'], {buffer: false}); const [result, stderr] = await Promise.all([ promise, - getStream(promise.stderr) + getStream(promise.stderr), + getStream(promise.all) ]); t.is(result.stderr, undefined);