Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inputFile option #542

Merged
merged 4 commits into from Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/scripts.md
Expand Up @@ -635,7 +635,7 @@ await cat

```js
// Execa
await $({input: fs.createReadStream('file.txt')})`cat`
await $({inputFile: 'file.txt'})`cat`
```

### Silent stderr
Expand Down
18 changes: 18 additions & 0 deletions index.d.ts
Expand Up @@ -258,15 +258,33 @@ export type CommonOptions<EncodingType> = {
export type Options<EncodingType = string> = {
/**
Write some input to the `stdin` of your binary.
ehmicky marked this conversation as resolved.
Show resolved Hide resolved

If the input is a file, use the `inputFile` option instead.
*/
readonly input?: string | Buffer | ReadableStream;

/**
Use a file as input to the the `stdin` of your binary.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should mention input somehow. Maybe using a @see annotation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a Tip like the input option. Would that work?


If the input is not a file, use the `input` option instead.
*/
readonly inputFile?: string;
} & CommonOptions<EncodingType>;

export type SyncOptions<EncodingType = string> = {
/**
Write some input to the `stdin` of your binary.

If the input is a file, use the `inputFile` option instead.
*/
readonly input?: string | Buffer;

/**
Use a file as input to the the `stdin` of your binary.

If the input is not a file, use the `input` option instead.
*/
readonly inputFile?: string;
} & CommonOptions<EncodingType>;

export type NodeOptions<EncodingType = string> = {
Expand Down
8 changes: 4 additions & 4 deletions index.js
Expand Up @@ -10,7 +10,7 @@ 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 {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js';
import {mergePromise, getSpawnedPromise} from './lib/promise.js';
import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js';
import {logCommand, verboseDefault} from './lib/verbose.js';
Expand Down Expand Up @@ -159,7 +159,7 @@ export function execa(file, args, options) {

const handlePromiseOnce = onetime(handlePromise);

handleInput(spawned, parsed.options.input);
handleInput(spawned, parsed.options);

spawned.all = makeAllStream(spawned, parsed.options);

Expand All @@ -174,11 +174,11 @@ export function execaSync(file, args, options) {
const escapedCommand = getEscapedCommand(file, args);
logCommand(escapedCommand, parsed.options);

validateInputSync(parsed.options);
const input = handleInputSync(parsed.options);

let result;
try {
result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options);
result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input});
} catch (error) {
throw makeError({
error,
Expand Down
1 change: 1 addition & 0 deletions index.test-d.ts
Expand Up @@ -134,6 +134,7 @@ execa('unicorns', {buffer: false});
execa('unicorns', {input: ''});
execa('unicorns', {input: Buffer.from('')});
execa('unicorns', {input: process.stdin});
execa('unicorns', {inputFile: ''});
execa('unicorns', {stdin: 'pipe'});
execa('unicorns', {stdin: 'overlapped'});
execa('unicorns', {stdin: 'ipc'});
Expand Down
48 changes: 40 additions & 8 deletions lib/stream.js
@@ -1,9 +1,47 @@
import {createReadStream, readFileSync} from 'node:fs';
import {isStream} from 'is-stream';
import getStream from 'get-stream';
import mergeStream from 'merge-stream';

// `input` option
export const handleInput = (spawned, input) => {
const validateInputOptions = input => {
if (input !== undefined) {
throw new TypeError('The `input` and `inputFile` options cannot be both set.');
}
};

const getInputSync = ({input, inputFile}) => {
if (typeof inputFile !== 'string') {
return input;
}

validateInputOptions(input);
return readFileSync(inputFile);
};

// `input` and `inputFile` option in sync mode
export const handleInputSync = options => {
const input = getInputSync(options);

if (isStream(input)) {
throw new TypeError('The `input` option cannot be a stream in sync mode');
}

return input;
};

const getInput = ({input, inputFile}) => {
if (typeof inputFile !== 'string') {
return input;
}

validateInputOptions(input);
return createReadStream(inputFile);
};

// `input` and `inputFile` option in async mode
export const handleInput = (spawned, options) => {
const input = getInput(options);

if (input === undefined) {
return;
}
Expand Down Expand Up @@ -79,9 +117,3 @@ export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer,
]);
}
};

export const validateInputSync = ({input}) => {
if (isStream(input)) {
throw new TypeError('The `input` option cannot be a stream in sync mode');
}
};
12 changes: 11 additions & 1 deletion readme.md
Expand Up @@ -128,7 +128,7 @@ await execa('echo', ['unicorns'], {all:true}).pipeAll('all.txt');
import {execa} from 'execa';

// Similar to `cat < stdin.txt` in Bash
const {stdout} = await execa('cat', {input:fs.createReadStream('stdin.txt')});
const {stdout} = await execa('cat', {inputFile:'stdin.txt'});
console.log(stdout);
//=> 'unicorns'
```
Expand Down Expand Up @@ -497,6 +497,16 @@ Type: `string | Buffer | stream.Readable`
Write some input to the `stdin` of your binary.\
Streams are not allowed when using the synchronous methods.

If the input is a file, use the [`inputFile` option](#inputfile) instead.

#### inputFile

Type: `string`

Use a file as input to the the `stdin` of your binary.

If the input is not a file, use the [`input` option](#input) instead.

#### stdin

Type: `string | number | Stream | undefined`\
Expand Down
26 changes: 26 additions & 0 deletions test/stream.js
Expand Up @@ -71,6 +71,19 @@ test('input can be a Stream', async t => {
t.is(stdout, 'howdy');
});

test('inputFile can be set', async t => {
const inputFile = tempfile();
fs.writeFileSync(inputFile, 'howdy');
const {stdout} = await execa('stdin.js', {inputFile});
t.is(stdout, 'howdy');
});

test('inputFile and input cannot be both set', t => {
t.throws(() => execa('stdin.js', {inputFile: '', input: ''}), {
message: /cannot be both set/,
});
});

test('you can write to child.stdin', async t => {
const subprocess = execa('stdin.js');
subprocess.stdin.end('unicorns');
Expand Down Expand Up @@ -105,6 +118,19 @@ test('helpful error trying to provide an input stream in sync mode', t => {
);
});

test('inputFile can be set - sync', t => {
const inputFile = tempfile();
fs.writeFileSync(inputFile, 'howdy');
const {stdout} = execaSync('stdin.js', {inputFile});
t.is(stdout, 'howdy');
});

test('inputFile and input cannot be both set - sync', t => {
t.throws(() => execaSync('stdin.js', {inputFile: '', input: ''}), {
message: /cannot be both set/,
});
});

test('maxBuffer affects stdout', async t => {
await t.notThrowsAsync(execa('max-buffer.js', ['stdout', '10'], {maxBuffer: 10}));
const {stdout, all} = await t.throwsAsync(execa('max-buffer.js', ['stdout', '11'], {maxBuffer: 10, all: true}), {message: /max-buffer.js stdout/});
Expand Down