diff --git a/CHANGELOG.md b/CHANGELOG.md
index 683c53f..45f6683 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+## [6.0.4](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.3...v6.0.4) (2018-01-31)
+
+
+### Bug Fixes
+
+* fix paths being incorrectly normalized on unix ([06ee3c6](https://github.com/moxystudio/node-cross-spawn/commit/06ee3c6)), closes [#90](https://github.com/moxystudio/node-cross-spawn/issues/90)
+
+
+
## [6.0.3](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.2...v6.0.3) (2018-01-23)
diff --git a/lib/parse.js b/lib/parse.js
index a8f0bf0..6f9c76d 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -49,6 +49,10 @@ function parseNonShell(parsed) {
// we need to double escape them
const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
+ // Normalize posix paths into OS compatible paths (e.g.: foo/bar -> foo\bar)
+ // This is necessary otherwise it will always fail with ENOENT in those cases
+ parsed.command = path.normalize(parsed.command);
+
// Escape command & arguments
parsed.command = escape.command(parsed.command);
parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
@@ -104,7 +108,7 @@ function parse(command, args, options) {
// Build our parsed object
const parsed = {
- command: path.normalize(command),
+ command,
args,
options,
file: undefined,
diff --git a/lib/util/resolveCommand.js b/lib/util/resolveCommand.js
index 0d32711..2fd5ad2 100644
--- a/lib/util/resolveCommand.js
+++ b/lib/util/resolveCommand.js
@@ -4,15 +4,40 @@ const path = require('path');
const which = require('which');
const pathKey = require('path-key')();
-function resolveCommandAttempt(parsed, withPathExt) {
- withPathExt = !!withPathExt;
+function resolveCommandAttempt(parsed, withoutPathExt) {
+ const cwd = process.cwd();
+ const hasCustomCwd = parsed.options.cwd != null;
+
+ // If a custom `cwd` was specified, we need to change the process cwd
+ // because `which` will do stat calls but does not support a custom cwd
+ if (hasCustomCwd) {
+ try {
+ process.chdir(parsed.options.cwd);
+ } catch (err) {
+ /* Empty */
+ }
+ }
+
+ let resolved;
try {
- return which.sync(parsed.command, {
+ resolved = which.sync(parsed.command, {
path: (parsed.options.env || process.env)[pathKey],
- pathExt: withPathExt && process.env.PATHEXT ? path.delimiter + process.env.PATHEXT : undefined,
+ pathExt: withoutPathExt ? path.delimiter : undefined,
});
- } catch (e) { /* Empty */ }
+ } catch (e) {
+ /* Empty */
+ } finally {
+ process.chdir(cwd);
+ }
+
+ // If we successfully resolved, ensure that an absolute path is returned
+ // Note that when a custom `cwd` was used, we need to resolve to an absolute path based on it
+ if (resolved) {
+ resolved = path.resolve(hasCustomCwd ? parsed.options.cwd : '', resolved);
+ }
+
+ return resolved;
}
function resolveCommand(parsed) {
diff --git a/package-lock.json b/package-lock.json
index 40ccd7d..ef26158 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "cross-spawn",
- "version": "6.0.3",
+ "version": "6.0.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 625136c..190ed10 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cross-spawn",
- "version": "6.0.3",
+ "version": "6.0.4",
"description": "Cross platform child_process#spawn and child_process#spawnSync",
"keywords": [
"spawn",
diff --git a/test/index.test.js b/test/index.test.js
index fe5e020..2217d9c 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -32,7 +32,7 @@ run.methods.forEach((method) => {
expect(stdout.trim()).toBe('foo');
});
- it('should support shebang in executables with /usr/bin/env', async () => {
+ it('should support shebang in executables with `/usr/bin/env`', async () => {
const { stdout: stdout1 } = await run(method, `${__dirname}/fixtures/shebang`);
expect(stdout1).toBe('shebang works!');
@@ -219,6 +219,26 @@ run.methods.forEach((method) => {
expect(stdout3.trim()).toBe('foo');
});
+ it('should work with a relative posix path to a command with a custom `cwd`', async () => {
+ const relativeTestPath = path.relative(process.cwd(), __dirname).replace(/\\/, '/');
+
+ const { stdout: stdout1 } = await run(method, 'fixtures/say-foo', { cwd: relativeTestPath });
+
+ expect(stdout1.trim()).toBe('foo');
+
+ const { stdout: stdout2 } = await run(method, './fixtures/say-foo', { cwd: `./${relativeTestPath}` });
+
+ expect(stdout2.trim()).toBe('foo');
+
+ if (!isWin) {
+ return;
+ }
+
+ const { stdout: stdout3 } = await run(method, './fixtures/say-foo.bat', { cwd: `./${relativeTestPath}` });
+
+ expect(stdout3.trim()).toBe('foo');
+ });
+
{
const assertError = (err) => {
const syscall = run.isMethodSync(method) ? 'spawnSync' : 'spawn';
@@ -354,6 +374,45 @@ run.methods.forEach((method) => {
});
}
+ if (run.isMethodSync(method)) {
+ it('should fail with ENOENT a non-existing `cwd` was specified', () => {
+ expect.assertions(1);
+
+ try {
+ run(method, 'fixtures/say-foo', { cwd: 'somedirthatwillneverexist' });
+ } catch (err) {
+ expect(err.code).toBe('ENOENT');
+ }
+ });
+ } else {
+ it('should emit `error` and `close` if a non-existing `cwd` was specified', async () => {
+ expect.assertions(3);
+
+ await new Promise((resolve, reject) => {
+ const promise = run(method, 'somecommandthatwillneverexist', ['foo']);
+ const { cp } = promise;
+
+ promise.catch(() => {});
+
+ let timeout;
+
+ cp
+ .on('error', (err) => expect(err.code).toBe('ENOENT'))
+ .on('exit', () => {
+ cp.removeAllListeners();
+ clearTimeout(timeout);
+ reject(new Error('Should not emit exit'));
+ })
+ .on('close', (code, signal) => {
+ expect(code).not.toBe(0);
+ expect(signal).toBe(null);
+
+ timeout = setTimeout(resolve, 1000);
+ });
+ });
+ });
+ }
+
if (isWin) {
it('should use nodejs\' spawn when options.shell is specified (windows)', async () => {
const { stdout } = await run(method, 'echo', ['%RANDOM%'], { shell: true });