Repository: sindresorhus/execa (js)

Issue:
Piping shortcut notation
## Problem

Redirecting a process's output to two different targets is a common pattern. For example, a user might want to `inherit` stdout/stderr for debugging reasons, while still using `pipe` to get a process's return value.

This is currently possible, but requires 3 statements, which trips up users who are less familiar with process execution. This required us to explicitly add those in our [Tips documentation](https://github.com/sindresorhus/execa#save-and-pipe-output-from-a-child-process).

```js
const subprocess = execa('echo', ['foo']);
subprocess.stdout.pipe(process.stdout);
const {stdout} = await subprocess;
```

This also makes chaining processes rather verbose. For example, we just got an issue #521 which was mostly related to that problem. The solution is not as simple as it could be.

```js
const ffmpegProcess = execa('ffmpeg', ['-ss', '5', '-i', 'input.mp4', '-t', '1', '-vf', 'cropdetect', '-f', 'null', '-'])
const awkProcess = execa('awk', ['/crop/ {print $NF}'])
const tailProcess = execa('tail', ['-1'])

ffmpegProcess.stderr.pipe(awkProcess.stdin)
awkProcess.stdout.pipe(tailProcess.stdin)

const { stdout } = await tailProcess
```

Having a simple syntax for chaining processes is especially important with the new `$` API because users might expect the convenience of the `|` shell syntax. For example, `zx` provides 2 ways of doing this:

```js
await $`cat package.json | grep name`
```

```js
await $`cat package.json`.pipe($`grep name`)
```

## Solution

This problem could be solved by introducing a shortcut notation. For example, the above examples above could be written as:

```js
const {stdout} = await execa('echo', ['foo']).pipeStdout(process.stdout);
```

```js
const {stdout} = await execa('ffmpeg', ['-ss', '5', '-i', 'input.mp4', '-t', '1', '-vf', 'cropdetect', '-f', 'null', '-'])
  .pipeStdout(execa('awk', ['/crop/ {print $NF}']))
  .pipeStdout(execa('tail', ['-1']))
```

```js
await $`cat package.json`.pipeStdout($`grep name`)
```

It would do the same thing, just in a shorter way, so it would be rather minimal to implement and document.

### Return value

When piping to a stream (like `process.stdout`), the return value would be the `childProcessResult`, not the stream, since users would most likely want to retrieve the process's result.

However, when piping to another `childProcessResult` (i.e. to its `stdin`), that other `childProcessResult` would be returned. This allows command chaining. Also, this mimics how piping works in shells. For example, in Bash, `$(cat package.json | grep name)` returns the result after `grep` has been applied. Overall, this is what users would expect.

### Stderr

`pipeStderr()` and `pipeAll()` could be available to redirect stderr, or stdout+stderr.

### Synchronous methods

Those methods only make sense with the async `execa()` methods, since they would otherwise be called once the process has already completed.

### Alternative solution

We could also implement this using the new `$` API with a special pipe symbol, like `zx` does.

```js
await $`cat package.json | grep name`
```

However, I think this solution might not be as good for the following reasons:
  - It does not work with normal `execa()` and `execaCommand()` calls
  - It introduces special tokens to the ``$`...` `` syntax. At the moment, the string argument only has command and arguments, with no shell-like syntax, which is much simpler to document and understand. It would also create some confusion for users expecting the command to be run in a shell by default (which is not the case) due to the usage of the pipe symbol.
  - It does not work well with functional utilities. For example, `pipeStdout()` can use `.bind()` or be assigned to a variable or parameter. Using a string makes this kind of manipulation harder.
  - It would require 3 different tokens to redirect to stdout, stderr or both.
  - Pipe characters would need a way to be escaped.

### Naming

If you have other naming ideas for `pipeStdout()`, `pipeStderr()` and `pipeAll()`, please let me know!
  - `stdout()`, etc. does not work because `childProcess.stdout` is already assigned as a stream
  - `stdoutTo()`, etc. could work, but users might understand the work "pipe" better
  - `pipe()` could work for `pipeStdout()`, but users might not understand it is a different method than `Stream.prototype.pipe()`.

--- 

What do you think @sindresorhus?

Pull request description:
Add `pipeStdout()`-related methods
Fixes #525.

This adds the following shortcut methods: `pipeStdout()`, `pipeStderr()` and `pipeAll()`.

For Node.js shell-like scripts, this is the equivalent to the `|`, `|&`, `>`, `&>`, or `2>&1` shell characters.

Pipe to stream, before:

```js
const subprocess = $`echo foo`;
subprocess.stdout.pipe(process.stdout);
const {stdout} = await subprocess;
```

After:

```js
const {stdout} = await $`echo foo`.pipeStdout(process.stdout);
```

Pipe to file, before:

```js
const subprocess = $`echo foo`;
subprocess.stdout.pipe(fs.createWriteStream('stdout.txt'))
const {stdout} = await subprocess;
```

After:

```js
const {stdout} = await $`echo foo`.pipeStdout('stdout.txt');
```

Pipe to another process, before:

```js
const ffmpegProcess = $`ffmpeg -ss 5 -i input.mp4 -t 1 -vf cropdetect -f null -`
const awkProcess = $`awk ${'/crop/ {print $NF}'}`
const tailProcess = $`tail -1`

ffmpegProcess.stderr.pipe(awkProcess.stdin)
awkProcess.stdout.pipe(tailProcess.stdin)

const { stdout } = await tailProcess
```

After:

```js
const {stdout} = await $`ffmpeg -ss 5 -i input.mp4 -t 1 -vf cropdetect -f null -`
  .pipeStderr($`awk ${'/crop/ {print $NF}'}`)
  .pipeStdout($`tail -1`)
```

The PR's code and tests are done, but I haven't added the types, type tests nor readme documentation. I'd like to first confirm the overall idea and design make sense to you @sindresorhus. :)

Patch:
diff --git a/index.d.ts b/index.d.ts
index 83c1727..6c45b62 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,6 +1,6 @@
 import {type Buffer} from 'node:buffer';
 import {type ChildProcess} from 'node:child_process';
-import {type Stream, type Readable as ReadableStream} from 'node:stream';
+import {type Stream, type Readable as ReadableStream, type Writable as WritableStream} from 'node:stream';
 
 export type StdioOption =
 	| 'pipe'
@@ -285,7 +285,9 @@ export type NodeOptions<EncodingType = string> = {
 	readonly nodeOptions?: string[];
 } & Options<EncodingType>;
 
-export type ExecaReturnBase<StdoutStderrType> = {
+type StdoutStderrAll = string | Buffer | undefined;
+
+export type ExecaReturnBase<StdoutStderrType extends StdoutStderrAll> = {
 	/**
 	The file and arguments that were run, for logging purposes.
 
@@ -346,7 +348,7 @@ export type ExecaReturnBase<StdoutStderrType> = {
 	signalDescription?: string;
 };
 
-export type ExecaSyncReturnValue<StdoutStderrType = string> = {
+export type ExecaSyncReturnValue<StdoutStderrType extends StdoutStderrAll = string> = {
 } & ExecaReturnBase<StdoutStderrType>;
 
 /**
@@ -359,7 +361,7 @@ The child process fails when:
 - being canceled
 - there's not enough memory or there are already too many child processes
 */
-export type ExecaReturnValue<StdoutStderrType = string> = {
+export type ExecaReturnValue<StdoutStderrType extends StdoutStderrAll = string> = {
 	/**
 	The output of the process with `stdout` and `stderr` interleaved.
 
@@ -377,7 +379,7 @@ export type ExecaReturnValue<StdoutStderrType = string> = {
 	isCanceled: boolean;
 } & ExecaSyncReturnValue<StdoutStderrType>;
 
-export type ExecaSyncError<StdoutStderrType = string> = {
+export type ExecaSyncError<StdoutStderrType extends StdoutStderrAll = string> = {
 	/**
 	Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored.
 
@@ -398,7 +400,7 @@ export type ExecaSyncError<StdoutStderrType = string> = {
 	originalMessage?: string;
 } & Error & ExecaReturnBase<StdoutStderrType>;
 
-export type ExecaError<StdoutStderrType = string> = {
+export type ExecaError<StdoutStderrType extends StdoutStderrAll = string> = {
 	/**
 	The output of the process with `stdout` and `stderr` interleaved.
 
@@ -425,7 +427,7 @@ export type KillOptions = {
 	forceKillAfterTimeout?: number | false;
 };
 
-export type ExecaChildPromise<StdoutStderrType> = {
+export type ExecaChildPromise<StdoutStderrType extends StdoutStderrAll> = {
 	/**
 	Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr).
 
@@ -448,9 +450,38 @@ export type ExecaChildPromise<StdoutStderrType> = {
 	Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This used to be preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. But now this is deprecated and you should either use `.kill()` or the `signal` option when creating the child process.
 	*/
 	cancel(): void;
+
+	/**
+	[Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be:
+	- Another `execa()` return value
+	- A writable stream
+	- A file path string
+
+	If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result.
+
+	The `stdout` option] must be kept as `pipe`, its default value.
+	*/
+	pipeStdout?<Target extends ExecaChildPromise<StdoutStderrAll>>(target: Target): Target;
+	pipeStdout?(target: WritableStream | string): ExecaChildProcess<StdoutStderrType>;
+
+	/**
+	Like `pipeStdout()` but piping the child process's `stderr` instead.
+
+	The `stderr` option must be kept as `pipe`, its default value.
+	*/
+	pipeStderr?<Target extends ExecaChildPromise<StdoutStderrAll>>(target: Target): Target;
+	pipeStderr?(target: WritableStream | string): ExecaChildProcess<StdoutStderrType>;
+
+	/**
+	Combines both `pipeStdout()` and `pipeStderr()`.
+
+	Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`.
+	*/
+	pipeAll?<Target extends ExecaChildPromise<StdoutStderrAll>>(target: Target): Target;
+	pipeAll?(target: WritableStream | string): ExecaChildProcess<StdoutStderrType>;
 };
 
-export type ExecaChildProcess<StdoutStderrType = string> = ChildProcess &
+export type ExecaChildProcess<StdoutStderrType extends StdoutStderrAll = string> = ChildProcess &
 ExecaChildPromise<StdoutStderrType> &
 Promise<ExecaReturnValue<StdoutStderrType>>;
 
@@ -557,7 +588,7 @@ type TemplateExpression =
 	| ExecaSyncReturnValue<string | Buffer>
 	| Array<string | number | ExecaReturnValue<string | Buffer> | ExecaSyncReturnValue<string | Buffer>>;
 
-type Execa$<StdoutStderrType = string> = {
+type Execa$<StdoutStderrType extends StdoutStderrAll = string> = {
 	/**
 	Same as `execa()` except both file and arguments are specified in a single tagged template string. For example, `` $`echo unicorns` `` is the same as `execa('echo', ['unicorns'])`.
 
diff --git a/index.js b/index.js
index 5c6ac50..e5525b9 100644
--- a/index.js
+++ b/index.js
@@ -9,6 +9,7 @@ import onetime from 'onetime';
 import {makeError} from './lib/error.js';
 import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js';
 import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js';
+import {addPipeMethods} from './lib/pipe.js';
 import {handleInput, getSpawnedResult, makeAllStream, validateInputSync} from './lib/stream.js';
 import {mergePromise, getSpawnedPromise} from './lib/promise.js';
 import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js';
@@ -100,7 +101,8 @@ export function execa(file, args, options) {
 			isCanceled: false,
 			killed: false,
 		}));
-		return mergePromise(dummySpawned, errorPromise);
+		mergePromise(dummySpawned, errorPromise);
+		return dummySpawned;
 	}
 
 	const spawnedPromise = getSpawnedPromise(spawned);
@@ -161,7 +163,9 @@ export function execa(file, args, options) {
 
 	spawned.all = makeAllStream(spawned, parsed.options);
 
-	return mergePromise(spawned, handlePromiseOnce);
+	addPipeMethods(spawned);
+	mergePromise(spawned, handlePromiseOnce);
+	return spawned;
 }
 
 export function execaSync(file, args, options) {
diff --git a/lib/pipe.js b/lib/pipe.js
new file mode 100644
index 0000000..e73ffcc
--- /dev/null
+++ b/lib/pipe.js
@@ -0,0 +1,42 @@
+import {createWriteStream} from 'node:fs';
+import {ChildProcess} from 'node:child_process';
+import {isWritableStream} from 'is-stream';
+
+const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function';
+
+const pipeToTarget = (spawned, streamName, target) => {
+	if (typeof target === 'string') {
+		spawned[streamName].pipe(createWriteStream(target));
+		return spawned;
+	}
+
+	if (isWritableStream(target)) {
+		spawned[streamName].pipe(target);
+		return spawned;
+	}
+
+	if (!isExecaChildProcess(target)) {
+		throw new TypeError('The second argument must be a string, a stream or an Execa child process.');
+	}
+
+	if (!isWritableStream(target.stdin)) {
+		throw new TypeError('The target child process\'s stdin must be available.');
+	}
+
+	spawned[streamName].pipe(target.stdin);
+	return target;
+};
+
+export const addPipeMethods = spawned => {
+	if (spawned.stdout !== null) {
+		spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout');
+	}
+
+	if (spawned.stderr !== null) {
+		spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr');
+	}
+
+	if (spawned.all !== undefined) {
+		spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all');
+	}
+};
diff --git a/lib/promise.js b/lib/promise.js
index 975aee0..a4773f3 100644
--- a/lib/promise.js
+++ b/lib/promise.js
@@ -16,8 +16,6 @@ export const mergePromise = (spawned, promise) => {
 
 		Reflect.defineProperty(spawned, property, {...descriptor, value});
 	}
-
-	return spawned;
 };
 
 // Use promises instead of `child_process` events
diff --git a/readme.md b/readme.md
index 69aa9ea..c16583e 100644
--- a/readme.md
+++ b/readme.md
@@ -20,6 +20,7 @@ This package improves [`child_process`](https://nodejs.org/api/child_process.htm
 - [Executes locally installed binaries by name.](#preferlocal)
 - [Cleans up spawned processes when the parent process dies.](#cleanup)
 - [Get interleaved output](#all) from `stdout` and `stderr` similar to what is printed on the terminal. [*(Async only)*](#execasyncfile-arguments-options)
+- Convenience methods to [pipe processes' output](#redirect-output-to-a-file)
 - [Can specify file and arguments as a single string without a shell](#execacommandcommand-options)
 - More descriptive errors.
 
@@ -115,12 +116,41 @@ unicorns
 rainbows
 ```
 
-### Pipe the child process stdout to the parent
+### Redirect output to a file
+
+```js
+import {execa} from 'execa';
+
+// Similar to `echo unicorns > stdout.txt` in Bash
+await execa('echo', ['unicorns']).pipeStdout('stdout.txt')
+
+// Similar to `echo unicorns 2> stdout.txt` in Bash
+await execa('echo', ['unicorns']).pipeStderr('stderr.txt')
+
+// Similar to `echo unicorns &> stdout.txt` in Bash
+await execa('echo', ['unicorns'], {all:true}).pipeAll('all.txt')
+```
+
+### Save and pipe output from a child process
 
 ```js
 import {execa} from 'execa';
 
-execa('echo', ['unicorns']).stdout.pipe(process.stdout);
+const {stdout} = await execa('echo', ['unicorns']).pipeStdout(process.stdout);
+// Prints `unicorns`
+console.log(stdout);
+// Also returns 'unicorns'
+```
+
+### Pipe multiple processes
+
+```js
+import {execa} from 'execa';
+
+// Similar to `echo unicorns | cat` in Bash
+const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat'));
+console.log(stdout);
+//=> 'unicorns'
 ```
 
 ### Handling Errors
@@ -261,6 +291,29 @@ This is `undefined` if either:
   - the [`all` option](#all-2) is `false` (the default value)
   - both [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio)
 
+#### pipeStdout(target)
+
+[Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be:
+  - Another [`execa()` return value](#pipe-multiple-processes)
+  - A [writable stream](#save-and-pipe-output-from-a-child-process)
+  - A [file path string](#redirect-output-to-a-file)
+
+If the `target` is another [`execa()` return value](#execacommandcommand-options), it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the [final result](#childprocessresult).
+
+The [`stdout` option](#stdout-1) must be kept as `pipe`, its default value.
+
+#### pipeStderr(target)
+
+Like [`pipeStdout()`](#pipestdouttarget) but piping the child process's `stderr` instead.
+
+The [`stderr` option](#stderr-1) must be kept as `pipe`, its default value.
+
+#### pipeAll(target)
+
+Combines both [`pipeStdout()`](#pipestdouttarget) and [`pipeStderr()`](#pipestderrtarget).
+
+Either the [`stdout` option](#stdout-1) or the [`stderr` option](#stderr-1) must be kept as `pipe`, their default value. Also, the [`all` option](#all-2) must be set to `true`.
+
 ### execaSync(file, arguments?, options?)
 
 Execute a file synchronously.
@@ -706,29 +759,6 @@ const run = async () => {
 console.log(await pRetry(run, {retries: 5}));
 ```
 
-### Save and pipe output from a child process
-
-Let's say you want to show the output of a child process in real-time while also saving it to a variable.
-
-```js
-import {execa} from 'execa';
-
-const subprocess = execa('echo', ['foo']);
-subprocess.stdout.pipe(process.stdout);
-
-const {stdout} = await subprocess;
-console.log('child output:', stdout);
-```
-
-### Redirect output to a file
-
-```js
-import {execa} from 'execa';
-
-const subprocess = execa('echo', ['foo'])
-subprocess.stdout.pipe(fs.createWriteStream('stdout.txt'))
-```
-
 ### Redirect input from a file
 
 ```js


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 17b383f..1520899 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -4,7 +4,8 @@ import {Buffer} from 'node:buffer';
 // to get treated as `any` by `@typescript-eslint/no-unsafe-assignment`.
 import * as process from 'node:process';
 import {type Readable as ReadableStream} from 'node:stream';
-import {expectType, expectError} from 'tsd';
+import {createWriteStream} from 'node:fs';
+import {expectType, expectError, expectAssignable} from 'tsd';
 import {
 	$,
 	execa,
@@ -24,6 +25,39 @@ try {
 	execaPromise.cancel();
 	expectType<ReadableStream | undefined>(execaPromise.all);
 
+	const execaBufferPromise = execa('unicorns', {encoding: null});
+	const writeStream = createWriteStream('output.txt');
+
+	expectAssignable<Function | undefined>(execaPromise.pipeStdout);
+	expectType<ExecaChildProcess>(execaPromise.pipeStdout!('file.txt'));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeStdout!('file.txt'));
+	expectType<ExecaChildProcess>(execaPromise.pipeStdout!(writeStream));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeStdout!(writeStream));
+	expectType<ExecaChildProcess>(execaPromise.pipeStdout!(execaPromise));
+	expectType<ExecaChildProcess<Buffer>>(execaPromise.pipeStdout!(execaBufferPromise));
+	expectType<ExecaChildProcess>(execaBufferPromise.pipeStdout!(execaPromise));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeStdout!(execaBufferPromise));
+
+	expectAssignable<Function | undefined>(execaPromise.pipeStderr);
+	expectType<ExecaChildProcess>(execaPromise.pipeStderr!('file.txt'));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeStderr!('file.txt'));
+	expectType<ExecaChildProcess>(execaPromise.pipeStderr!(writeStream));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeStderr!(writeStream));
+	expectType<ExecaChildProcess>(execaPromise.pipeStderr!(execaPromise));
+	expectType<ExecaChildProcess<Buffer>>(execaPromise.pipeStderr!(execaBufferPromise));
+	expectType<ExecaChildProcess>(execaBufferPromise.pipeStderr!(execaPromise));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeStderr!(execaBufferPromise));
+
+	expectAssignable<Function | undefined>(execaPromise.pipeAll);
+	expectType<ExecaChildProcess>(execaPromise.pipeAll!('file.txt'));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeAll!('file.txt'));
+	expectType<ExecaChildProcess>(execaPromise.pipeAll!(writeStream));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeAll!(writeStream));
+	expectType<ExecaChildProcess>(execaPromise.pipeAll!(execaPromise));
+	expectType<ExecaChildProcess<Buffer>>(execaPromise.pipeAll!(execaBufferPromise));
+	expectType<ExecaChildProcess>(execaBufferPromise.pipeAll!(execaPromise));
+	expectType<ExecaChildProcess<Buffer>>(execaBufferPromise.pipeAll!(execaBufferPromise));
+
 	const unicornsResult = await execaPromise;
 	expectType<string>(unicornsResult.command);
 	expectType<string>(unicornsResult.escapedCommand);
@@ -63,6 +97,9 @@ try {
 	expectType<string>(unicornsResult.stdout);
 	expectType<string>(unicornsResult.stderr);
 	expectError(unicornsResult.all);
+	expectError(unicornsResult.pipeStdout);
+	expectError(unicornsResult.pipeStderr);
+	expectError(unicornsResult.pipeAll);
 	expectType<boolean>(unicornsResult.failed);
 	expectType<boolean>(unicornsResult.timedOut);
 	expectError(unicornsResult.isCanceled);
diff --git a/test/pipe.js b/test/pipe.js
new file mode 100644
index 0000000..b465440
--- /dev/null
+++ b/test/pipe.js
@@ -0,0 +1,78 @@
+import {PassThrough, Readable} from 'node:stream';
+import {spawn} from 'node:child_process';
+import {readFile} from 'node:fs/promises';
+import tempfile from 'tempfile';
+import test from 'ava';
+import getStream from 'get-stream';
+import {execa} from '../index.js';
+import {setFixtureDir} from './helpers/fixtures-dir.js';
+
+setFixtureDir();
+
+const pipeToProcess = async (t, fixtureName, funcName) => {
+	const {stdout} = await execa(fixtureName, ['test'], {all: true})[funcName](execa('stdin.js'));
+	t.is(stdout, 'test');
+};
+
+test('pipeStdout() can pipe to Execa child processes', pipeToProcess, 'noop.js', 'pipeStdout');
+test('pipeStderr() can pipe to Execa child processes', pipeToProcess, 'noop-err.js', 'pipeStderr');
+test('pipeAll() can pipe stdout to Execa child processes', pipeToProcess, 'noop.js', 'pipeAll');
+test('pipeAll() can pipe stderr to Execa child processes', pipeToProcess, 'noop-err.js', 'pipeAll');
+
+const pipeToStream = async (t, fixtureName, funcName, streamName) => {
+	const stream = new PassThrough();
+	const result = await execa(fixtureName, ['test'], {all: true})[funcName](stream);
+	t.is(result[streamName], 'test');
+	t.is(await getStream(stream), 'test\n');
+};
+
+test('pipeStdout() can pipe to streams', pipeToStream, 'noop.js', 'pipeStdout', 'stdout');
+test('pipeStderr() can pipe to streams', pipeToStream, 'noop-err.js', 'pipeStderr', 'stderr');
+test('pipeAll() can pipe stdout to streams', pipeToStream, 'noop.js', 'pipeAll', 'stdout');
+test('pipeAll() can pipe stderr to streams', pipeToStream, 'noop-err.js', 'pipeAll', 'stderr');
+
+const pipeToFile = async (t, fixtureName, funcName, streamName) => {
+	const file = tempfile('.txt');
+	const result = await execa(fixtureName, ['test'], {all: true})[funcName](file);
+	t.is(result[streamName], 'test');
+	t.is(await readFile(file, 'utf8'), 'test\n');
+};
+
+test('pipeStdout() can pipe to files', pipeToFile, 'noop.js', 'pipeStdout', 'stdout');
+test('pipeStderr() can pipe to files', pipeToFile, 'noop-err.js', 'pipeStderr', 'stderr');
+test('pipeAll() can pipe stdout to files', pipeToFile, 'noop.js', 'pipeAll', 'stdout');
+test('pipeAll() can pipe stderr to files', pipeToFile, 'noop-err.js', 'pipeAll', 'stderr');
+
+const invalidTarget = (t, funcName, getTarget) => {
+	t.throws(() => execa('noop.js', {all: true})[funcName](getTarget()), {
+		message: /a stream or an Execa child process/,
+	});
+};
+
+test('pipeStdout() can only pipe to writable streams', invalidTarget, 'pipeStdout', () => new Readable());
+test('pipeStderr() can only pipe to writable streams', invalidTarget, 'pipeStderr', () => new Readable());
+test('pipeAll() can only pipe to writable streams', invalidTarget, 'pipeAll', () => new Readable());
+test('pipeStdout() cannot pipe to non-processes', invalidTarget, 'pipeStdout', () => ({stdin: new PassThrough()}));
+test('pipeStderr() cannot pipe to non-processes', invalidTarget, 'pipeStderr', () => ({stdin: new PassThrough()}));
+test('pipeAll() cannot pipe to non-processes', invalidTarget, 'pipeStderr', () => ({stdin: new PassThrough()}));
+test('pipeStdout() cannot pipe to non-Execa processes', invalidTarget, 'pipeStdout', () => spawn('node', ['--version']));
+test('pipeStderr() cannot pipe to non-Execa processes', invalidTarget, 'pipeStderr', () => spawn('node', ['--version']));
+test('pipeAll() cannot pipe to non-Execa processes', invalidTarget, 'pipeStderr', () => spawn('node', ['--version']));
+
+const invalidSource = (t, funcName) => {
+	t.false(funcName in execa('noop.js', {stdout: 'ignore', stderr: 'ignore'}));
+};
+
+test('Must set "stdout" option to "pipe" to use pipeStdout()', invalidSource, 'pipeStdout');
+test('Must set "stderr" option to "pipe" to use pipeStderr()', invalidSource, 'pipeStderr');
+test('Must set "stdout" or "stderr" option to "pipe" to use pipeAll()', invalidSource, 'pipeAll');
+
+const invalidPipeToProcess = async (t, fixtureName, funcName) => {
+	t.throws(() => execa(fixtureName, ['test'], {all: true})[funcName](execa('stdin.js', {stdin: 'ignore'})), {
+		message: /stdin must be available/,
+	});
+};
+
+test('Must set target "stdin" option to "pipe" to use pipeStdout()', invalidPipeToProcess, 'noop.js', 'pipeStdout');
+test('Must set target "stdin" option to "pipe" to use pipeStderr()', invalidPipeToProcess, 'noop-err.js', 'pipeStderr');
+test('Must set target "stdin" option to "pipe" to use pipeAll()', invalidPipeToProcess, 'noop.js', 'pipeAll');