Repository: sindresorhus/execa (js)

Issue:
`${childProcessResult}` shortcut notation
[zx](https://github.com/google/zx/) has a `${childProcessResult}` shortcut notation:

```js
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
```

This is equivalent to:

```js
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch.stdout.replace(/\n$/, '')}`
```

This is quite handy and would be minimal to implement for us as part of the new `$` API. 
What do you think @sindresorhus @aaronccasanova?

Pull request description:
Allow `execa` return values in `$` template expressions
Closes #523 

This PR adds support for interpolating `ExecaReturnValue` and `ExecaSyncReturnValue` in `$` template expressions:

#### Asynchronous

```js
import {$} from 'execa';

const rainbows = await $`echo rainbows`;

await $({stdio: 'inherit'})`echo ${rainbows} unicorns`;
//=> 'rainbows unicorns'
```

#### Synchronous

```js
import {$} from 'execa';

const rainbows = $.sync`echo rainbows`;

$({stdio: 'inherit'}).sync`echo ${rainbows} unicorns`;
//=> 'rainbows unicorns'
```

Patch:
diff --git a/index.d.ts b/index.d.ts
index dc97323..b5a681a 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -27,7 +27,7 @@ export type CommonOptions<EncodingType> = {
 
 	If you `$ npm install foo`, you can then `execa('foo')`.
 
-	@default false
+	@default `true` with `$`/`$.sync`, `false` otherwise
 	*/
 	readonly preferLocal?: boolean;
 
@@ -541,7 +541,12 @@ console.log(stdout);
 export function execaCommand(command: string, options?: Options): ExecaChildProcess;
 export function execaCommand(command: string, options?: Options<null>): ExecaChildProcess<Buffer>;
 
-type TemplateExpression = string | number | Array<string | number>;
+type TemplateExpression =
+	| string
+	| number
+	| ExecaReturnValue<string | Buffer>
+	| ExecaSyncReturnValue<string | Buffer>
+	| Array<string | number | ExecaReturnValue<string | Buffer> | ExecaSyncReturnValue<string | Buffer>>;
 
 type Execa$<StdoutStderrType = string> = {
 	/**
@@ -551,6 +556,8 @@ type Execa$<StdoutStderrType = string> = {
 
 	The `shell` option must be used if the `command` uses shell-specific features (for example, `&&` or `||`), as opposed to being a simple `file` followed by its `arguments`.
 
+	As a convenience, the result from previous `` $`command` `` or `` $.sync`command` `` calls can be used as template expressions in subsequent commands and `$`/`$.sync` will use the `stdout` value. See the example below `` with results from `$` or `$.sync` `` for more details.
+
 	@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.
 
 	@example <caption>Basic</caption>
@@ -596,6 +603,16 @@ type Execa$<StdoutStderrType = string> = {
 	$({stdio: 'inherit'}).sync`echo rainbows`;
 	//=> 'rainbows'
 	```
+
+	@example <caption>With results from `$` or `$.sync`</caption>
+	```
+	import {$} from 'execa';
+
+	const unicorns = await $`echo unicorns`;
+
+	$({stdio: 'inherit'}).sync`echo ${unicorns} rainbows`;
+	//=> 'unicorns rainbows'
+	```
 	*/
 	(options: Options<undefined>): Execa$<StdoutStderrType>;
 	(options: Options): Execa$;
@@ -635,6 +652,8 @@ It's important to note that quotes, backslashes, and spaces are automatically es
 
 The `shell` option must be used if the `command` uses shell-specific features (for example, `&&` or `||`), as opposed to being a simple `file` followed by its `arguments`.
 
+As a convenience, the result from previous `` $`command` `` or `` $.sync`command` `` calls can be used as template expressions in subsequent commands and `$`/`$.sync` will use the `stdout` value. See the example below `` with results from `$` or `$.sync` `` for more details.
+
 @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.
 
 @example <caption>Basic</caption>
@@ -680,6 +699,16 @@ console.log(stdout);
 $({stdio: 'inherit'}).sync`echo rainbows`;
 //=> 'rainbows'
 ```
+
+@example <caption>With results from `$` or `$.sync`</caption>
+```
+import {$} from 'execa';
+
+const unicorns = await $`echo unicorns`;
+
+$({stdio: 'inherit'}).sync`echo ${unicorns} rainbows`;
+//=> 'unicorns rainbows'
+```
 */
 export const $: Execa$;
 
diff --git a/index.js b/index.js
index e9f3582..bd49574 100644
--- a/index.js
+++ b/index.js
@@ -242,7 +242,7 @@ function create$(options) {
 	return $;
 }
 
-export const $ = create$();
+export const $ = create$({preferLocal: true});
 
 export function execaCommand(command, options) {
 	const [file, ...args] = parseCommand(command);
diff --git a/lib/command.js b/lib/command.js
index d40877f..b6d2e5e 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -1,3 +1,6 @@
+import {Buffer} from 'node:buffer';
+import {ChildProcess} from 'node:child_process';
+
 const normalizeArgs = (file, args = []) => {
 	if (!Array.isArray(args)) {
 		return [file];
@@ -40,6 +43,39 @@ export const parseCommand = command => {
 	return tokens;
 };
 
+const parseExpression = expression => {
+	const typeOfExpression = typeof expression;
+
+	if (typeOfExpression === 'string') {
+		return expression;
+	}
+
+	if (typeOfExpression === 'number') {
+		return String(expression);
+	}
+
+	if (
+		typeOfExpression === 'object'
+		&& expression !== null
+		&& !(expression instanceof ChildProcess)
+		&& 'stdout' in expression
+	) {
+		const typeOfStdout = typeof expression.stdout;
+
+		if (typeOfStdout === 'string') {
+			return expression.stdout;
+		}
+
+		if (Buffer.isBuffer(expression.stdout)) {
+			return expression.stdout.toString();
+		}
+
+		throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`);
+	}
+
+	throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`);
+};
+
 const parseTemplate = (template, index, templates, expressions) => {
 	const templateString = template ?? templates.raw[index];
 	const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean);
@@ -51,8 +87,8 @@ const parseTemplate = (template, index, templates, expressions) => {
 	const expression = expressions[index];
 
 	return Array.isArray(expression)
-		? [...templateTokens, ...expression.map(String)]
-		: [...templateTokens, String(expression)];
+		? [...templateTokens, ...expression.map(expression => parseExpression(expression))]
+		: [...templateTokens, parseExpression(expression)];
 };
 
 export const parseTemplates = (templates, expressions) => templates.flatMap(
diff --git a/readme.md b/readme.md
index 0b9a5a2..09bfe67 100644
--- a/readme.md
+++ b/readme.md
@@ -90,6 +90,17 @@ $({stdio: 'inherit'}).sync`echo rainbows`;
 //=> 'rainbows'
 ```
 
