Repository: sindresorhus/execa (js)

Issue:
Improved syntax for the `stdin|stdout|stderr: filePath` option
Background in https://github.com/sindresorhus/execa/issues/656#issuecomment-1882593657

Instead of passing the `stdin: './path.txt'` option, users should pass `stdin: { file: './path.txt' }` instead. This applies also to the `stdout` and `stderr` options. This does not apply to file `URLs`.

The reason is: those options can also be a string such as `inherit` or `pipe`. This creates the current problems:
  - This requires relative file paths to start with `.`
  - This could be a security problem when the file path is dynamic, as the user providing that file path could make it `inherit`, `pipe`, etc.

The new syntax is a plain object with a single `file` property, which value is a string.

Pull request description:
Improve syntax of `std*: filePath` option
Fixes #667.

This replaces the `{ stdin: './file/path' }` option syntax by `{ stdin: { file: './file/path' } }` syntax instead, in order to avoid typing conflict with `{ stdin: string }` such as `{ stdin: 'inherit' }`. 

This PR also applies to the `stdout`, `stderr` and `stdio` options.

Since passing a file path to the `std*` options has not been released yet, this is not a breaking change.

Patch:
diff --git a/index.d.ts b/index.d.ts
index bbba21e..757d030 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -13,7 +13,7 @@ type CommonStdioOption =
 	| number
 	| undefined
 	| URL
-	| string;
+	| {file: string};
 
 type InputStdioOption =
 	| Iterable<string | Uint8Array>
@@ -119,7 +119,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- `'inherit'`: Re-use the current process' `stdin`.
 	- an integer: Re-use a specific file descriptor from the current process.
 	- a Node.js `Readable` stream.
