Skip to content

Commit

Permalink
Add execa.command() and execa.command.sync()
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed May 22, 2019
1 parent b5c8ac2 commit 0049816
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 76 deletions.
62 changes: 32 additions & 30 deletions index.js
Expand Up @@ -17,37 +17,7 @@ const TEN_MEGABYTES = 1000 * 1000 * 10;

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) {
return command
.trim()
.split(SPACES_REGEXP)
.reduce(handleEscaping, []);
}

function handleArgs(command, args, options = {}) {
if (args && !Array.isArray(args) && typeof args === 'object') {
options = args;
}

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

const parsed = crossSpawn._parse(command, args, options);
command = parsed.command;
args = parsed.args;
Expand Down Expand Up @@ -455,3 +425,35 @@ module.exports.sync = (command, args, options) => {
killed: false
};
};

// 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) {
return command
.trim()
.split(SPACES_REGEXP)
.reduce(handleEscaping, []);
}

module.exports.command = (command, options) => {
const [file, ...args] = parseCommand(command);
return execa(file, args, {...options, shell: false});
};

module.exports.command.sync = (command, options) => {
const [file, ...args] = parseCommand(command);
return execa.sync(file, args, {...options, shell: false});
};
24 changes: 17 additions & 7 deletions readme.md
Expand Up @@ -55,7 +55,7 @@ const execa = require('execa');

// Catching an error
try {
await execa('wrong command');
await execa('wrong', ['command']);
} catch (error) {
console.log(error);
/*
Expand Down Expand Up @@ -94,7 +94,7 @@ const execa = require('execa');

// Catching an error with a sync method
try {
execa.sync('wrong command');
execa.sync('wrong', ['command']);
} catch (error) {
console.log(error);
/*
Expand Down Expand Up @@ -122,13 +122,10 @@ try {
## API

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

Execute a file. Think of this as a mix of [`child_process.execFile()`](https://nodejs.org/api/child_process.html#child_process_child_process_execfile_file_args_options_callback) and [`child_process.spawn()`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options).

Arguments can be specified in either:
- `arguments`: `execa('echo', ['unicorns'])`. No escaping/quoting is needed.
- `command`: `execa('echo unicorns')`. No escaping/quoting is needed, except significant spaces which must be escaped with a backslash.
No escaping/quoting is needed.

Unless the [`shell`](#shell) option is used, no shell interpreter (Bash, `cmd.exe`, etc.) is used, so shell features such as variables substitution (`echo $PATH`) are not allowed.

Expand All @@ -145,12 +142,25 @@ Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#chi
Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr).

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

Execute a file synchronously.

Returns or throws a [`childProcessResult`](#childProcessResult).

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

Same as [`execa()`](https://github.com/sindresorhus/execa#execafile-arguments-options) except both file and arguments are specified in a single `command` string. For example, `execa('echo', ['unicorns'])` is the same as `execa('echo unicorns')`.

If the file or an argument contains spaces, they must be escaped with a backslash. Otherwise, no escaping/quoting is needed.

The [`shell`](#shell) option cannot be used.

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

Same as [`execa.command()`](https://github.com/sindresorhus/execa#execacommand-command-options) but synchronously.

Returns or throws a [`childProcessResult`](#childProcessResult).

### childProcessResult

Type: `object`
Expand Down
79 changes: 40 additions & 39 deletions test.js
Expand Up @@ -97,45 +97,6 @@ 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('allow commands with spaces and array arguments', async t => {
const {stdout} = await execa('./fixtures/command with space', ['foo', 'bar']);
t.is(stdout, 'foo\nbar');
});

test('forbid commands with spaces and no array arguments', async t => {
await t.throwsAsync(execa('./fixtures/command with space'));
});

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.sync()', t => {
const {stdout} = execa.sync('noop', ['foo']);
t.is(stdout, 'foo');
Expand Down Expand Up @@ -709,3 +670,43 @@ test('calling cancel method on a process which has been killed does not make err
const {isCanceled} = await t.throwsAsync(subprocess);
t.false(isCanceled);
});

test('allow commands with spaces and no array arguments', async t => {
const {stdout} = await execa('./fixtures/command with space');
t.is(stdout, '');
});

test('allow commands with spaces and array arguments', async t => {
const {stdout} = await execa('./fixtures/command with space', ['foo', 'bar']);
t.is(stdout, 'foo\nbar');
});

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

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

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

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

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

test('execa.command.sync()', t => {
const {stdout} = execa.command.sync('node fixtures/echo foo bar');
t.is(stdout, 'foo\nbar');
});

0 comments on commit 0049816

Please sign in to comment.