Skip to content

Commit

Permalink
Add inputFile option (#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Mar 9, 2023
1 parent 5320fef commit 0726126
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 14 deletions.
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.
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.
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

0 comments on commit 0726126

Please sign in to comment.