-	- a file path. If relative, it must start with `.`.
+	- `{ file: 'path' }` object.
 	- a file URL.
 	- a web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
 	- an [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) or an [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
@@ -140,7 +140,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- `'inherit'`: Re-use the current process' `stdout`.
 	- an integer: Re-use a specific file descriptor from the current process.
 	- a Node.js `Writable` stream.
-	- a file path. If relative, it must start with `.`.
+	- `{ file: 'path' }` object.
 	- a file URL.
 	- a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
@@ -159,7 +159,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	- `'inherit'`: Re-use the current process' `stderr`.
 	- an integer: Re-use a specific file descriptor from the current process.
 	- a Node.js `Writable` stream.
-	- a file path. If relative, it must start with `.`.
+	- `{ file: 'path' }` object.
 	- a file URL.
 	- a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
diff --git a/lib/stdio/async.js b/lib/stdio/async.js
index 6d067bd..31826f5 100644
--- a/lib/stdio/async.js
+++ b/lib/stdio/async.js
@@ -14,14 +14,16 @@ const forbiddenIfAsync = ({type, optionName}) => {
 
 const addPropertiesAsync = {
 	input: {
-		filePath: ({value}) => ({value: createReadStream(value)}),
+		fileUrl: ({value}) => ({value: createReadStream(value)}),
+		filePath: ({value}) => ({value: createReadStream(value.file)}),
 		webStream: ({value}) => ({value: Readable.fromWeb(value)}),
 		iterable: ({value}) => ({value: Readable.from(value)}),
 		string: ({value}) => ({value: Readable.from(value)}),
 		uint8Array: ({value}) => ({value: Readable.from(Buffer.from(value))}),
 	},
 	output: {
-		filePath: ({value}) => ({value: createWriteStream(value)}),
+		fileUrl: ({value}) => ({value: createWriteStream(value)}),
+		filePath: ({value}) => ({value: createWriteStream(value.file)}),
 		webStream: ({value}) => ({value: Writable.fromWeb(value)}),
 		iterable: forbiddenIfAsync,
 		uint8Array: forbiddenIfAsync,
diff --git a/lib/stdio/direction.js b/lib/stdio/direction.js
index d155b0f..22c2172 100644
--- a/lib/stdio/direction.js
+++ b/lib/stdio/direction.js
@@ -28,6 +28,7 @@ const KNOWN_DIRECTIONS = ['input', 'output', 'output'];
 
 // `string` can only be added through the `input` option, i.e. does not need to be handled here
 const guessStreamDirection = {
+	fileUrl: () => undefined,
 	filePath: () => undefined,
 	iterable: () => 'input',
 	uint8Array: () => 'input',
diff --git a/lib/stdio/handle.js b/lib/stdio/handle.js
index 194a438..56be992 100644
--- a/lib/stdio/handle.js
+++ b/lib/stdio/handle.js
@@ -71,7 +71,7 @@ For example, you can use the \`pathToFileURL()\` method of the \`url\` core modu
 	}
 
 	if (isUnknownStdioString(type, value)) {
-		throw new TypeError(`The \`${optionName}: filePath\` option must either be an absolute file path or start with \`.\`.`);
+		throw new TypeError(`The \`${optionName}: { file: '...' }\` option must be used instead of \`${optionName}: '...'\`.`);
 	}
 };
 
diff --git a/lib/stdio/input.js b/lib/stdio/input.js
index 8572761..3fa5a6d 100644
--- a/lib/stdio/input.js
+++ b/lib/stdio/input.js
@@ -1,4 +1,5 @@
 import {isReadableStream} from 'is-stream';
+import {isUrl, isFilePathString} from './type.js';
 import {isUint8Array} from './utils.js';
 
 // Append the `stdin` option with the `input` and `inputFile` options
@@ -8,13 +9,13 @@ export const handleInputOptions = ({input, inputFile}) => [
 ].filter(Boolean);
 
 const handleInputOption = input => input === undefined ? undefined : {
-	type: getType(input),
+	type: getInputType(input),
 	value: input,
 	optionName: 'input',
 	index: 0,
 };
 
-const getType = input => {
+const getInputType = input => {
 	if (isReadableStream(input)) {
 		return 'nodeStream';
 	}
@@ -31,8 +32,19 @@ const getType = input => {
 };
 
 const handleInputFileOption = inputFile => inputFile === undefined ? undefined : {
-	type: 'filePath',
-	value: inputFile,
+	...getInputFileType(inputFile),
 	optionName: 'inputFile',
 	index: 0,
 };
+
+const getInputFileType = inputFile => {
+	if (isUrl(inputFile)) {
+		return {type: 'fileUrl', value: inputFile};
+	}
+
+	if (isFilePathString(inputFile)) {
+		return {type: 'filePath', value: {file: inputFile}};
+	}
+
+	throw new Error('The `inputFile` option must be a file path string or a file URL.');
+};
diff --git a/lib/stdio/sync.js b/lib/stdio/sync.js
index 7bb8772..d50ea3f 100644
--- a/lib/stdio/sync.js
+++ b/lib/stdio/sync.js
@@ -25,13 +25,15 @@ const forbiddenIfSync = ({type, optionName}) => {
 
 const addPropertiesSync = {
 	input: {
-		filePath: ({value}) => ({value: bufferToUint8Array(readFileSync(value)), type: 'uint8Array'}),
+		fileUrl: ({value}) => ({value: bufferToUint8Array(readFileSync(value)), type: 'uint8Array'}),
+		filePath: ({value}) => ({value: bufferToUint8Array(readFileSync(value.file)), type: 'uint8Array'}),
 		webStream: forbiddenIfSync,
 		nodeStream: forbiddenIfSync,
 		iterable: forbiddenIfSync,
 		native: forbiddenIfStreamSync,
 	},
 	output: {
+		filePath: ({value}) => ({value: value.file}),
 		webStream: forbiddenIfSync,
 		nodeStream: forbiddenIfSync,
 		iterable: forbiddenIfSync,
@@ -69,7 +71,7 @@ const pipeStdioOptionSync = (result, {type, value, direction}) => {
 		return;
 	}
 
-	if (type === 'filePath') {
+	if (type === 'fileUrl' || type === 'filePath') {
 		writeFileSync(value, result);
 	}
 };
diff --git a/lib/stdio/type.js b/lib/stdio/type.js
index b3e5945..e829858 100644
--- a/lib/stdio/type.js
+++ b/lib/stdio/type.js
@@ -1,10 +1,13 @@
-import {isAbsolute} from 'node:path';
 import {isStream as isNodeStream} from 'is-stream';
 import {isUint8Array} from './utils.js';
 
 // The `stdin`/`stdout`/`stderr` option can be of many types. This detects it.
 export const getStdioOptionType = stdioOption => {
-	if (isFileUrl(stdioOption) || isFilePath(stdioOption)) {
+	if (isUrl(stdioOption)) {
+		return 'fileUrl';
+	}
+
+	if (isFilePathObject(stdioOption)) {
 		return 'filePath';
 	}
 
@@ -27,13 +30,14 @@ export const getStdioOptionType = stdioOption => {
 	return 'native';
 };
 
-const isUrlInstance = stdioOption => Object.prototype.toString.call(stdioOption) === '[object URL]';
-const hasFileProtocol = url => url.protocol === 'file:';
-const isFileUrl = stdioOption => isUrlInstance(stdioOption) && hasFileProtocol(stdioOption);
-export const isRegularUrl = stdioOption => isUrlInstance(stdioOption) && !hasFileProtocol(stdioOption);
+export const isUrl = stdioOption => Object.prototype.toString.call(stdioOption) === '[object URL]';
+export const isRegularUrl = stdioOption => isUrl(stdioOption) && stdioOption.protocol !== 'file:';
 
-const stringIsFilePath = stdioOption => stdioOption.startsWith('.') || isAbsolute(stdioOption);
-const isFilePath = stdioOption => typeof stdioOption === 'string' && stringIsFilePath(stdioOption);
+const isFilePathObject = stdioOption => typeof stdioOption === 'object'
+	&& stdioOption !== null
+	&& Object.keys(stdioOption).length === 1
+	&& isFilePathString(stdioOption.file);
+export const isFilePathString = file => typeof file === 'string';
 
 export const isUnknownStdioString = (type, stdioOption) => type === 'native' && typeof stdioOption === 'string' && !KNOWN_STDIO_STRINGS.has(stdioOption);
 const KNOWN_STDIO_STRINGS = new Set(['ipc', 'ignore', 'inherit', 'overlapped', 'pipe']);
@@ -49,7 +53,8 @@ const isIterableObject = stdioOption => typeof stdioOption === 'object'
 
 // Convert types to human-friendly strings for error messages
 export const TYPE_TO_MESSAGE = {
-	filePath: 'a file path',
+	fileUrl: 'a file URL',
+	filePath: 'a file path string',
 	webStream: 'a web stream',
 	nodeStream: 'a Node.js stream',
 	native: 'any value',
diff --git a/lib/stream.js b/lib/stream.js
index 62ec322..a0b9128 100644
--- a/lib/stream.js
+++ b/lib/stream.js
@@ -71,7 +71,7 @@ const getCustomStreams = stdioStreams => stdioStreams.filter(({type}) => type !=
 // However, for the `stdin`/`input`/`inputFile` options, we only wait for errors, not completion.
 // This is because source streams completion should end destination streams, but not the other way around.
 // This allows for source streams to pipe to multiple destinations.
-// We make an exception for `filePath`, since we create that source stream and we know it is piped to a single destination.
+// We make an exception for `fileUrl` and `filePath`, since we create that source stream and we know it is piped to a single destination.
 const waitForCustomStreamsEnd = customStreams => customStreams
 	.filter(({value, type, direction}) => shouldWaitForCustomStream(value, type, direction))
 	.map(({value}) => finished(value));
@@ -80,8 +80,8 @@ const throwOnCustomStreamsError = customStreams => customStreams
 	.filter(({value, type, direction}) => !shouldWaitForCustomStream(value, type, direction))
 	.map(({value}) => throwOnStreamError(value));
 
-const shouldWaitForCustomStream = (value, type, direction) => (type === 'filePath' || direction === 'output')
-	&& !STANDARD_STREAMS.includes(value);
+const shouldWaitForCustomStream = (value, type, direction) => (type === 'fileUrl' || type === 'filePath')
+	|| (direction === 'output' && !STANDARD_STREAMS.includes(value));
 
 const throwIfStreamError = stream => stream === null ? [] : [throwOnStreamError(stream)];
 
diff --git a/readme.md b/readme.md
index 1c55e1d..c5b1020 100644
--- a/readme.md
+++ b/readme.md
@@ -583,7 +583,7 @@ Default: `inherit` with [`$`](#command), `pipe` otherwise
 - `'inherit'`: Re-use the current process' `stdin`.
 - an integer: Re-use a specific file descriptor from the current process.
 - a [Node.js `Readable` stream](#redirect-a-nodejs-stream-fromto-stdinstdoutstderr).
-- a file path. If relative, it must start with `.`.
+- `{ file: 'path' }` object.
 - a file URL.
 - a web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
 - an [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) or an [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
@@ -604,7 +604,7 @@ Default: `pipe`
 - `'inherit'`: Re-use the current process' `stdout`.
 - an integer: Re-use a specific file descriptor from the current process.
 - a [Node.js `Writable` stream](#redirect-a-nodejs-stream-fromto-stdinstdoutstderr).
-- a file path. If relative, it must start with `.`.
+- `{ file: 'path' }` object.
 - a file URL.
 - a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 
@@ -623,7 +623,7 @@ Default: `pipe`
 - `'inherit'`: Re-use the current process' `stderr`.
 - an integer: Re-use a specific file descriptor from the current process.
 - a [Node.js `Writable` stream](#redirect-a-nodejs-stream-fromto-stdinstdoutstderr).
-- a file path. If relative, it must start with `.`.
+- `{ file: 'path' }` object.
 - a file URL.
 - a web [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
 


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 415c9ed..5e88bb4 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -190,8 +190,8 @@ expectError(execa('unicorns', {stdin: numberGenerator()}));
 expectError(execa('unicorns', {stdin: [numberGenerator()]}));
 execa('unicorns', {stdin: fileUrl});
 execa('unicorns', {stdin: [fileUrl]});
-execa('unicorns', {stdin: './test'});
-execa('unicorns', {stdin: ['./test']});
+execa('unicorns', {stdin: {file: './test'}});
+execa('unicorns', {stdin: [{file: './test'}]});
 execa('unicorns', {stdin: 1});
 execa('unicorns', {stdin: [1]});
 execa('unicorns', {stdin: undefined});
@@ -219,8 +219,8 @@ expectError(execa('unicorns', {stdout: new ReadableStream()}));
 expectError(execa('unicorn', {stdout: [new ReadableStream()]}));
 execa('unicorns', {stdout: fileUrl});
 execa('unicorns', {stdout: [fileUrl]});
-execa('unicorns', {stdout: './test'});
-execa('unicorns', {stdout: ['./test']});
+execa('unicorns', {stdout: {file: './test'}});
+execa('unicorns', {stdout: [{file: './test'}]});
 execa('unicorns', {stdout: 1});
 execa('unicorns', {stdout: [1]});
 execa('unicorns', {stdout: undefined});
@@ -248,8 +248,8 @@ expectError(execa('unicorns', {stderr: new ReadableStream()}));
 expectError(execa('unicorns', {stderr: [new ReadableStream()]}));
 execa('unicorns', {stderr: fileUrl});
 execa('unicorns', {stderr: [fileUrl]});
-execa('unicorns', {stderr: './test'});
-execa('unicorns', {stderr: ['./test']});
+execa('unicorns', {stderr: {file: './test'}});
+execa('unicorns', {stderr: [{file: './test'}]});
 execa('unicorns', {stderr: 1});
 execa('unicorns', {stderr: [1]});
 execa('unicorns', {stderr: undefined});
@@ -271,7 +271,7 @@ execa('unicorns', {stdio: 'inherit'});
 expectError(execa('unicorns', {stdio: 'ipc'}));
 expectError(execa('unicorns', {stdio: 1}));
 expectError(execa('unicorns', {stdio: fileUrl}));
-expectError(execa('unicorns', {stdio: './test'}));
+expectError(execa('unicorns', {stdio: {file: './test'}}));
 expectError(execa('unicorns', {stdio: new Writable()}));
 expectError(execa('unicorns', {stdio: new Readable()}));
 expectError(execa('unicorns', {stdio: new WritableStream()}));
@@ -302,7 +302,7 @@ execa('unicorns', {
 		1,
 		undefined,
 		fileUrl,
-		'./test',
+		{file: './test'},
 		new Writable(),
 		new Readable(),
 		new WritableStream(),
@@ -324,7 +324,7 @@ execa('unicorns', {
 		[1],
 		[undefined],
 		[fileUrl],
-		['./test'],
+		[{file: './test'}],
 		[new Writable()],
 		[new Readable()],
 		[new WritableStream()],
diff --git a/test/stdio/array.js b/test/stdio/array.js
index cfa9c11..84109a8 100644
--- a/test/stdio/array.js
+++ b/test/stdio/array.js
@@ -116,7 +116,7 @@ test('Cannot pass both readable and writable values to stdio[*] - process.stderr
 
 const testAmbiguousDirection = async (t, execaMethod) => {
 	const [filePathOne, filePathTwo] = [tempfile(), tempfile()];
-	await execaMethod('noop-fd3.js', ['foobar'], getStdioOption([filePathOne, filePathTwo]));
+	await execaMethod('noop-fd3.js', ['foobar'], getStdioOption([{file: filePathOne}, {file: filePathTwo}]));
 	t.deepEqual(await Promise.all([readFile(filePathOne, 'utf8'), readFile(filePathTwo, 'utf8')]), ['foobar\n', 'foobar\n']);
 	await Promise.all([rm(filePathOne), rm(filePathTwo)]);
 };
@@ -127,7 +127,7 @@ test('stdio[*] default direction is output - sync', testAmbiguousDirection, exec
 const testAmbiguousMultiple = async (t, fixtureName, getOptions) => {
 	const filePath = tempfile();
 	await writeFile(filePath, 'foobar');
-	const {stdout} = await execa(fixtureName, getOptions([filePath, stringGenerator()]));
+	const {stdout} = await execa(fixtureName, getOptions([{file: filePath}, stringGenerator()]));
 	t.is(stdout, 'foobarfoobar');
 	await rm(filePath);
 };
diff --git a/test/stdio/file-path.js b/test/stdio/file-path.js
index f65737d..d6a3a82 100644
--- a/test/stdio/file-path.js
+++ b/test/stdio/file-path.js
@@ -14,7 +14,9 @@ const textEncoder = new TextEncoder();
 const binaryFoobar = textEncoder.encode('foobar');
 const nonFileUrl = new URL('https://example.com');
 
-const getRelativePath = filePath => relative('.', filePath);
+const getAbsolutePath = file => ({file});
+const getRelativePath = filePath => ({file: relative('.', filePath)});
+const getStdinFilePath = file => ({stdin: {file}});
 
 const testStdinFile = async (t, mapFilePath, getOptions, execaMethod) => {
 	const filePath = tempfile();
@@ -27,14 +29,14 @@ const testStdinFile = async (t, mapFilePath, getOptions, execaMethod) => {
 test('inputFile can be a file URL', testStdinFile, pathToFileURL, getInputFileOption, execa);
 test('stdin can be a file URL', testStdinFile, pathToFileURL, getStdinOption, execa);
 test('inputFile can be an absolute file path', testStdinFile, identity, getInputFileOption, execa);
-test('stdin can be an absolute file path', testStdinFile, identity, getStdinOption, execa);
-test('inputFile can be a relative file path', testStdinFile, getRelativePath, getInputFileOption, execa);
+test('stdin can be an absolute file path', testStdinFile, getAbsolutePath, getStdinOption, execa);
+test('inputFile can be a relative file path', testStdinFile, identity, getInputFileOption, execa);
 test('stdin can be a relative file path', testStdinFile, getRelativePath, getStdinOption, execa);
 test('inputFile can be a file URL - sync', testStdinFile, pathToFileURL, getInputFileOption, execaSync);
 test('stdin can be a file URL - sync', testStdinFile, pathToFileURL, getStdinOption, execaSync);
 test('inputFile can be an absolute file path - sync', testStdinFile, identity, getInputFileOption, execaSync);
-test('stdin can be an absolute file path - sync', testStdinFile, identity, getStdinOption, execaSync);
-test('inputFile can be a relative file path - sync', testStdinFile, getRelativePath, getInputFileOption, execaSync);
+test('stdin can be an absolute file path - sync', testStdinFile, getAbsolutePath, getStdinOption, execaSync);
+test('inputFile can be a relative file path - sync', testStdinFile, identity, getInputFileOption, execaSync);
 test('stdin can be a relative file path - sync', testStdinFile, getRelativePath, getStdinOption, execaSync);
 
 // eslint-disable-next-line max-params
@@ -48,18 +50,18 @@ const testOutputFile = async (t, mapFile, fixtureName, getOptions, execaMethod)
 test('stdout can be a file URL', testOutputFile, pathToFileURL, 'noop.js', getStdoutOption, execa);
 test('stderr can be a file URL', testOutputFile, pathToFileURL, 'noop-err.js', getStderrOption, execa);
 test('stdio[*] can be a file URL', testOutputFile, pathToFileURL, 'noop-fd3.js', getStdioOption, execa);
-test('stdout can be an absolute file path', testOutputFile, identity, 'noop.js', getStdoutOption, execa);
-test('stderr can be an absolute file path', testOutputFile, identity, 'noop-err.js', getStderrOption, execa);
-test('stdio[*] can be an absolute file path', testOutputFile, identity, 'noop-fd3.js', getStdioOption, execa);
+test('stdout can be an absolute file path', testOutputFile, getAbsolutePath, 'noop.js', getStdoutOption, execa);
+test('stderr can be an absolute file path', testOutputFile, getAbsolutePath, 'noop-err.js', getStderrOption, execa);
+test('stdio[*] can be an absolute file path', testOutputFile, getAbsolutePath, 'noop-fd3.js', getStdioOption, execa);
 test('stdout can be a relative file path', testOutputFile, getRelativePath, 'noop.js', getStdoutOption, execa);
 test('stderr can be a relative file path', testOutputFile, getRelativePath, 'noop-err.js', getStderrOption, execa);
 test('stdio[*] can be a relative file path', testOutputFile, getRelativePath, 'noop-fd3.js', getStdioOption, execa);
 test('stdout can be a file URL - sync', testOutputFile, pathToFileURL, 'noop.js', getStdoutOption, execaSync);
 test('stderr can be a file URL - sync', testOutputFile, pathToFileURL, 'noop-err.js', getStderrOption, execaSync);
 test('stdio[*] can be a file URL - sync', testOutputFile, pathToFileURL, 'noop-fd3.js', getStdioOption, execaSync);
-test('stdout can be an absolute file path - sync', testOutputFile, identity, 'noop.js', getStdoutOption, execaSync);
-test('stderr can be an absolute file path - sync', testOutputFile, identity, 'noop-err.js', getStderrOption, execaSync);
-test('stdio[*] can be an absolute file path - sync', testOutputFile, identity, 'noop-fd3.js', getStdioOption, execaSync);
+test('stdout can be an absolute file path - sync', testOutputFile, getAbsolutePath, 'noop.js', getStdoutOption, execaSync);
+test('stderr can be an absolute file path - sync', testOutputFile, getAbsolutePath, 'noop-err.js', getStderrOption, execaSync);
+test('stdio[*] can be an absolute file path - sync', testOutputFile, getAbsolutePath, 'noop-fd3.js', getStdioOption, execaSync);
 test('stdout can be a relative file path - sync', testOutputFile, getRelativePath, 'noop.js', getStdoutOption, execaSync);
 test('stderr can be a relative file path - sync', testOutputFile, getRelativePath, 'noop-err.js', getStderrOption, execaSync);
 test('stdio[*] can be a relative file path - sync', testOutputFile, getRelativePath, 'noop-fd3.js', getStdioOption, execaSync);
@@ -81,14 +83,23 @@ test('stdout cannot be a non-file URL - sync', testStdioNonFileUrl, getStdoutOpt
 test('stderr cannot be a non-file URL - sync', testStdioNonFileUrl, getStderrOption, execaSync);
 test('stdio[*] cannot be a non-file URL - sync', testStdioNonFileUrl, getStdioOption, execaSync);
 
-const testInputFileValidUrl = async (t, execaMethod) => {
+const testInvalidInputFile = (t, execaMethod) => {
+	t.throws(() => {
+		execaMethod('noop.js', getInputFileOption(false));
+	}, {message: /a file path string or a file URL/});
+};
+
+test('inputFile must be a file URL or string', testInvalidInputFile, execa);
+test('inputFile must be a file URL or string - sync', testInvalidInputFile, execaSync);
+
+const testInputFileValidUrl = async (t, getOptions, execaMethod) => {
 	const filePath = tempfile();
 	await writeFile(filePath, 'foobar');
 	const currentCwd = process.cwd();
 	process.chdir(dirname(filePath));
 
 	try {
-		const {stdout} = await execaMethod('stdin.js', {inputFile: basename(filePath)});
+		const {stdout} = await execaMethod('stdin.js', getOptions(basename(filePath)));
 		t.is(stdout, 'foobar');
 	} finally {
 		process.chdir(currentCwd);
@@ -96,23 +107,25 @@ const testInputFileValidUrl = async (t, execaMethod) => {
 	}
 };
 
-test.serial('inputFile does not need to start with . when being a relative file path', testInputFileValidUrl, execa);
-test.serial('inputFile does not need to start with . when being a relative file path - sync', testInputFileValidUrl, execaSync);
+test.serial('inputFile does not need to start with . when being a relative file path', testInputFileValidUrl, getInputFileOption, execa);
+test.serial('stdin does not need to start with . when being a relative file path', testInputFileValidUrl, getStdinFilePath, execa);
+test.serial('inputFile does not need to start with . when being a relative file path - sync', testInputFileValidUrl, getInputFileOption, execaSync);
+test.serial('stdin does not need to start with . when being a relative file path - sync', testInputFileValidUrl, getStdinFilePath, execaSync);
 
-const testStdioValidUrl = (t, getOptions, execaMethod) => {
+const testFilePathObject = (t, getOptions, execaMethod) => {
 	t.throws(() => {
 		execaMethod('noop.js', getOptions('foobar'));
-	}, {message: /absolute file path/});
+	}, {message: /must be used/});
 };
 
-test('stdin must start with . when being a relative file path', testStdioValidUrl, getStdinOption, execa);
-test('stdout must start with . when being a relative file path', testStdioValidUrl, getStdoutOption, execa);
-test('stderr must start with . when being a relative file path', testStdioValidUrl, getStderrOption, execa);
-test('stdio[*] must start with . when being a relative file path', testStdioValidUrl, getStdioOption, execa);
-test('stdin must start with . when being a relative file path - sync', testStdioValidUrl, getStdinOption, execaSync);
-test('stdout must start with . when being a relative file path - sync', testStdioValidUrl, getStdoutOption, execaSync);
-test('stderr must start with . when being a relative file path - sync', testStdioValidUrl, getStderrOption, execaSync);
-test('stdio[*] must start with . when being a relative file path - sync', testStdioValidUrl, getStdioOption, execaSync);
+test('stdin must be an object when it is a file path string', testFilePathObject, getStdinOption, execa);
+test('stdout must be an object when it is a file path string', testFilePathObject, getStdoutOption, execa);
+test('stderr must be an object when it is a file path string', testFilePathObject, getStderrOption, execa);
+test('stdio[*] must be an object when it is a file path string', testFilePathObject, getStdioOption, execa);
+test('stdin be an object when it is a file path string - sync', testFilePathObject, getStdinOption, execaSync);
+test('stdout be an object when it is a file path string - sync', testFilePathObject, getStdoutOption, execaSync);
+test('stderr be an object when it is a file path string - sync', testFilePathObject, getStderrOption, execaSync);
+test('stdio[*] must be an object when it is a file path string - sync', testFilePathObject, getStdioOption, execaSync);
 
 const testFileError = async (t, mapFile, getOptions) => {
 	await t.throwsAsync(
@@ -127,10 +140,10 @@ test('stdout file URL errors should be handled', testFileError, pathToFileURL, g
 test('stderr file URL errors should be handled', testFileError, pathToFileURL, getStderrOption);
 test('stdio[*] file URL errors should be handled', testFileError, pathToFileURL, getStdioOption);
 test('inputFile file path errors should be handled', testFileError, identity, getInputFileOption);
-test('stdin file path errors should be handled', testFileError, identity, getStdinOption);
-test('stdout file path errors should be handled', testFileError, identity, getStdoutOption);
-test('stderr file path errors should be handled', testFileError, identity, getStderrOption);
-test('stdio[*] file path errors should be handled', testFileError, identity, getStdioOption);
+test('stdin file path errors should be handled', testFileError, getAbsolutePath, getStdinOption);
+test('stdout file path errors should be handled', testFileError, getAbsolutePath, getStdoutOption);
+test('stderr file path errors should be handled', testFileError, getAbsolutePath, getStderrOption);
+test('stdio[*] file path errors should be handled', testFileError, getAbsolutePath, getStdioOption);
 
 const testFileErrorSync = (t, mapFile, getOptions) => {
 	t.throws(() => {
@@ -144,10 +157,10 @@ test('stdout file URL errors should be handled - sync', testFileErrorSync, pathT
 test('stderr file URL errors should be handled - sync', testFileErrorSync, pathToFileURL, getStderrOption);
 test('stdio[*] file URL errors should be handled - sync', testFileErrorSync, pathToFileURL, getStdioOption);
 test('inputFile file path errors should be handled - sync', testFileErrorSync, identity, getInputFileOption);
-test('stdin file path errors should be handled - sync', testFileErrorSync, identity, getStdinOption);
-test('stdout file path errors should be handled - sync', testFileErrorSync, identity, getStdoutOption);
-test('stderr file path errors should be handled - sync', testFileErrorSync, identity, getStderrOption);
-test('stdio[*] file path errors should be handled - sync', testFileErrorSync, identity, getStdioOption);
+test('stdin file path errors should be handled - sync', testFileErrorSync, getAbsolutePath, getStdinOption);
+test('stdout file path errors should be handled - sync', testFileErrorSync, getAbsolutePath, getStdoutOption);
+test('stderr file path errors should be handled - sync', testFileErrorSync, getAbsolutePath, getStderrOption);
+test('stdio[*] file path errors should be handled - sync', testFileErrorSync, getAbsolutePath, getStdioOption);
 
 const testInputFile = async (t, execaMethod) => {
 	const inputFile = tempfile();
@@ -184,16 +197,16 @@ const getStringInput = () => ({input: 'foobar'});
 const getBinaryInput = () => ({input: binaryFoobar});
 
 test('input String and inputFile can be both set', testMultipleInputs, [getInputFileOption, getStringInput], execa);
-test('input String and stdin can be both set', testMultipleInputs, [getStdinOption, getStringInput], execa);
+test('input String and stdin can be both set', testMultipleInputs, [getStdinFilePath, getStringInput], execa);
 test('input Uint8Array and inputFile can be both set', testMultipleInputs, [getInputFileOption, getBinaryInput], execa);
-test('input Uint8Array and stdin can be both set', testMultipleInputs, [getStdinOption, getBinaryInput], execa);
-test('stdin and inputFile can be both set', testMultipleInputs, [getStdinOption, getInputFileOption], execa);
-test('input String, stdin and inputFile can be all set', testMultipleInputs, [getInputFileOption, getStdinOption, getStringInput], execa);
-test('input Uint8Array, stdin and inputFile can be all set', testMultipleInputs, [getInputFileOption, getStdinOption, getBinaryInput], execa);
+test('input Uint8Array and stdin can be both set', testMultipleInputs, [getStdinFilePath, getBinaryInput], execa);
+test('stdin and inputFile can be both set', testMultipleInputs, [getStdinFilePath, getInputFileOption], execa);
+test('input String, stdin and inputFile can be all set', testMultipleInputs, [getInputFileOption, getStdinFilePath, getStringInput], execa);
+test('input Uint8Array, stdin and inputFile can be all set', testMultipleInputs, [getInputFileOption, getStdinFilePath, getBinaryInput], execa);
 test('input String and inputFile can be both set - sync', testMultipleInputs, [getInputFileOption, getStringInput], execaSync);
-test('input String and stdin can be both set - sync', testMultipleInputs, [getStdinOption, getStringInput], execaSync);
+test('input String and stdin can be both set - sync', testMultipleInputs, [getStdinFilePath, getStringInput], execaSync);
 test('input Uint8Array and inputFile can be both set - sync', testMultipleInputs, [getInputFileOption, getBinaryInput], execaSync);
-test('input Uint8Array and stdin can be both set - sync', testMultipleInputs, [getStdinOption, getBinaryInput], execaSync);
-test('stdin and inputFile can be both set - sync', testMultipleInputs, [getStdinOption, getInputFileOption], execaSync);
-test('input String, stdin and inputFile can be all set - sync', testMultipleInputs, [getInputFileOption, getStdinOption, getStringInput], execaSync);
-test('input Uint8Array, stdin and inputFile can be all set - sync', testMultipleInputs, [getInputFileOption, getStdinOption, getBinaryInput], execaSync);
+test('input Uint8Array and stdin can be both set - sync', testMultipleInputs, [getStdinFilePath, getBinaryInput], execaSync);
+test('stdin and inputFile can be both set - sync', testMultipleInputs, [getStdinFilePath, getInputFileOption], execaSync);
+test('input String, stdin and inputFile can be all set - sync', testMultipleInputs, [getInputFileOption, getStdinFilePath, getStringInput], execaSync);
+test('input Uint8Array, stdin and inputFile can be all set - sync', testMultipleInputs, [getInputFileOption, getStdinFilePath, getBinaryInput], execaSync);