├── test ├── fixtures │ ├── exit1.bat │ ├── foo.bat │ ├── exit1 │ ├── foo │ ├── ()%!^&;, .bat │ ├── bar space │ ├── bar space.bat │ ├── exit.js │ ├── prepare_()%!^&;, .sh │ ├── shebang_enoent │ ├── shebang │ └── echo.js ├── .eslintrc ├── prepare.js ├── util │ └── buffered.js └── test.js ├── .npmignore ├── .gitignore ├── .travis.yml ├── .eslintrc ├── .editorconfig ├── appveyor.yml ├── index.js ├── LICENSE ├── package.json ├── lib ├── resolveCommand.js ├── enoent.js └── parse.js └── README.md /test/fixtures/exit1.bat: -------------------------------------------------------------------------------- 1 | exit 1 2 | -------------------------------------------------------------------------------- /test/fixtures/foo.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo foo 3 | -------------------------------------------------------------------------------- /test/fixtures/exit1: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exit 1 4 | -------------------------------------------------------------------------------- /test/fixtures/foo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo foo 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.* 3 | test/ 4 | -------------------------------------------------------------------------------- /test/fixtures/()%!^&;, .bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo special 3 | -------------------------------------------------------------------------------- /test/fixtures/bar space: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo bar 4 | -------------------------------------------------------------------------------- /test/fixtures/bar space.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo bar 3 | -------------------------------------------------------------------------------- /test/fixtures/exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.exit(25); 4 | -------------------------------------------------------------------------------- /test/fixtures/prepare_()%!^&;, .sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo special 4 | -------------------------------------------------------------------------------- /test/fixtures/shebang_enoent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env somecommandthatwillneverexist 2 | 3 | echo foo 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.* 3 | test/fixtures/(* 4 | test/fixtures/shebang_noenv 5 | test/tmp 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4' 6 | - '5' 7 | - '6' 8 | -------------------------------------------------------------------------------- /test/fixtures/shebang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.stdout.write('shebang works!'); 6 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-invalid-this": 0 7 | } 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "@satazor/eslint-config/es5", 5 | "@satazor/eslint-config/addons/node" 6 | ] 7 | } -------------------------------------------------------------------------------- /test/fixtures/echo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var args = process.argv.slice(2); 4 | 5 | args.forEach(function (arg, index) { 6 | process.stdout.write(arg + (index < args.length - 1 ? '\n' : '')); 7 | }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [package.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /test/prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var glob = require('glob'); 4 | var fs = require('fs'); 5 | 6 | var fixturesDir = __dirname + '/fixtures'; 7 | 8 | glob.sync('prepare_*', { cwd: __dirname + '/fixtures' }).forEach(function (file) { 9 | var contents = fs.readFileSync(fixturesDir + '/' + file); 10 | var finalFile = file.replace(/^prepare_/, '').replace(/\.sh$/, ''); 11 | 12 | fs.writeFileSync(fixturesDir + '/' + finalFile, contents); 13 | fs.chmodSync(fixturesDir + '/' + finalFile, parseInt('0777', 8)); 14 | 15 | process.stdout.write('Copied "' + file + '" to "' + finalFile + '"\n'); 16 | }); 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # appveyor file 2 | # http://www.appveyor.com/docs/appveyor-yml 3 | 4 | # build version format 5 | version: "{build}" 6 | 7 | # fix lineendings in Windows 8 | init: 9 | - git config --global core.autocrlf input 10 | 11 | # what combinations to test 12 | environment: 13 | matrix: 14 | - nodejs_version: 0.10 15 | - nodejs_version: 0.12 16 | - nodejs_version: 4 17 | - nodejs_version: 5 18 | - nodejs_version: 6 19 | 20 | # get the latest stable version of Node 0.STABLE.latest 21 | install: 22 | - ps: Install-Product node $env:nodejs_version 23 | - npm install 24 | 25 | build: off 26 | 27 | test_script: 28 | - node --version 29 | - npm --version 30 | - cmd: npm test --no-color 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cp = require('child_process'); 4 | var parse = require('./lib/parse'); 5 | var enoent = require('./lib/enoent'); 6 | 7 | function spawn(command, args, options) { 8 | var parsed; 9 | var spawned; 10 | 11 | // Parse the arguments 12 | parsed = parse(command, args, options); 13 | 14 | // Spawn the child process 15 | spawned = cp.spawn(parsed.command, parsed.args, parsed.options); 16 | 17 | // Hook into child process "exit" event to emit an error if the command 18 | // does not exists, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16 19 | enoent.hookChildProcess(spawned, parsed); 20 | 21 | return spawned; 22 | } 23 | 24 | module.exports = spawn; 25 | module.exports.spawn = spawn; 26 | module.exports._parse = parse; 27 | module.exports._enoent = enoent; 28 | -------------------------------------------------------------------------------- /test/util/buffered.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spawn = require('../../'); 4 | 5 | function buffered(command, args, options, callback) { 6 | var cp; 7 | var stdout = null; 8 | var stderr = null; 9 | 10 | if (typeof options === 'function') { 11 | callback = options; 12 | options = null; 13 | } 14 | 15 | if (typeof args === 'function') { 16 | callback = args; 17 | args = options = null; 18 | } 19 | 20 | cp = spawn(command, args, options); 21 | 22 | cp.stdout && cp.stdout.on('data', function (buffer) { 23 | stdout = stdout || ''; 24 | stdout += buffer.toString(); 25 | }); 26 | 27 | cp.stderr && cp.stderr.on('data', function (buffer) { 28 | stderr = stderr || ''; 29 | stderr += buffer.toString(); 30 | }); 31 | 32 | cp.on('error', callback); 33 | 34 | cp.on('close', function (code) { 35 | code !== 0 && stderr && console.warn(stderr); 36 | callback(null, stdout, code); 37 | }); 38 | } 39 | 40 | module.exports = buffered; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 IndigoUnited 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cross-spawn-async", 3 | "version": "2.2.5", 4 | "description": "Cross platform child_process#spawn", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test/prepare && mocha --bail test/test", 8 | "lint": "eslint '{*.js,lib/**/*.js,test/**/*.js}'" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/IndigoUnited/node-cross-spawn-async/issues/" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/IndigoUnited/node-cross-spawn-async.git" 16 | }, 17 | "keywords": [ 18 | "spawn", 19 | "windows", 20 | "cross", 21 | "platform", 22 | "path", 23 | "ext", 24 | "path-ext", 25 | "path_ext", 26 | "shebang", 27 | "hashbang", 28 | "cmd", 29 | "execute" 30 | ], 31 | "author": "IndigoUnited (http://indigounited.com)", 32 | "license": "MIT", 33 | "dependencies": { 34 | "lru-cache": "^4.0.0", 35 | "which": "^1.2.8" 36 | }, 37 | "devDependencies": { 38 | "@satazor/eslint-config": "^3.0.0", 39 | "eslint": "^3.0.0", 40 | "expect.js": "^0.3.0", 41 | "glob": "^7.0.0", 42 | "mkdirp": "^0.5.1", 43 | "mocha": "^3.0.2", 44 | "rimraf": "^2.5.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/resolveCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var which = require('which'); 5 | var LRU = require('lru-cache'); 6 | 7 | var commandCache = new LRU({ max: 50, maxAge: 30 * 1000 }); // Cache just for 30sec 8 | var hasSepInPathRegExp = new RegExp(process.platform === 'win32' ? /[\/\\]/ : /\//); 9 | 10 | function resolveCommand(command, noExtension) { 11 | var resolved; 12 | 13 | // If command looks like a file path, make it absolute to make it canonical 14 | // and also to circuvent a bug in which, see: https://github.com/npm/node-which/issues/33 15 | if (hasSepInPathRegExp.test(command)) { 16 | command = path.resolve(command); 17 | } 18 | 19 | noExtension = !!noExtension; 20 | resolved = commandCache.get(command + '!' + noExtension); 21 | 22 | // Check if its resolved in the cache 23 | if (commandCache.has(command)) { 24 | return commandCache.get(command); 25 | } 26 | 27 | try { 28 | resolved = !noExtension ? 29 | which.sync(command) : 30 | which.sync(command, { pathExt: path.delimiter + (process.env.PATHEXT || '') }); 31 | } catch (e) { /* empty */ } 32 | 33 | commandCache.set(command + '!' + noExtension, resolved); 34 | 35 | return resolved; 36 | } 37 | 38 | module.exports = resolveCommand; 39 | -------------------------------------------------------------------------------- /lib/enoent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isWin = process.platform === 'win32'; 4 | 5 | function notFoundError(command, syscall) { 6 | var err; 7 | 8 | err = new Error(syscall + ' ' + command + ' ENOENT'); 9 | err.code = err.errno = 'ENOENT'; 10 | err.syscall = syscall + ' ' + command; 11 | 12 | return err; 13 | } 14 | 15 | function hookChildProcess(cp, parsed) { 16 | var originalEmit; 17 | 18 | if (!isWin) { 19 | return; 20 | } 21 | 22 | originalEmit = cp.emit; 23 | cp.emit = function (name, arg1) { 24 | var err; 25 | 26 | // If emitting "exit" event and exit code is 1, we need to check if 27 | // the command exists and emit an "error" instead 28 | // See: https://github.com/IndigoUnited/node-cross-spawn/issues/16 29 | if (name === 'exit') { 30 | err = verifyENOENT(arg1, parsed, 'spawn'); 31 | 32 | if (err) { 33 | return originalEmit.call(cp, 'error', err); 34 | } 35 | } 36 | 37 | return originalEmit.apply(cp, arguments); 38 | }; 39 | } 40 | 41 | function verifyENOENT(status, parsed, syscall) { 42 | if (isWin && status === 1 && !parsed.file) { 43 | return notFoundError(parsed.original, syscall); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | module.exports.hookChildProcess = hookChildProcess; 50 | module.exports.verifyENOENT = verifyENOENT; 51 | module.exports.notFoundError = notFoundError; 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cross-spawn-async 2 | 3 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Build status][appveyor-image]][appveyor-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] 4 | 5 | [npm-url]:https://npmjs.org/package/cross-spawn-async 6 | [downloads-image]:http://img.shields.io/npm/dm/cross-spawn-async.svg 7 | [npm-image]:http://img.shields.io/npm/v/cross-spawn-async.svg 8 | [travis-url]:https://travis-ci.org/IndigoUnited/node-cross-spawn-async 9 | [travis-image]:http://img.shields.io/travis/IndigoUnited/node-cross-spawn-async/master.svg 10 | [appveyor-url]:https://ci.appveyor.com/project/satazor/node-cross-spawn-async 11 | [appveyor-image]:https://img.shields.io/appveyor/ci/satazor/node-cross-spawn-async/master.svg 12 | [david-dm-url]:https://david-dm.org/IndigoUnited/node-cross-spawn-async 13 | [david-dm-image]:https://img.shields.io/david/IndigoUnited/node-cross-spawn-async.svg 14 | [david-dm-dev-url]:https://david-dm.org/IndigoUnited/node-cross-spawn-async?type=dev 15 | [david-dm-dev-image]:https://img.shields.io/david/dev/IndigoUnited/node-cross-spawn-async.svg 16 | 17 | A cross platform solution to node's spawn. 18 | 19 | **This module is deprecated, use [cross-spawn](https://github.com/IndigoUnited/node-cross-spawn) instead which no longer requires a build toolchain.** 20 | 21 | 22 | ## Installation 23 | 24 | `$ npm install cross-spawn-async` 25 | 26 | 27 | ## Why 28 | 29 | Node has issues when using spawn on Windows: 30 | 31 | - It ignores [PATHEXT](https://github.com/joyent/node/issues/2318) 32 | - It does not support [shebangs](http://pt.wikipedia.org/wiki/Shebang) 33 | - It does not allow you to run `del` or `dir` 34 | - It does not properly escape arguments with spaces or special characters 35 | 36 | All these issues are handled correctly by `cross-spawn-async`. 37 | There are some known modules, such as [win-spawn](https://github.com/ForbesLindesay/win-spawn), that try to solve this but they are either broken or provide faulty escaping of shell arguments. 38 | 39 | 40 | ## Usage 41 | 42 | Exactly the same way as node's [`spawn`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options), so it's a drop in replacement. 43 | 44 | ```javascript 45 | var spawn = require('cross-spawn-async'); 46 | 47 | var child = spawn('npm', ['list', '-g', '-depth', '0'], { stdio: 'inherit' }); 48 | ``` 49 | 50 | 51 | ## Tests 52 | 53 | `$ npm test` 54 | 55 | 56 | ## License 57 | 58 | Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php). 59 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var LRU = require('lru-cache'); 5 | var resolveCommand = require('./resolveCommand'); 6 | 7 | var isWin = process.platform === 'win32'; 8 | var shebangCache = new LRU({ max: 50, maxAge: 30 * 1000 }); // Cache just for 30sec 9 | 10 | function readShebang(command) { 11 | var buffer; 12 | var fd; 13 | var match; 14 | var shebang; 15 | 16 | // Check if it is in the cache first 17 | if (shebangCache.has(command)) { 18 | return shebangCache.get(command); 19 | } 20 | 21 | // Read the first 150 bytes from the file 22 | buffer = new Buffer(150); 23 | 24 | try { 25 | fd = fs.openSync(command, 'r'); 26 | fs.readSync(fd, buffer, 0, 150, 0); 27 | fs.closeSync(fd); 28 | } catch (e) { /* empty */ } 29 | 30 | // Check if it is a shebang 31 | match = buffer.toString().trim().match(/#!(.+)/i); 32 | 33 | if (match) { 34 | shebang = match[1].replace(/\/usr\/bin\/env\s+/i, ''); // Remove /usr/bin/env 35 | } 36 | 37 | // Store the shebang in the cache 38 | shebangCache.set(command, shebang); 39 | 40 | return shebang; 41 | } 42 | 43 | function escapeArg(arg, quote) { 44 | // Convert to string 45 | arg = '' + arg; 46 | 47 | // If we are not going to quote the argument, 48 | // escape shell metacharacters, including double and single quotes: 49 | if (!quote) { 50 | arg = arg.replace(/([\(\)%!\^<>&|;,"'\s])/g, '^$1'); 51 | } else { 52 | // Sequence of backslashes followed by a double quote: 53 | // double up all the backslashes and escape the double quote 54 | arg = arg.replace(/(\\*)"/g, '$1$1\\"'); 55 | 56 | // Sequence of backslashes followed by the end of the string 57 | // (which will become a double quote later): 58 | // double up all the backslashes 59 | arg = arg.replace(/(\\*)$/, '$1$1'); 60 | 61 | // All other backslashes occur literally 62 | 63 | // Quote the whole thing: 64 | arg = '"' + arg + '"'; 65 | } 66 | 67 | return arg; 68 | } 69 | 70 | function escapeCommand(command) { 71 | // Do not escape if this command is not dangerous.. 72 | // We do this so that commands like "echo" or "ifconfig" work 73 | // Quoting them, will make them unaccessible 74 | return /^[a-z0-9_-]+$/i.test(command) ? command : escapeArg(command, true); 75 | } 76 | 77 | function parse(command, args, options) { 78 | var shebang; 79 | var applyQuotes; 80 | var file; 81 | var original; 82 | 83 | // Normalize arguments, similar to nodejs 84 | if (args && !Array.isArray(args)) { 85 | options = args; 86 | args = null; 87 | } 88 | 89 | args = args ? args.slice(0) : []; // Clone array to avoid changing the original 90 | options = options || {}; 91 | original = command; 92 | 93 | if (isWin) { 94 | // Detect & add support for shebangs 95 | file = resolveCommand(command); 96 | file = file || resolveCommand(command, true); 97 | shebang = file && readShebang(file); 98 | 99 | if (shebang) { 100 | args.unshift(file); 101 | command = shebang; 102 | } 103 | 104 | // Escape command & arguments 105 | applyQuotes = command !== 'echo'; // Do not quote arguments for the special "echo" command 106 | command = escapeCommand(command); 107 | args = args.map(function (arg) { 108 | return escapeArg(arg, applyQuotes); 109 | }); 110 | 111 | // Use cmd.exe 112 | args = ['/s', '/c', '"' + command + (args.length ? ' ' + args.join(' ') : '') + '"']; 113 | command = process.env.comspec || 'cmd.exe'; 114 | 115 | // Tell node's spawn that the arguments are already escaped 116 | options.windowsVerbatimArguments = true; 117 | } 118 | 119 | return { 120 | command: command, 121 | args: args, 122 | options: options, 123 | file: file, 124 | original: original, 125 | }; 126 | } 127 | 128 | module.exports = parse; 129 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var which = require('which'); 6 | var rimraf = require('rimraf'); 7 | var mkdirp = require('mkdirp'); 8 | var expect = require('expect.js'); 9 | var buffered = require('./util/buffered'); 10 | var spawn = require('../'); 11 | 12 | var isWin = process.platform === 'win32'; 13 | 14 | // Fix AppVeyor tests because Git bin folder is in PATH and it has a "echo" program there 15 | if (isWin) { 16 | process.env.PATH = process.env.PATH 17 | .split(path.delimiter) 18 | .filter(function (entry) { 19 | return !/\\git\\bin$/i.test(path.normalize(entry)); 20 | }) 21 | .join(path.delimiter); 22 | } 23 | 24 | describe('cross-spawn-async', function () { 25 | var originalPath = process.env.PATH; 26 | 27 | before(function () { 28 | mkdirp.sync(__dirname + '/tmp'); 29 | }); 30 | 31 | after(function (next) { 32 | // Give it some time, RIMRAF was giving problems on windows 33 | this.timeout(10000); 34 | 35 | rimraf(__dirname + '/tmp', function () { 36 | // Ignore errors, RIMRAF was giving problems on windows 37 | next(null); 38 | }); 39 | }); 40 | 41 | afterEach(function () { 42 | process.env.PATH = originalPath; 43 | }); 44 | 45 | it('should support shebang in executables with /usr/bin/env', function (next) { 46 | buffered(__dirname + '/fixtures/shebang', function (err, data, code) { 47 | expect(err).to.not.be.ok(); 48 | expect(code).to.be(0); 49 | expect(data).to.equal('shebang works!'); 50 | 51 | // Test if the actual shebang file is resolved against the PATH 52 | process.env.PATH = path.normalize(__dirname + '/fixtures/') + path.delimiter + process.env.PATH; 53 | 54 | buffered('shebang', function (err, data, code) { 55 | expect(err).to.not.be.ok(); 56 | expect(code).to.be(0); 57 | expect(data).to.equal('shebang works!'); 58 | 59 | next(); 60 | }); 61 | }); 62 | }); 63 | 64 | it('should support shebang in executables without /usr/bin/env', function (next) { 65 | var nodejs = which.sync('node'); 66 | var file = __dirname + '/fixtures/shebang_noenv'; 67 | 68 | fs.writeFileSync(file, '#!' + nodejs + '\n\nprocess.stdout.write(\'shebang works!\');', { 69 | mode: parseInt('0777', 8), 70 | }); 71 | 72 | buffered(file, function (err, data, code) { 73 | expect(err).to.not.be.ok(); 74 | expect(code).to.be(0); 75 | expect(data).to.equal('shebang works!'); 76 | 77 | // Test if the actual shebang file is resolved against the PATH 78 | process.env.PATH = path.normalize(__dirname + '/fixtures/') + path.delimiter + process.env.PATH; 79 | 80 | buffered('shebang_noenv', function (err, data, code) { 81 | expect(err).to.not.be.ok(); 82 | expect(code).to.be(0); 83 | expect(data).to.equal('shebang works!'); 84 | 85 | next(); 86 | }); 87 | }); 88 | }); 89 | 90 | it('should support shebang in executables with relative path', function (next) { 91 | var executable = './' + path.relative(process.cwd(), __dirname + '/fixtures/shebang'); 92 | 93 | fs.writeFileSync(__dirname + '/tmp/shebang', '#!/usr/bin/env node\n\nprocess.stdout.write(\'yeah\');', 94 | { mode: parseInt('0777', 8) }); 95 | process.env.PATH = path.normalize(__dirname + '/tmp/') + path.delimiter + process.env.PATH; 96 | 97 | buffered(executable, function (err, data, code) { 98 | expect(err).to.not.be.ok(); 99 | expect(code).to.be(0); 100 | expect(data).to.equal('shebang works!'); 101 | 102 | next(); 103 | }); 104 | }); 105 | 106 | it('should support shebang in executables with relative path that starts with `..`', function (next) { 107 | var executable = '../' + path.basename(process.cwd()) + '/' + path.relative(process.cwd(), __dirname + '/fixtures/shebang'); 108 | 109 | fs.writeFileSync(__dirname + '/tmp/shebang', '#!/usr/bin/env node\n\nprocess.stdout.write(\'yeah\');', 110 | { mode: parseInt('0777', 8) }); 111 | process.env.PATH = path.normalize(__dirname + '/tmp/') + path.delimiter + process.env.PATH; 112 | 113 | buffered(executable, function (err, data, code) { 114 | expect(err).to.not.be.ok(); 115 | expect(code).to.be(0); 116 | expect(data).to.equal('shebang works!'); 117 | 118 | next(); 119 | }); 120 | }); 121 | 122 | it('should support shebang in executables with extensions', function (next) { 123 | fs.writeFileSync(__dirname + '/tmp/shebang.js', '#!/usr/bin/env node\n\nprocess.stdout.write(\'shebang with extension\');', 124 | { mode: parseInt('0777', 8) }); 125 | process.env.PATH = path.normalize(__dirname + '/tmp/') + path.delimiter + process.env.PATH; 126 | 127 | buffered(__dirname + '/tmp/shebang.js', function (err, data, code) { 128 | expect(err).to.not.be.ok(); 129 | expect(code).to.be(0); 130 | expect(data).to.equal('shebang with extension'); 131 | 132 | // Test if the actual shebang file is resolved against the PATH 133 | process.env.PATH = path.normalize(__dirname + '/fixtures/') + path.delimiter + process.env.PATH; 134 | 135 | buffered('shebang.js', function (err, data, code) { 136 | expect(err).to.not.be.ok(); 137 | expect(code).to.be(0); 138 | expect(data).to.equal('shebang with extension'); 139 | 140 | next(); 141 | }); 142 | }); 143 | }); 144 | 145 | it('should expand using PATHEXT properly', function (next) { 146 | buffered(__dirname + '/fixtures/foo', function (err, data, code) { 147 | expect(err).to.not.be.ok(); 148 | expect(code).to.be(0); 149 | expect(data.trim()).to.equal('foo'); 150 | 151 | next(); 152 | }); 153 | }); 154 | 155 | it('should handle commands with spaces', function (next) { 156 | buffered(__dirname + '/fixtures/bar space', function (err, data, code) { 157 | expect(err).to.not.be.ok(); 158 | expect(code).to.be(0); 159 | expect(data.trim()).to.equal('bar'); 160 | 161 | next(); 162 | }); 163 | }); 164 | 165 | it('should handle commands with special shell chars', function (next) { 166 | buffered(__dirname + '/fixtures/()%!^&;, ', function (err, data, code) { 167 | expect(err).to.not.be.ok(); 168 | expect(code).to.be(0); 169 | expect(data.trim()).to.equal('special'); 170 | 171 | next(); 172 | }); 173 | }); 174 | 175 | it('should handle arguments with quotes', function (next) { 176 | buffered('node', [ 177 | __dirname + '/fixtures/echo', 178 | '"foo"', 179 | 'foo"bar"foo', 180 | ], function (err, data, code) { 181 | expect(err).to.not.be.ok(); 182 | expect(code).to.be(0); 183 | expect(data).to.equal('"foo"\nfoo"bar"foo'); 184 | 185 | next(); 186 | }); 187 | }); 188 | 189 | it('should handle empty arguments', function (next) { 190 | buffered('node', [ 191 | __dirname + '/fixtures/echo', 192 | 'foo', 193 | '', 194 | 'bar', 195 | ], function (err, data, code) { 196 | expect(err).to.not.be.ok(); 197 | expect(code).to.be(0); 198 | expect(data).to.equal('foo\n\nbar'); 199 | 200 | buffered('echo', [ 201 | 'foo', 202 | '', 203 | 'bar', 204 | ], function (err, data, code) { 205 | expect(err).to.not.be.ok(); 206 | expect(code).to.be(0); 207 | expect(data.trim()).to.equal('foo bar'); 208 | 209 | next(); 210 | }); 211 | }); 212 | }); 213 | 214 | it('should handle non-string arguments', function (next) { 215 | buffered('node', [ 216 | __dirname + '/fixtures/echo', 217 | 1234, 218 | ], function (err, data, code) { 219 | expect(err).to.not.be.ok(); 220 | expect(code).to.be(0); 221 | expect(data).to.equal('1234'); 222 | 223 | next(); 224 | }); 225 | }); 226 | 227 | it('should handle arguments with spaces', function (next) { 228 | buffered('node', [ 229 | __dirname + '/fixtures/echo', 230 | 'I am', 231 | 'André Cruz', 232 | ], function (err, data, code) { 233 | expect(err).to.not.be.ok(); 234 | expect(code).to.be(0); 235 | expect(data).to.equal('I am\nAndré Cruz'); 236 | 237 | next(); 238 | }); 239 | }); 240 | 241 | it('should handle arguments with \\"', function (next) { 242 | buffered('node', [ 243 | __dirname + '/fixtures/echo', 244 | 'foo', 245 | '\\"', 246 | 'bar', 247 | ], function (err, data, code) { 248 | expect(err).to.not.be.ok(); 249 | expect(code).to.be(0); 250 | expect(data).to.equal('foo\n\\"\nbar'); 251 | 252 | next(); 253 | }); 254 | }); 255 | 256 | it('should handle arguments that end with \\', function (next) { 257 | buffered('node', [ 258 | __dirname + '/fixtures/echo', 259 | 'foo', 260 | 'bar\\', 261 | 'baz', 262 | ], function (err, data, code) { 263 | expect(err).to.not.be.ok(); 264 | expect(code).to.be(0); 265 | expect(data).to.equal('foo\nbar\\\nbaz'); 266 | 267 | next(); 268 | }); 269 | }); 270 | 271 | it('should handle arguments that contain shell special chars', function (next) { 272 | buffered('node', [ 273 | __dirname + '/fixtures/echo', 274 | 'foo', 275 | '()', 276 | 'foo', 277 | '%!', 278 | 'foo', 279 | '^<', 280 | 'foo', 281 | '>&', 282 | 'foo', 283 | '|;', 284 | 'foo', 285 | ', ', 286 | 'foo', 287 | ], function (err, data, code) { 288 | expect(err).to.not.be.ok(); 289 | expect(code).to.be(0); 290 | expect(data).to.equal('foo\n()\nfoo\n%!\nfoo\n^<\nfoo\n>&\nfoo\n|;\nfoo\n, \nfoo'); 291 | 292 | next(); 293 | }); 294 | }); 295 | 296 | it('should handle special arguments when using echo', function (next) { 297 | buffered('echo', ['foo\\"foo\\foo&bar"foo\'bar'], function (err, data, code) { 298 | expect(err).to.not.be.ok(); 299 | expect(code).to.be(0); 300 | expect(data.trim()).to.equal('foo\\"foo\\foo&bar"foo\'bar'); 301 | 302 | buffered('echo', [ 303 | 'foo', 304 | '()', 305 | 'foo', 306 | '%!', 307 | 'foo', 308 | '^<', 309 | 'foo', 310 | '>&', 311 | 'foo', 312 | '|;', 313 | 'foo', 314 | ', ', 315 | 'foo', 316 | ], function (err, data, code) { 317 | expect(err).to.not.be.ok(); 318 | expect(code).to.be(0); 319 | expect(data.trim()).to.equal('foo () foo %! foo ^< foo >& foo |; foo , foo'); 320 | 321 | next(); 322 | }); 323 | }); 324 | }); 325 | 326 | it('should handle optional args correctly', function (next) { 327 | buffered(__dirname + '/fixtures/foo', function (err, data, code) { 328 | expect(err).to.not.be.ok(); 329 | expect(code).to.be(0); 330 | 331 | buffered(__dirname + '/fixtures/foo', { 332 | stdio: ['pipe', 'ignore', 'pipe'], 333 | }, function (err, data, code) { 334 | expect(err).to.not.be.ok(); 335 | expect(code).to.be(0); 336 | expect(data).to.be(null); 337 | 338 | buffered(__dirname + '/fixtures/foo', null, { 339 | stdio: ['pipe', 'ignore', 'pipe'], 340 | }, function (err, data, code) { 341 | expect(err).to.not.be.ok(); 342 | expect(code).to.be(0); 343 | expect(data).to.be(null); 344 | 345 | next(); 346 | }); 347 | }); 348 | }); 349 | }); 350 | 351 | it('should not mutate args nor options', function (next) { 352 | var args = []; 353 | var options = {}; 354 | 355 | buffered(__dirname + '/fixtures/foo', function (err, data, code) { 356 | expect(err).to.not.be.ok(); 357 | expect(code).to.be(0); 358 | 359 | expect(args).to.have.length(0); 360 | expect(Object.keys(options)).to.have.length(0); 361 | 362 | next(); 363 | }); 364 | }); 365 | 366 | it('should give correct exit code', function (next) { 367 | buffered('node', [__dirname + '/fixtures/exit'], function (err, data, code) { 368 | expect(err).to.not.be.ok(); 369 | expect(code).to.be(25); 370 | 371 | next(); 372 | }); 373 | }); 374 | 375 | it('should work with a relative command', function (next) { 376 | buffered(path.relative(process.cwd(), __dirname + '/fixtures/foo'), function (err, data, code) { 377 | expect(err).to.not.be.ok(); 378 | expect(code).to.be(0); 379 | expect(data.trim()).to.equal('foo'); 380 | 381 | if (!isWin) { 382 | return next(); 383 | } 384 | 385 | buffered(path.relative(process.cwd(), __dirname + '/fixtures/foo.bat'), function (err, data, code) { 386 | expect(err).to.not.be.ok(); 387 | expect(code).to.be(0); 388 | expect(data.trim()).to.equal('foo'); 389 | 390 | next(); 391 | }); 392 | }); 393 | }); 394 | 395 | it('should emit "error" and "close" if command does not exist', function (next) { 396 | var spawned; 397 | var errors = []; 398 | var timeout; 399 | 400 | this.timeout(5000); 401 | 402 | spawned = spawn('somecommandthatwillneverexist') 403 | .on('error', function (err) { 404 | errors.push(err); 405 | }) 406 | .on('exit', function () { 407 | spawned.removeAllListeners(); 408 | clearTimeout(timeout); 409 | next(new Error('Should not emit exit')); 410 | }) 411 | .on('close', function (code, signal) { 412 | expect(code).to.not.be(0); 413 | expect(signal).to.be(null); 414 | 415 | timeout = setTimeout(function () { 416 | var err; 417 | 418 | expect(errors).to.have.length(1); 419 | 420 | err = errors[0]; 421 | expect(err).to.be.an(Error); 422 | expect(err.message).to.contain('spawn'); 423 | expect(err.message).to.contain('ENOENT'); 424 | expect(err.message).to.not.contain('undefined'); 425 | expect(err.code).to.be('ENOENT'); 426 | expect(err.errno).to.be('ENOENT'); 427 | expect(err.syscall).to.contain('spawn'); 428 | expect(err.syscall).to.not.contain('undefined'); 429 | 430 | next(); 431 | }, 1000); 432 | }); 433 | }); 434 | 435 | it('should NOT emit "error" if shebang command does not exist', function (next) { 436 | var spawned; 437 | var exited; 438 | var timeout; 439 | 440 | this.timeout(5000); 441 | 442 | spawned = spawn(__dirname + '/fixtures/shebang_enoent') 443 | .on('error', function () { 444 | spawned.removeAllListeners(); 445 | clearTimeout(timeout); 446 | next(new Error('Should not emit error')); 447 | }) 448 | .on('exit', function () { 449 | exited = true; 450 | }) 451 | .on('close', function (code, signal) { 452 | expect(code).to.not.be(0); 453 | expect(signal).to.be(null); 454 | expect(exited).to.be(true); 455 | 456 | timeout = setTimeout(next, 1000); 457 | }); 458 | }); 459 | 460 | it('should NOT emit "error" if the command actual exists but exited with 1', function (next) { 461 | var spawned; 462 | var exited; 463 | var timeout; 464 | 465 | this.timeout(5000); 466 | 467 | spawned = spawn(__dirname + '/fixtures/exit1') 468 | .on('error', function () { 469 | spawned.removeAllListeners(); 470 | clearTimeout(timeout); 471 | next(new Error('Should not emit error')); 472 | }) 473 | .on('exit', function () { 474 | exited = true; 475 | }) 476 | .on('close', function (code, signal) { 477 | expect(code).to.not.be(0); 478 | expect(signal).to.be(null); 479 | expect(exited).to.be(true); 480 | 481 | timeout = setTimeout(next, 1000); 482 | }); 483 | }); 484 | }); 485 | --------------------------------------------------------------------------------