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

Support single string input without using the shell option #182

Merged
merged 32 commits into from Apr 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2951747
Allow execa() to specify argument in same string as the command
ehmicky Mar 1, 2019
95f98a8
Improve style
ehmicky Mar 1, 2019
3803798
Add tests
ehmicky Mar 1, 2019
8128f68
Add documentation
ehmicky Mar 1, 2019
8fabb37
Fix linting
ehmicky Mar 1, 2019
dcd393b
Rename variable
ehmicky Mar 1, 2019
dfc533a
Small README change
ehmicky Mar 1, 2019
bc7a20b
Fix Windows support
ehmicky Mar 6, 2019
08ccc66
Fix rebasing
ehmicky Mar 6, 2019
20f4f81
Fix rebasing
ehmicky Mar 7, 2019
80fba79
Refactoring
ehmicky Mar 7, 2019
d0feb17
Add test
ehmicky Mar 7, 2019
74c9583
Improve tests
ehmicky Mar 7, 2019
a9d5d99
No hard wrapping
ehmicky Mar 19, 2019
8301d1e
Fix README
ehmicky Mar 19, 2019
cea9337
Improve README
ehmicky Mar 19, 2019
b094363
Improve coding style with tests
ehmicky Mar 19, 2019
333a074
Add test
ehmicky Mar 19, 2019
4a9f642
Fix rebasing error
ehmicky Apr 21, 2019
857bae7
Move lines
ehmicky Apr 21, 2019
455bb55
Change function arrow style
ehmicky Apr 21, 2019
d4ea67d
Remove duplicated example
ehmicky Apr 21, 2019
56262a4
Fix entry point documentation
ehmicky Apr 21, 2019
4fc80de
Improve documentation
ehmicky Apr 21, 2019
e61912e
Improve documentation
ehmicky Apr 21, 2019
d69312c
Do not allow arguments to be passed both inside command and as additi…
ehmicky Apr 21, 2019
8f9e5b3
Move lines
ehmicky Apr 21, 2019
44617b4
Fix whitespaces
ehmicky Apr 21, 2019
7b26251
Fix grammar
ehmicky Apr 21, 2019
0dc7e4a
Fix grammar
ehmicky Apr 21, 2019
97c91ea
Add parenthesis
ehmicky Apr 21, 2019
90926b6
Fix function style
ehmicky Apr 22, 2019
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
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