Skip to content

Commit

Permalink
Support single string input without using the shell option (#182)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored and sindresorhus committed Apr 30, 2019
1 parent 9e5b976 commit 075faf3
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 3 deletions.
3 changes: 3 additions & 0 deletions fixtures/echo
@@ -0,0 +1,3 @@
#!/usr/bin/env node
'use strict';
console.log(process.argv.slice(2).join('\n'))
40 changes: 39 additions & 1 deletion index.js
Expand Up @@ -15,7 +15,45 @@ const stdio = require('./lib/stdio');

const TEN_MEGABYTES = 1000 * 1000 * 10;

function handleArgs(command, args, options) {
const SPACES_REGEXP = / +/g;

// Allow spaces to be escaped by a backslash if not meant as a delimiter
function handleEscaping(tokens, token, index) {
if (index === 0) {
return [token];
}

const previousToken = tokens[index - 1];

if (!previousToken.endsWith('\\')) {
return [...tokens, token];
}

return [...tokens.slice(0, index - 1), `${previousToken.slice(0, -1)} ${token}`];
}

function parseCommand(command, args = []) {
if (args.length !== 0) {
throw new Error('Arguments cannot be inside `command` when also specified as an array of strings');
}

const [file, ...extraArgs] = command
.trim()
.split(SPACES_REGEXP)
.reduce(handleEscaping, []);
return [file, extraArgs];
}

function handleArgs(command, args, options = {}) {
if (args && !Array.isArray(args)) {
options = args;
args = [];
}

if (!options.shell && command.includes(' ')) {
[command, args] = parseCommand(command, args);
}

const parsed = crossSpawn._parse(command, args, options);
command = parsed.command;
args = parsed.args;
Expand Down
17 changes: 15 additions & 2 deletions readme.md
Expand Up @@ -16,6 +16,7 @@
- [Executes locally installed binaries by name.](#preferlocal)
- [Cleans up spawned processes when the parent process dies.](#cleanup)
- [Adds an `.all` property](#execafile-arguments-options) with interleaved output from `stdout` and `stderr`, similar to what the terminal sees. [*(Async only)*](#execasyncfile-arguments-options)
- [Can specify command and arguments as a single string without a shell](#execafile-arguments-options)


## Install
Expand Down Expand Up @@ -115,11 +116,20 @@ try {
## API

### execa(file, [arguments], [options])
### execa(command, [options])

Execute a file.

Arguments can be specified in either:
- `arguments`: `execa('echo', ['unicorns'])`.
- `command`: `execa('echo unicorns')`.

Arguments should not be escaped nor quoted. Exception: inside `command`, spaces can be escaped with a backslash.

Think of this as a mix of `child_process.execFile` and `child_process.spawn`.

As opposed to [`execa.shell()`](#execashellcommand-options), no shell interpreter (Bash, `cmd.exe`, etc.) is used, so shell features such as variables substitution (`echo $PATH`) are not allowed.

Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which is enhanced to be a `Promise`.

It exposes an additional `.all` stream, with `stdout` and `stderr` interleaved.
Expand All @@ -129,22 +139,25 @@ The spawned process can be canceled with the `.cancel()` method on the promise,
The promise result is an `Object` with `stdout`, `stderr` and `all` properties.

### execa.stdout(file, [arguments], [options])
### execa.stdout(command, [options])

Same as `execa()`, but returns only `stdout`.

### execa.stderr(file, [arguments], [options])
### execa.stderr(command, [options])

Same as `execa()`, but returns only `stderr`.

### execa.shell(command, [options])

Execute a command through the system shell. Prefer `execa()` whenever possible, as it's both faster and safer.
Execute a command through the system shell. Prefer `execa()` whenever possible, as it's faster, safer and more cross-platform.

Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess).

The `child_process` instance is enhanced to also be promise for a result object with `stdout` and `stderr` properties.

### execa.sync(file, [arguments], [options])
### execa.sync(command, [options])

Execute a file synchronously.

Expand All @@ -154,7 +167,7 @@ It does not have the `.all` property that `execa()` has because the [underlying

This method throws an `Error` if the command fails.

### execa.shellSync(file, [options])
### execa.shellSync(command, [options])

Execute a command synchronously through the system shell.

Expand Down
34 changes: 34 additions & 0 deletions test.js
Expand Up @@ -101,6 +101,40 @@ test('pass `stderr` to a file descriptor', async t => {
t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n');
});

test('allow string arguments', async t => {
const {stdout} = await execa('node fixtures/echo foo bar');
t.is(stdout, 'foo\nbar');
});

test('allow string arguments in synchronous mode', t => {
const {stdout} = execa.sync('node fixtures/echo foo bar');
t.is(stdout, 'foo\nbar');
});

test('forbid string arguments together with array arguments', t => {
t.throws(() => execa('node fixtures/echo foo bar', ['foo', 'bar']), /Arguments cannot be inside/);
});

test('ignore consecutive spaces in string arguments', async t => {
const {stdout} = await execa('node fixtures/echo foo bar');
t.is(stdout, 'foo\nbar');
});

test('escape other whitespaces in string arguments', async t => {
const {stdout} = await execa('node fixtures/echo foo\tbar');
t.is(stdout, 'foo\tbar');
});

test('allow escaping spaces in string arguments', async t => {
const {stdout} = await execa('node fixtures/echo foo\\ bar');
t.is(stdout, 'foo bar');
});

test('trim string arguments', async t => {
const {stdout} = await execa(' node fixtures/echo foo bar ');
t.is(stdout, 'foo\nbar');
});

test('execa.shell()', async t => {
const {stdout} = await execa.shell('node fixtures/noop foo');
t.is(stdout, 'foo');
Expand Down

0 comments on commit 075faf3

Please sign in to comment.