From 29517471b9e69a5388e4dbbec758bc930aee4912 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 11:19:17 +0100 Subject: [PATCH 01/32] Allow execa() to specify argument in same string as the command --- index.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/index.js b/index.js index 9899d2c48..2a9cfe203 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,10 @@ const stdio = require('./lib/stdio'); const TEN_MEGABYTES = 1000 * 1000 * 10; function handleArgs(command, args, options) { + if (!options.shell && command.includes(' ')) { + [command, args] = parseCommand(command, args); + } + const parsed = crossSpawn._parse(command, args, options); command = parsed.command; args = parsed.args; @@ -68,6 +72,32 @@ function handleArgs(command, args, options) { return {command, args, options, parsed}; } +function parseCommand(command, args = []) { + const [newCommand, ...extraArgs] = command + .trim() + .split(SPACES_REGEXP) + .reduce(handleEscaping, []); + const newArgs = [...extraArgs, ...args]; + return [newCommand, newArgs]; +} + +const SPACES_REGEXP = / +/g; + +// Allow spaces to be escaped by a backslash if not meant as a delimiter +const handleEscaping = function (tokens, token, index) { + if (index === 0) { + return [token]; + } + + const previousToken = tokens[index - 1]; + + if (!previousToken.endsWith('\\')) { + return tokens.concat(token); + } + + return tokens.slice(0, index - 1).concat(`${previousToken.slice(0, -1)} ${token}`); +}; + function handleInput(spawned, input) { if (input === undefined) { return; From 95f98a83e9d5b334c7781a6cf039720917f438a6 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 11:29:48 +0100 Subject: [PATCH 02/32] Improve style --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 2a9cfe203..cbd400e0a 100644 --- a/index.js +++ b/index.js @@ -92,10 +92,10 @@ const handleEscaping = function (tokens, token, index) { const previousToken = tokens[index - 1]; if (!previousToken.endsWith('\\')) { - return tokens.concat(token); + return [...tokens, token]; } - return tokens.slice(0, index - 1).concat(`${previousToken.slice(0, -1)} ${token}`); + return [...tokens.slice(0, index - 1), `${previousToken.slice(0, -1)} ${token}`]; }; function handleInput(spawned, input) { From 3803798a18e489e6b14705cd6fe72825957d32b5 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 11:57:54 +0100 Subject: [PATCH 03/32] Add tests --- fixtures/echo | 3 +++ test.js | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100755 fixtures/echo diff --git a/fixtures/echo b/fixtures/echo new file mode 100755 index 000000000..cdfa972df --- /dev/null +++ b/fixtures/echo @@ -0,0 +1,3 @@ +#!/usr/bin/env node +'use strict'; +console.log(process.argv.slice(2).join('\n')) diff --git a/test.js b/test.js index 07a3b0180..32eb9b9eb 100644 --- a/test.js +++ b/test.js @@ -101,6 +101,31 @@ 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 m('fixtures/echo foo bar'); + t.is(stdout, 'foo\nbar') +}); + +test('allow string arguments together with array arguments', async t => { + const {stdout} = await m('fixtures/echo foo bar', ['foo', 'bar']); + t.is(stdout, 'foo\nbar\nfoo\nbar') +}); + +test('ignore consecutive spaces in string arguments', async t => { + const {stdout} = await m('fixtures/echo foo bar'); + t.is(stdout, 'foo\nbar') +}); + +test('escape other whitespaces in string arguments', async t => { + const {stdout} = await m('fixtures/echo foo\tbar'); + t.is(stdout, 'foo\tbar') +}); + +test('allow escaping spaces in string arguments', async t => { + const {stdout} = await m('fixtures/echo foo\\ bar'); + t.is(stdout, 'foo bar') +}); + test('execa.shell()', async t => { const {stdout} = await execa.shell('node fixtures/noop foo'); t.is(stdout, 'foo'); From 8128f6889e5d375600065d874a4f45757e3a51ec Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 11:58:07 +0100 Subject: [PATCH 04/32] Add documentation --- readme.md | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 3dccd8581..0e3b71cd0 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -51,10 +52,25 @@ const execa = require('execa'); execa('echo', ['unicorns']).stdout.pipe(process.stdout); - // Run a shell command + // Run a shell command as a string const {stdout} = await execa.shell('echo unicorns'); //=> 'unicorns' + // Cancelling a spawned process + const subprocess = execa('node'); + setTimeout(() => { spawned.cancel() }, 1000); + try { + await subprocess; + } catch (error) { + console.log(subprocess.killed); // true + console.log(error.isCanceled); // true + } + + // Run a command as a string without a shell + const {stdout} = await execa('echo unicorns'); + //=> 'unicorns' + + // Catching an error try { await execa.shell('exit 3'); @@ -114,10 +130,14 @@ try { ## API -### execa(file, [arguments], [options]) +### execa(command, [arguments], [options]) Execute a file. +Arguments can be specified either inside `command` (a string) or `arguments` +(an array of strings). When specified inside `command`, spaces can be escaped +with a backslash. Otherwise arguments need neither escaping nor quoting. + Think of this as a mix of `child_process.execFile` and `child_process.spawn`. Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which is enhanced to be a `Promise`. @@ -128,23 +148,24 @@ 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, [arguments], [options]) Same as `execa()`, but returns only `stdout`. -### execa.stderr(file, [arguments], [options]) +### execa.stderr(command, [arguments], [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, [arguments], [options]) Execute a file synchronously. @@ -154,7 +175,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. From 8fabb370465c81a27ff43ed657e3ab76759d0120 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 12:10:53 +0100 Subject: [PATCH 05/32] Fix linting --- test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test.js b/test.js index 32eb9b9eb..d79e1553c 100644 --- a/test.js +++ b/test.js @@ -103,27 +103,27 @@ test('pass `stderr` to a file descriptor', async t => { test('allow string arguments', async t => { const {stdout} = await m('fixtures/echo foo bar'); - t.is(stdout, 'foo\nbar') + t.is(stdout, 'foo\nbar'); }); test('allow string arguments together with array arguments', async t => { const {stdout} = await m('fixtures/echo foo bar', ['foo', 'bar']); - t.is(stdout, 'foo\nbar\nfoo\nbar') + t.is(stdout, 'foo\nbar\nfoo\nbar'); }); test('ignore consecutive spaces in string arguments', async t => { const {stdout} = await m('fixtures/echo foo bar'); - t.is(stdout, 'foo\nbar') + t.is(stdout, 'foo\nbar'); }); test('escape other whitespaces in string arguments', async t => { const {stdout} = await m('fixtures/echo foo\tbar'); - t.is(stdout, 'foo\tbar') + t.is(stdout, 'foo\tbar'); }); test('allow escaping spaces in string arguments', async t => { const {stdout} = await m('fixtures/echo foo\\ bar'); - t.is(stdout, 'foo bar') + t.is(stdout, 'foo bar'); }); test('execa.shell()', async t => { From dcd393b0b660ce5f97543fba9ae4ed3a2fa6d0be Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 12:12:25 +0100 Subject: [PATCH 06/32] Rename variable --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index cbd400e0a..080051121 100644 --- a/index.js +++ b/index.js @@ -73,12 +73,12 @@ function handleArgs(command, args, options) { } function parseCommand(command, args = []) { - const [newCommand, ...extraArgs] = command + const [file, ...extraArgs] = command .trim() .split(SPACES_REGEXP) .reduce(handleEscaping, []); const newArgs = [...extraArgs, ...args]; - return [newCommand, newArgs]; + return [file, newArgs]; } const SPACES_REGEXP = / +/g; From dfc533a6eed50b2e968dae3dcead5be1623c9d03 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 1 Mar 2019 12:13:55 +0100 Subject: [PATCH 07/32] Small README change --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0e3b71cd0..3aadc3bde 100644 --- a/readme.md +++ b/readme.md @@ -52,7 +52,7 @@ const execa = require('execa'); execa('echo', ['unicorns']).stdout.pipe(process.stdout); - // Run a shell command as a string + // Run a shell command const {stdout} = await execa.shell('echo unicorns'); //=> 'unicorns' From bc7a20b0b161f1ec12053173193485b3f1a5ad15 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Wed, 6 Mar 2019 13:03:28 +0100 Subject: [PATCH 08/32] Fix Windows support --- index.js | 13 ++++++++++--- test.js | 10 +++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 080051121..2f9eee397 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,13 @@ const stdio = require('./lib/stdio'); const TEN_MEGABYTES = 1000 * 1000 * 10; function handleArgs(command, args, options) { + if (args && !Array.isArray(args)) { + options = args; + args = null; + } + + options = options || {}; + if (!options.shell && command.includes(' ')) { [command, args] = parseCommand(command, args); } @@ -30,7 +37,7 @@ function handleArgs(command, args, options) { buffer: true, stripFinalNewline: true, preferLocal: true, - localDir: options.cwd || process.cwd(), + localDir: parsed.options.cwd || process.cwd(), encoding: 'utf8', reject: true, cleanup: true, @@ -64,9 +71,9 @@ function handleArgs(command, args, options) { options.cleanup = false; } - if (process.platform === 'win32' && path.basename(command, '.exe') === 'cmd') { + if (process.platform === 'win32' && path.basename(parsed.command, 'exe') === 'cmd') { // #116 - args.unshift('/q'); + parsed.args.unshift('/q'); } return {command, args, options, parsed}; diff --git a/test.js b/test.js index d79e1553c..40b293e5b 100644 --- a/test.js +++ b/test.js @@ -102,27 +102,27 @@ test('pass `stderr` to a file descriptor', async t => { }); test('allow string arguments', async t => { - const {stdout} = await m('fixtures/echo foo bar'); + const {stdout} = await m('node fixtures/echo foo bar'); t.is(stdout, 'foo\nbar'); }); test('allow string arguments together with array arguments', async t => { - const {stdout} = await m('fixtures/echo foo bar', ['foo', 'bar']); + const {stdout} = await m('node fixtures/echo foo bar', ['foo', 'bar']); t.is(stdout, 'foo\nbar\nfoo\nbar'); }); test('ignore consecutive spaces in string arguments', async t => { - const {stdout} = await m('fixtures/echo foo bar'); + const {stdout} = await m('node fixtures/echo foo bar'); t.is(stdout, 'foo\nbar'); }); test('escape other whitespaces in string arguments', async t => { - const {stdout} = await m('fixtures/echo foo\tbar'); + const {stdout} = await m('node fixtures/echo foo\tbar'); t.is(stdout, 'foo\tbar'); }); test('allow escaping spaces in string arguments', async t => { - const {stdout} = await m('fixtures/echo foo\\ bar'); + const {stdout} = await m('node fixtures/echo foo\\ bar'); t.is(stdout, 'foo bar'); }); From 08ccc665b0951ad8ea704b902775139d6637933d Mon Sep 17 00:00:00 2001 From: ehmicky Date: Wed, 6 Mar 2019 18:27:09 +0100 Subject: [PATCH 09/32] Fix rebasing --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 2f9eee397..026d41053 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,7 @@ function handleArgs(command, args, options) { buffer: true, stripFinalNewline: true, preferLocal: true, - localDir: parsed.options.cwd || process.cwd(), + localDir: options.cwd || process.cwd(), encoding: 'utf8', reject: true, cleanup: true, @@ -71,9 +71,9 @@ function handleArgs(command, args, options) { options.cleanup = false; } - if (process.platform === 'win32' && path.basename(parsed.command, 'exe') === 'cmd') { + if (process.platform === 'win32' && path.basename(command, 'exe') === 'cmd') { // #116 - parsed.args.unshift('/q'); + args.unshift('/q'); } return {command, args, options, parsed}; From 20f4f81a3a98961b2cf6660400f78a3cab1a8b77 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Thu, 7 Mar 2019 11:19:00 +0100 Subject: [PATCH 10/32] Fix rebasing --- test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test.js b/test.js index 40b293e5b..13074cc16 100644 --- a/test.js +++ b/test.js @@ -102,27 +102,27 @@ test('pass `stderr` to a file descriptor', async t => { }); test('allow string arguments', async t => { - const {stdout} = await m('node fixtures/echo foo bar'); + const {stdout} = await execa('node fixtures/echo foo bar'); t.is(stdout, 'foo\nbar'); }); test('allow string arguments together with array arguments', async t => { - const {stdout} = await m('node fixtures/echo foo bar', ['foo', 'bar']); + const {stdout} = await execa('node fixtures/echo foo bar', ['foo', 'bar']); t.is(stdout, 'foo\nbar\nfoo\nbar'); }); test('ignore consecutive spaces in string arguments', async t => { - const {stdout} = await m('node fixtures/echo foo bar'); + 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 m('node fixtures/echo foo\tbar'); + 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 m('node fixtures/echo foo\\ bar'); + const {stdout} = await execa('node fixtures/echo foo\\ bar'); t.is(stdout, 'foo bar'); }); From 80fba793816468c24de4d635e6520f82ee6e2a77 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Thu, 7 Mar 2019 11:19:37 +0100 Subject: [PATCH 11/32] Refactoring --- index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 026d41053..c537ade6a 100644 --- a/index.js +++ b/index.js @@ -15,14 +15,12 @@ const stdio = require('./lib/stdio'); const TEN_MEGABYTES = 1000 * 1000 * 10; -function handleArgs(command, args, options) { +function handleArgs(command, args, options = {}) { if (args && !Array.isArray(args)) { options = args; - args = null; + args = []; } - options = options || {}; - if (!options.shell && command.includes(' ')) { [command, args] = parseCommand(command, args); } From d0feb17def6d72a21ffca97144c07be0e0c9644e Mon Sep 17 00:00:00 2001 From: ehmicky Date: Thu, 7 Mar 2019 11:20:59 +0100 Subject: [PATCH 12/32] Add test --- test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test.js b/test.js index 13074cc16..cc61dafec 100644 --- a/test.js +++ b/test.js @@ -126,6 +126,11 @@ test('allow escaping spaces in string arguments', async t => { 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'); From 74c95839d60f82ac375c50a6e749cb2f09d10431 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Thu, 7 Mar 2019 11:22:00 +0100 Subject: [PATCH 13/32] Improve tests --- test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test.js b/test.js index cc61dafec..c34f95ce1 100644 --- a/test.js +++ b/test.js @@ -102,32 +102,32 @@ test('pass `stderr` to a file descriptor', async t => { }); test('allow string arguments', async t => { - const {stdout} = await execa('node fixtures/echo foo bar'); + const stdout = await execa.stdout('node fixtures/echo foo bar'); t.is(stdout, 'foo\nbar'); }); test('allow string arguments together with array arguments', async t => { - const {stdout} = await execa('node fixtures/echo foo bar', ['foo', 'bar']); + const stdout = await execa.stdout('node fixtures/echo foo bar', ['foo', 'bar']); t.is(stdout, 'foo\nbar\nfoo\nbar'); }); test('ignore consecutive spaces in string arguments', async t => { - const {stdout} = await execa('node fixtures/echo foo bar'); + const stdout = await execa.stdout('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'); + const stdout = await execa.stdout('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'); + const stdout = await execa.stdout('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 '); + const stdout = await execa.stdout(' node fixtures/echo foo bar '); t.is(stdout, 'foo\nbar'); }); From a9d5d99879df49c2e093c34248b20fb541caad86 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 19 Mar 2019 15:43:17 +0100 Subject: [PATCH 14/32] No hard wrapping --- readme.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 3aadc3bde..de0d18af5 100644 --- a/readme.md +++ b/readme.md @@ -134,9 +134,7 @@ try { Execute a file. -Arguments can be specified either inside `command` (a string) or `arguments` -(an array of strings). When specified inside `command`, spaces can be escaped -with a backslash. Otherwise arguments need neither escaping nor quoting. +Arguments can be specified either inside `command` (a string) or `arguments` (an array of strings). When specified inside `command`, spaces can be escaped with a backslash. Otherwise arguments need neither escaping nor quoting. Think of this as a mix of `child_process.execFile` and `child_process.spawn`. @@ -158,8 +156,7 @@ 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 faster, safer and more cross-platform. +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). From 8301d1e76229af96916fe424923b913f97cb7b24 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 19 Mar 2019 15:44:37 +0100 Subject: [PATCH 15/32] Fix README --- readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index de0d18af5..7fb3d887b 100644 --- a/readme.md +++ b/readme.md @@ -130,9 +130,9 @@ try { ## API -### execa(command, [arguments], [options]) +### execa(file | command, [arguments], [options]) -Execute a file. +Execute a `file`. Arguments can be specified either inside `command` (a string) or `arguments` (an array of strings). When specified inside `command`, spaces can be escaped with a backslash. Otherwise arguments need neither escaping nor quoting. @@ -146,11 +146,11 @@ 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(command, [arguments], [options]) +### execa.stdout(file | command, [arguments], [options]) Same as `execa()`, but returns only `stdout`. -### execa.stderr(command, [arguments], [options]) +### execa.stderr(file | command, [arguments], [options]) Same as `execa()`, but returns only `stderr`. @@ -162,7 +162,7 @@ Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#c The `child_process` instance is enhanced to also be promise for a result object with `stdout` and `stderr` properties. -### execa.sync(command, [arguments], [options]) +### execa.sync(file | command, [arguments], [options]) Execute a file synchronously. From cea933739bfea439a0762ee5f1bc490250308c86 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 19 Mar 2019 15:45:35 +0100 Subject: [PATCH 16/32] Improve README --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 7fb3d887b..0b682b332 100644 --- a/readme.md +++ b/readme.md @@ -132,7 +132,7 @@ try { ### execa(file | command, [arguments], [options]) -Execute a `file`. +Execute a file. Arguments can be specified either inside `command` (a string) or `arguments` (an array of strings). When specified inside `command`, spaces can be escaped with a backslash. Otherwise arguments need neither escaping nor quoting. From b0943632710d06b384de939f17ea1f0d97116a68 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 19 Mar 2019 15:47:49 +0100 Subject: [PATCH 17/32] Improve coding style with tests --- test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test.js b/test.js index c34f95ce1..cc61dafec 100644 --- a/test.js +++ b/test.js @@ -102,32 +102,32 @@ test('pass `stderr` to a file descriptor', async t => { }); test('allow string arguments', async t => { - const stdout = await execa.stdout('node fixtures/echo foo bar'); + const {stdout} = await execa('node fixtures/echo foo bar'); t.is(stdout, 'foo\nbar'); }); test('allow string arguments together with array arguments', async t => { - const stdout = await execa.stdout('node fixtures/echo foo bar', ['foo', 'bar']); + const {stdout} = await execa('node fixtures/echo foo bar', ['foo', 'bar']); t.is(stdout, 'foo\nbar\nfoo\nbar'); }); test('ignore consecutive spaces in string arguments', async t => { - const stdout = await execa.stdout('node fixtures/echo foo bar'); + 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.stdout('node fixtures/echo foo\tbar'); + 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.stdout('node fixtures/echo foo\\ bar'); + const {stdout} = await execa('node fixtures/echo foo\\ bar'); t.is(stdout, 'foo bar'); }); test('trim string arguments', async t => { - const stdout = await execa.stdout(' node fixtures/echo foo bar '); + const {stdout} = await execa(' node fixtures/echo foo bar '); t.is(stdout, 'foo\nbar'); }); From 333a074fe740e946391226bd41bc029327adf475 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Tue, 19 Mar 2019 16:10:01 +0100 Subject: [PATCH 18/32] Add test --- test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test.js b/test.js index cc61dafec..3e4b32116 100644 --- a/test.js +++ b/test.js @@ -106,6 +106,11 @@ test('allow string arguments', async t => { 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 string arguments together with array arguments', async t => { const {stdout} = await execa('node fixtures/echo foo bar', ['foo', 'bar']); t.is(stdout, 'foo\nbar\nfoo\nbar'); From 4a9f642abc55099cf364fe545b7b65312969758f Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 14:41:31 +0200 Subject: [PATCH 19/32] Fix rebasing error --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index c537ade6a..335e11af9 100644 --- a/index.js +++ b/index.js @@ -69,7 +69,7 @@ function handleArgs(command, args, options = {}) { options.cleanup = false; } - if (process.platform === 'win32' && path.basename(command, 'exe') === 'cmd') { + if (process.platform === 'win32' && path.basename(command, '.exe') === 'cmd') { // #116 args.unshift('/q'); } From 857bae7a7219db0208b53abe468e62bdcf18b8b6 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 14:44:59 +0200 Subject: [PATCH 20/32] Move lines --- index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 335e11af9..b708cbd38 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,17 @@ const stdio = require('./lib/stdio'); const TEN_MEGABYTES = 1000 * 1000 * 10; +const SPACES_REGEXP = / +/g; + +function parseCommand(command, args = []) { + const [file, ...extraArgs] = command + .trim() + .split(SPACES_REGEXP) + .reduce(handleEscaping, []); + const newArgs = [...extraArgs, ...args]; + return [file, newArgs]; +} + function handleArgs(command, args, options = {}) { if (args && !Array.isArray(args)) { options = args; @@ -77,17 +88,6 @@ function handleArgs(command, args, options = {}) { return {command, args, options, parsed}; } -function parseCommand(command, args = []) { - const [file, ...extraArgs] = command - .trim() - .split(SPACES_REGEXP) - .reduce(handleEscaping, []); - const newArgs = [...extraArgs, ...args]; - return [file, newArgs]; -} - -const SPACES_REGEXP = / +/g; - // Allow spaces to be escaped by a backslash if not meant as a delimiter const handleEscaping = function (tokens, token, index) { if (index === 0) { From 455bb55f5a1b0a60f9dd93e0891ae25c96039fb0 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 14:45:50 +0200 Subject: [PATCH 21/32] Change function arrow style --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index b708cbd38..a83cb4786 100644 --- a/index.js +++ b/index.js @@ -89,7 +89,7 @@ function handleArgs(command, args, options = {}) { } // Allow spaces to be escaped by a backslash if not meant as a delimiter -const handleEscaping = function (tokens, token, index) { +const handleEscaping = (tokens, token, index) => { if (index === 0) { return [token]; } From d4ea67d57cd3f4412a40daa3195d6be9134b1792 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 14:46:46 +0200 Subject: [PATCH 22/32] Remove duplicated example --- readme.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/readme.md b/readme.md index 0b682b332..dfa5f4c8a 100644 --- a/readme.md +++ b/readme.md @@ -56,21 +56,6 @@ const execa = require('execa'); const {stdout} = await execa.shell('echo unicorns'); //=> 'unicorns' - // Cancelling a spawned process - const subprocess = execa('node'); - setTimeout(() => { spawned.cancel() }, 1000); - try { - await subprocess; - } catch (error) { - console.log(subprocess.killed); // true - console.log(error.isCanceled); // true - } - - // Run a command as a string without a shell - const {stdout} = await execa('echo unicorns'); - //=> 'unicorns' - - // Catching an error try { await execa.shell('exit 3'); From 56262a4b6ddc98d2f07fa0064e6c6f7c082625a0 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 14:48:18 +0200 Subject: [PATCH 23/32] Fix entry point documentation --- readme.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index dfa5f4c8a..1fd71d83b 100644 --- a/readme.md +++ b/readme.md @@ -115,7 +115,8 @@ try { ## API -### execa(file | command, [arguments], [options]) +### execa(file, [arguments], [options]) +### execa(command, [options]) Execute a file. @@ -131,11 +132,13 @@ 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 | command, [arguments], [options]) +### execa.stdout(file, [arguments], [options]) +### execa.stdout(command, [options]) Same as `execa()`, but returns only `stdout`. -### execa.stderr(file | command, [arguments], [options]) +### execa.stderr(file, [arguments], [options]) +### execa.stderr(command, [options]) Same as `execa()`, but returns only `stderr`. @@ -147,7 +150,8 @@ Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#c The `child_process` instance is enhanced to also be promise for a result object with `stdout` and `stderr` properties. -### execa.sync(file | command, [arguments], [options]) +### execa.sync(file, [arguments], [options]) +### execa.sync(command, [options]) Execute a file synchronously. From 4fc80de5d4e1c4641e46341b58481581ab216dcb Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 14:56:52 +0200 Subject: [PATCH 24/32] Improve documentation --- readme.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1fd71d83b..2d26e1061 100644 --- a/readme.md +++ b/readme.md @@ -120,7 +120,11 @@ try { Execute a file. -Arguments can be specified either inside `command` (a string) or `arguments` (an array of strings). When specified inside `command`, spaces can be escaped with a backslash. Otherwise arguments need neither escaping nor quoting. +Arguments can be specified 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`. From e61912e4b76936eea9fa673bc03898798b919afc Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:00:02 +0200 Subject: [PATCH 25/32] Improve documentation --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 2d26e1061..8f8da1a10 100644 --- a/readme.md +++ b/readme.md @@ -128,6 +128,8 @@ Arguments should not be escaped nor quoted. Exception: inside `command`, spaces 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 you cannot use shell features such as variables substitution `echo $PATH`. + 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. From d69312cd1e780bf764efca5d0ba4a85fa5a8ab99 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:11:58 +0200 Subject: [PATCH 26/32] Do not allow arguments to be passed both inside command and as additional array --- index.js | 7 +++++-- test.js | 5 ++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index a83cb4786..4f1333896 100644 --- a/index.js +++ b/index.js @@ -18,12 +18,15 @@ const TEN_MEGABYTES = 1000 * 1000 * 10; const SPACES_REGEXP = / +/g; 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, []); - const newArgs = [...extraArgs, ...args]; - return [file, newArgs]; + return [file, extraArgs]; } function handleArgs(command, args, options = {}) { diff --git a/test.js b/test.js index 3e4b32116..2c7e017dd 100644 --- a/test.js +++ b/test.js @@ -111,9 +111,8 @@ test('allow string arguments in synchronous mode', t => { t.is(stdout, 'foo\nbar'); }); -test('allow string arguments together with array arguments', async t => { - const {stdout} = await execa('node fixtures/echo foo bar', ['foo', 'bar']); - t.is(stdout, 'foo\nbar\nfoo\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 => { From 8f9e5b32f42bb6e020e00480dcf0641cff900363 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:13:20 +0200 Subject: [PATCH 27/32] Move lines --- index.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 4f1333896..d277631a0 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,21 @@ const TEN_MEGABYTES = 1000 * 1000 * 10; const SPACES_REGEXP = / +/g; +// Allow spaces to be escaped by a backslash if not meant as a delimiter +const 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'); @@ -91,21 +106,6 @@ function handleArgs(command, args, options = {}) { return {command, args, options, parsed}; } -// Allow spaces to be escaped by a backslash if not meant as a delimiter -const 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 handleInput(spawned, input) { if (input === undefined) { return; From 44617b40b1a14745fe269ac0bca660aa6202c4dd Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:14:18 +0200 Subject: [PATCH 28/32] Fix whitespaces --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 8f8da1a10..00e96121d 100644 --- a/readme.md +++ b/readme.md @@ -121,7 +121,7 @@ try { Execute a file. Arguments can be specified either: - - `arguments`: `execa('echo', ['unicorns'])`. + - `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. From 7b2625146dff48d79f9af723434ca672e9b0dee9 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:14:36 +0200 Subject: [PATCH 29/32] Fix grammar --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 00e96121d..98f6cbd8b 100644 --- a/readme.md +++ b/readme.md @@ -120,7 +120,7 @@ try { Execute a file. -Arguments can be specified either: +Arguments can be specified in either: - `arguments`: `execa('echo', ['unicorns'])`. - `command`: `execa('echo unicorns')`. From 0dc7e4a90308464dc0228cc1cc41416b02201ae9 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:15:34 +0200 Subject: [PATCH 30/32] Fix grammar --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 98f6cbd8b..2c1178370 100644 --- a/readme.md +++ b/readme.md @@ -128,7 +128,7 @@ Arguments should not be escaped nor quoted. Exception: inside `command`, spaces 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 you cannot use shell features such as variables substitution `echo $PATH`. +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`. From 97c91eaa0e19a2b5681f9f9d2bad4405f5a67994 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Apr 2019 15:29:40 +0200 Subject: [PATCH 31/32] Add parenthesis --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 2c1178370..971fa6d9a 100644 --- a/readme.md +++ b/readme.md @@ -128,7 +128,7 @@ Arguments should not be escaped nor quoted. Exception: inside `command`, spaces 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. +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`. From 90926b62b7e7d44be47437a97a3ca10c4fe77ceb Mon Sep 17 00:00:00 2001 From: ehmicky Date: Mon, 22 Apr 2019 11:06:14 +0200 Subject: [PATCH 32/32] Fix function style --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index d277631a0..bb635fadf 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,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 -const handleEscaping = (tokens, token, index) => { +function handleEscaping(tokens, token, index) { if (index === 0) { return [token]; } @@ -30,7 +30,7 @@ const handleEscaping = (tokens, token, index) => { } return [...tokens.slice(0, index - 1), `${previousToken.slice(0, -1)} ${token}`]; -}; +} function parseCommand(command, args = []) { if (args.length !== 0) {