+#### With results from `$` or `$.sync`
+
+```js
+import {$} from 'execa';
+
+const unicorns = await $`echo unicorns`;
+
+$({stdio: 'inherit'}).sync`echo ${unicorns} rainbows`;
+//=> 'unicorns rainbows'
+```
+
 ### Pipe the child process stdout to the parent
 
 ```js
@@ -250,6 +261,8 @@ It's important to note that quotes, backslashes, and spaces are automatically es
 
 The [`shell` option](#shell) must be used if the `command` uses shell-specific features (for example, `&&` or `||`), as opposed to being a simple `file` followed by its `arguments`.
 
+As a convenience, the result from previous [`` $`command` ``](#command) or [`` $.sync`command` ``](#synccommand) calls can be used as template expressions in subsequent commands and `$`/`$.sync` will use the `stdout` value. See the example above [with results from `$` or `$.sync`](#with-results-from--or-sync) for more details.
+
 Returns a `Promise` that resolves or rejects with a [`childProcessResult`](#childProcessResult).
 
 ### $.sync\`command\`
@@ -312,7 +325,7 @@ This is not escaped and should not be executed directly as a process, including
 
 Type: `string`
 
-Same as [`command`](#command) but escaped.
+Same as [`command`](#command-1) but escaped.
 
 This is meant to be copy and pasted into a shell, for debugging purposes.
 Since the escaping is fairly basic, this should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execaCommand()`](#execacommandcommand-options).
@@ -425,7 +438,7 @@ Kill the spawned process when the parent process exits unless either:
 #### preferLocal
 
 Type: `boolean`\
-Default: `false`
+Default: `true` with [`$`](#command)/[`$.sync`](#synccommand), `false` otherwise
 
 Prefer locally installed binaries when looking for a binary to execute.\
 If you `$ npm install foo`, you can then `execa('foo')`.


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 32fb555..a2a6d21 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -265,6 +265,12 @@ expectType<ExecaReturnValue>(await $`unicorns ${['foo', 'bar']}`);
 expectType<ExecaSyncReturnValue>($.sync`unicorns ${['foo', 'bar']}`);
 expectType<ExecaReturnValue>(await $`unicorns ${[1, 2]}`);
 expectType<ExecaSyncReturnValue>($.sync`unicorns ${[1, 2]}`);
+expectType<ExecaReturnValue>(await $`unicorns ${await $`echo foo`}`);
+expectError<ExecaReturnValue>(await $`unicorns ${$`echo foo`}`);
+expectType<ExecaSyncReturnValue>($.sync`unicorns ${$.sync`echo foo`}`);
+expectType<ExecaReturnValue>(await $`unicorns ${[await $`echo foo`, 'bar']}`);
+expectError<ExecaReturnValue>(await $`unicorns ${[$`echo foo`, 'bar']}`);
+expectType<ExecaSyncReturnValue>($.sync`unicorns ${[$.sync`echo foo`, 'bar']}`);
 expectType<ExecaReturnValue>(await $`unicorns ${true.toString()}`);
 expectType<ExecaSyncReturnValue>($.sync`unicorns ${false.toString()}`);
 expectError<ExecaReturnValue>(await $`unicorns ${true}`);
diff --git a/test/command.js b/test/command.js
index 1659306..43b7495 100644
--- a/test/command.js
+++ b/test/command.js
@@ -1,3 +1,4 @@
+import {inspect} from 'node:util';
 import test from 'ava';
 import {execa, execaSync, execaCommand, execaCommandSync, $} from '../index.js';
 import {setFixtureDir} from './helpers/fixtures-dir.js';
@@ -101,11 +102,40 @@ test('$ allows string interpolation', async t => {
 	t.is(stdout, 'foo\nbar');
 });
 
+test('$ allows number interpolation', async t => {
+	const {stdout} = await $`node test/fixtures/echo.js 1 ${2}`;
+	t.is(stdout, '1\n2');
+});
+
 test('$ allows array interpolation', async t => {
 	const {stdout} = await $`node test/fixtures/echo.js ${['foo', 'bar']}`;
 	t.is(stdout, 'foo\nbar');
 });
 
+test('$ allows execa return value interpolation', async t => {
+	const foo = await $`node test/fixtures/echo.js foo`;
+	const {stdout} = await $`node test/fixtures/echo.js ${foo} bar`;
+	t.is(stdout, 'foo\nbar');
+});
+
+test('$ allows execa return value array interpolation', async t => {
+	const foo = await $`node test/fixtures/echo.js foo`;
+	const {stdout} = await $`node test/fixtures/echo.js ${[foo, 'bar']}`;
+	t.is(stdout, 'foo\nbar');
+});
+
+test('$ allows execa return value buffer interpolation', async t => {
+	const foo = await $({encoding: null})`node test/fixtures/echo.js foo`;
+	const {stdout} = await $`node test/fixtures/echo.js ${foo} bar`;
+	t.is(stdout, 'foo\nbar');
+});
+
+test('$ allows execa return value buffer array interpolation', async t => {
+	const foo = await $({encoding: null})`node test/fixtures/echo.js foo`;
+	const {stdout} = await $`node test/fixtures/echo.js ${[foo, 'bar']}`;
+	t.is(stdout, 'foo\nbar');
+});
+
 test('$ ignores consecutive spaces', async t => {
 	const {stdout} = await $`node test/fixtures/echo.js foo    bar`;
 	t.is(stdout, 'foo\nbar');
@@ -141,11 +171,6 @@ test('$ handles invalid escape sequence', async t => {
 	t.is(stdout, '\\u');
 });
 
-test('$ coerces all interpolated expressions to strings', async t => {
-	const {stdout} = await $`node test/fixtures/echo.js ${0} ${undefined} ${null}`;
-	t.is(stdout, '0\nundefined\nnull');
-});
-
 test('$ allows escaping spaces in commands with interpolation', async t => {
 	const {stdout} = await $`${'command with space.js'} foo bar`;
 	t.is(stdout, 'foo\nbar');
@@ -170,3 +195,77 @@ test('$.sync accepts options', t => {
 	const {stdout} = $({stripFinalNewline: true}).sync`noop.js foo`;
 	t.is(stdout, 'foo');
 });
+
+test('$.sync allows execa return value interpolation', t => {
+	const foo = $.sync`node test/fixtures/echo.js foo`;
+	const {stdout} = $.sync`node test/fixtures/echo.js ${foo} bar`;
+	t.is(stdout, 'foo\nbar');
+});
+
+test('$.sync allows execa return value array interpolation', t => {
+	const foo = $.sync`node test/fixtures/echo.js foo`;
+	const {stdout} = $.sync`node test/fixtures/echo.js ${[foo, 'bar']}`;
+	t.is(stdout, 'foo\nbar');
+});
+
+test('$.sync allows execa return value buffer interpolation', t => {
+	const foo = $({encoding: null}).sync`node test/fixtures/echo.js foo`;
+	const {stdout} = $.sync`node test/fixtures/echo.js ${foo} bar`;
+	t.is(stdout, 'foo\nbar');
+});
+
+test('$.sync allows execa return value buffer array interpolation', t => {
+	const foo = $({encoding: null}).sync`node test/fixtures/echo.js foo`;
+	const {stdout} = $.sync`node test/fixtures/echo.js ${[foo, 'bar']}`;
+	t.is(stdout, 'foo\nbar');
+});
+
+const invalidExpression = test.macro({
+	async exec(t, input, expected) {
+		await t.throwsAsync(
+			async () => $`node test/fixtures/echo.js ${input}`,
+			{instanceOf: TypeError, message: expected},
+		);
+
+		t.throws(
+			() => $.sync`node test/fixtures/echo.js ${input}`,
+			{instanceOf: TypeError, message: expected},
+		);
+	},
+	title(prettyInput, input, expected) {
+		return `$ APIs throw on invalid '${prettyInput ?? inspect(input)}' expression with '${expected}'`;
+	},
+});
+
+test(invalidExpression, undefined, 'Unexpected "undefined" in template expression');
+test(invalidExpression, [undefined], 'Unexpected "undefined" in template expression');
+
+test(invalidExpression, null, 'Unexpected "object" in template expression');
+test(invalidExpression, [null], 'Unexpected "object" in template expression');
+
+test(invalidExpression, true, 'Unexpected "boolean" in template expression');
+test(invalidExpression, [true], 'Unexpected "boolean" in template expression');
+
+test(invalidExpression, {}, 'Unexpected "object" in template expression');
+test(invalidExpression, [{}], 'Unexpected "object" in template expression');
+
+test(invalidExpression, {foo: 'bar'}, 'Unexpected "object" in template expression');
+test(invalidExpression, [{foo: 'bar'}], 'Unexpected "object" in template expression');
+
+test(invalidExpression, {stdout: undefined}, 'Unexpected "undefined" stdout in template expression');
+test(invalidExpression, [{stdout: undefined}], 'Unexpected "undefined" stdout in template expression');
+
+test(invalidExpression, {stdout: 1}, 'Unexpected "number" stdout in template expression');
+test(invalidExpression, [{stdout: 1}], 'Unexpected "number" stdout in template expression');
+
+test(invalidExpression, Promise.resolve(), 'Unexpected "object" in template expression');
+test(invalidExpression, [Promise.resolve()], 'Unexpected "object" in template expression');
+
+test(invalidExpression, Promise.resolve({stdout: 'foo'}), 'Unexpected "object" in template expression');
+test(invalidExpression, [Promise.resolve({stdout: 'foo'})], 'Unexpected "object" in template expression');
+
+test('$`noop.js`', invalidExpression, $`noop.js`, 'Unexpected "object" in template expression');
+test('[ $`noop.js` ]', invalidExpression, [$`noop.js`], 'Unexpected "object" in template expression');
+
+test('$({stdio: \'inherit\'}).sync`noop.js`', invalidExpression, $({stdio: 'inherit'}).sync`noop.js`, 'Unexpected "undefined" stdout in template expression');
+test('[ $({stdio: \'inherit\'}).sync`noop.js` ]', invalidExpression, [$({stdio: 'inherit'}).sync`noop.js`], 'Unexpected "undefined" stdout in template expression');
diff --git a/test/test.js b/test/test.js
index 47192fb..14bcabe 100644
--- a/test/test.js
+++ b/test/test.js
@@ -4,7 +4,7 @@ import {fileURLToPath, pathToFileURL} from 'node:url';
 import test from 'ava';
 import isRunning from 'is-running';
 import getNode from 'get-node';
-import {execa, execaSync} from '../index.js';
+import {execa, execaSync, $} from '../index.js';
 import {setFixtureDir, PATH_KEY} from './helpers/fixtures-dir.js';
 
 setFixtureDir();
@@ -96,6 +96,10 @@ test('preferLocal: undefined', async t => {
 	await t.throwsAsync(execa('ava', ['--version'], {env: getPathWithoutLocalDir()}), {message: ENOENT_REGEXP});
 });
 
+test('preferLocal: undefined with $', async t => {
+	await t.notThrowsAsync($({env: getPathWithoutLocalDir()})`ava --version`);
+});
+
 test('localDir option', async t => {
 	const command = process.platform === 'win32' ? 'echo %PATH%' : 'echo $PATH';
 	const {stdout} = await execa(command, {shell: true, preferLocal: true, localDir: '/test'});