├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── package.json ├── tasks └── exec.js └── test ├── Gruntfile.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | 4 | node_modules 5 | .idea/ 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | exec: { 4 | create_dir: { cmd: 'mkdir "tmp dir"' }, 5 | remove_logs: { 6 | command: process.platform === 'win32' ? 'del *.log' : 'rm -f *.log' 7 | , stdout: false 8 | , stderr: false 9 | } 10 | , list_files: { 11 | cmd: process.platform === 'win32' ? 'dir' : 'ls -l **' 12 | } 13 | , list_all_files: process.platform === 'win32' ? 'dir' : 'ls -la' 14 | , echo_grunt_version: { 15 | cmd: function() { return 'echo ' + this.version; } 16 | } 17 | , print_name: { 18 | cmd: function(firstName, lastName) { 19 | var formattedName = [ 20 | (lastName || 'grunt').toUpperCase() 21 | , (firstName || 'exec').toUpperCase() 22 | ].join(', '); 23 | 24 | return 'echo ' + formattedName; 25 | } 26 | } 27 | , test_mysql_ticket: { 28 | 29 | cmd: function () { 30 | var DB_USER = "abc"; 31 | var DB_PASSWORD = "password"; 32 | var DATABASE = "database-with-dashes"; 33 | return `echo mysql -u "${DB_USER}" -p"${DB_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS \'${DATABASE}\';"`; 34 | }, 35 | callback: function(err, stdout, stderr) { 36 | console.log('stdout: ' + stdout); 37 | if (process.platform === 'win32') { 38 | if ((stdout + "").trim() !== `mysql -u "abc" -p"password" -e "CREATE DATABASE IF NOT EXISTS 'database-with-dashes';"`) { 39 | grunt.log.error("Unexpected result: " + stdout); 40 | return false; 41 | } 42 | } else { 43 | if ((stdout + "").trim() !== `mysql -u abc -ppassword -e CREATE DATABASE IF NOT EXISTS 'database-with-dashes';`) { 44 | grunt.log.error("Unexpected result: " + stdout); 45 | return false; 46 | } 47 | } 48 | } 49 | } 50 | , test_large_stdout_buffer: { 51 | // maxBuffer has to be equal to count + 1 52 | options: { maxBuffer: 10000001, encoding: 'buffer' }, 53 | 54 | stdout: false, // suppress the stdout otherwise you get a bunch of garbage being displayed 55 | 56 | cmd: function () { 57 | var count = 1000000; 58 | // generates count number of random bytes that are console-friendly (printable ascii 32->126) 59 | return `node -e "var garbage = ''; var count = ${count}; for (var i = 0; i < count; i++) { var c = String.fromCharCode(Math.floor(32 + (Math.random() * 94))); garbage += c; } process.stdout.write(garbage);"`; 60 | }, 61 | callback: function(err, stdout, stderr) { 62 | var count = 1000000; 63 | 64 | if (err && err.toString() === "Error: stdout maxBuffer exceeded") { 65 | grunt.log.error("Unexpected maxBuffer exceeded"); 66 | return false; 67 | } 68 | 69 | if (typeof stdout === "string") { 70 | grunt.log.error("Unexpected stdout type (string), expected buffer. String length was " + stdout.length); 71 | return false; 72 | } 73 | 74 | var str = stdout.toString(); 75 | 76 | if (str.length !== count) { 77 | grunt.log.error("Unexpected result: " + str.length + " != " + count.toString()); 78 | return false; 79 | } 80 | } 81 | } 82 | // this used to produce an error when using 'exec' 83 | , test_large_stdout_nocallback: { 84 | stdout: false, // suppress the stdout otherwise you get a bunch of garbage being displayed 85 | 86 | cmd: function () { 87 | var count = 1000000; 88 | // generates count number of random bytes that are console-friendly (printable ascii 32->126) 89 | return `node -e "var garbage = ''; var count = ${count}; for (var i = 0; i < count; i++) { var c = String.fromCharCode(Math.floor(32 + (Math.random() * 94))); garbage += c; } process.stdout.write(garbage);"`; 90 | }, 91 | } 92 | , test_large_stdout_string: { 93 | // maxBuffer has to be equal to count + 1 94 | options: { maxBuffer: 10000001 }, 95 | 96 | stdout: false, // suppress the stdout otherwise you get a bunch of garbage being displayed 97 | 98 | cmd: function () { 99 | var count = 1000000; 100 | // generates count number of random bytes that are console-friendly (printable ascii 32->126) 101 | return `node -e "var garbage = ''; var count = ${count}; for (var i = 0; i < count; i++) { var c = String.fromCharCode(Math.floor(32 + (Math.random() * 94))); garbage += c; } process.stdout.write(garbage);"`; 102 | }, 103 | callback: function(err, stdout, stderr) { 104 | var count = 1000000; 105 | 106 | if (err && err.toString() === "Error: stdout maxBuffer exceeded") { 107 | grunt.log.error("Unexpected maxBuffer exceeded"); 108 | return false; 109 | } 110 | 111 | if (typeof stdout !== "string") { 112 | grunt.log.error("Unexpected stdout type (" + (typeof stdout) + "), expected string."); 113 | return false; 114 | } 115 | 116 | if (stdout.length !== count) { 117 | grunt.log.error("Unexpected result: " + stdout.length + " != " + count.toString()); 118 | return false; 119 | } 120 | } 121 | } 122 | , test_callback: { 123 | cmd : process.platform === 'win32' ? 'dir' : 'ls -h', 124 | callback : function(error, stdout, stderr){ 125 | var util = require('util'); 126 | console.log(util.inspect(error)); 127 | console.log('stdout: ' + stdout); 128 | console.log('stderr: ' + stderr); 129 | }, 130 | stdout: 'pipe' 131 | }, 132 | test_callback_no_data: { 133 | cmd : process.platform === 'win32' ? 'dir' : 'ls -h', 134 | callback : function(error, stdout, stderr){ 135 | var util = require('util'); 136 | console.log(util.inspect(error)); 137 | console.log('stdout: ' + stdout); 138 | console.log('stderr: ' + stderr); 139 | } 140 | }, 141 | npm_outdated_color_test: { 142 | command: 'npm outdated --long --ansi --color', 143 | stdout: 'inherit', 144 | stderr: 'inherit' 145 | }, 146 | set_user_input: { 147 | cmd: 'set /p TestCurrentUserName= "Enter your name: "', 148 | stdout: 'inherit', 149 | stderr: 'inherit', 150 | stdin: 'inherit' 151 | }, 152 | test_timeout: { 153 | cmd: 'set /p TestCurrentUserName= "Please do not enter your name: "', 154 | stdout: 'inherit', 155 | stderr: 'inherit', 156 | stdin: 'inherit', 157 | timeout: 500 158 | }, 159 | test_stdio_inherit: { 160 | cmd: 'npm list', 161 | stdio: 'inherit' 162 | }, 163 | test_stdio_ignore: { 164 | cmd: 'npm list', 165 | stdio: 'ignore' 166 | } 167 | } 168 | 169 | , jshint: { 170 | options: { 171 | // enforcing options 172 | curly: true 173 | , forin: true 174 | , newcap: true 175 | , noarg: true 176 | , noempty: true 177 | , nonew: true 178 | , quotmark: true 179 | , undef: true 180 | , unused: true 181 | , trailing: true 182 | , maxlen: 80 183 | 184 | // relaxing options 185 | , boss: true 186 | , es5: true 187 | , expr: true 188 | , laxcomma: true 189 | 190 | // environments 191 | , node: true 192 | } 193 | , tasks: ['tasks/*.js'] 194 | , tests: ['test/*.js'] 195 | , gruntfile: ['Gruntfile.js'] 196 | } 197 | }); 198 | 199 | grunt.loadTasks('tasks'); 200 | grunt.loadNpmTasks('grunt-contrib-jshint'); 201 | 202 | grunt.registerTask('lint', 'jshint'); 203 | }; 204 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Original Copyright (c) 2012-2014 Jake Harding 2 | Copyright (c) 2016 grunt-exec 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://secure.travis-ci.org/jharding/grunt-exec.png?branch=master)](http://travis-ci.org/jharding/grunt-exec) 2 | grunt-exec 3 | ========== 4 | 5 | Grunt plugin for executing shell commands. 6 | 7 | [![NPM](https://nodei.co/npm/grunt-exec.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/grunt-exec/) 8 | [![NPM](https://nodei.co/npm-dl/grunt-exec.png)](https://nodei.co/npm/grunt-exec/) 9 | 10 | Installation 11 | ------------ 12 | 13 | Install grunt-exec using npm: 14 | 15 | ``` 16 | $ npm install grunt-exec --save-dev 17 | ``` 18 | 19 | Then add this line to your project's *Gruntfile.js*: 20 | 21 | ```javascript 22 | grunt.loadNpmTasks('grunt-exec'); 23 | ``` 24 | 25 | Usage 26 | ----- 27 | 28 | This plugin is a [multi task][types_of_tasks], meaning that grunt will 29 | automatically iterate over all exec targets if a target is not specified. 30 | 31 | If the exit code generated by the specified shell command is greater than 0, 32 | grunt-exec will assume an error has occurred and will abort grunt immediately. 33 | 34 | [types_of_tasks]: http://gruntjs.com/configuring-tasks#task-configuration-and-targets 35 | 36 | ### Properties 37 | 38 | * __command__ (alias: __cmd__): The shell command to be executed. Must be a 39 | string or a function that returns a string. 40 | * __stdin__: If `true`, stdin will be redirected from the child process to the current process allowing user interactivity (EXPERIMENTAL) 41 | * __stdout__: If `true`, stdout will be printed. Defaults to `true`. 42 | * __stderr__: If `true`, stderr will be printed. Defaults to `true`. 43 | * __cwd__: Current working directory of the shell command. Defaults to the 44 | directory containing your Gruntfile. 45 | * __exitCode__ (alias: __exitCodes__): The expected exit code(s), task will 46 | fail if the actual exit code doesn't match. Defaults to `0`. Can be an array 47 | for multiple allowed exit codes. 48 | * __callback__: The callback function passed `child_process.exec`. Defaults to 49 | a noop. 50 | * __callbackArgs__: Additional arguments to pass to the callback. Defaults to empty array. 51 | * __sync__: Whether to use `child_process.spawnSync`. Defaults to false. 52 | * __options__: Options to provide to `child_process.exec`. [NodeJS Documentation](http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) 53 | - `cwd` String Current working directory of the child process 54 | - `env` Object Environment key-value pairs 55 | - `encoding` String *(Default: 'utf8')* 56 | - `shell` String Shell to execute the command with *(Default: '/bin/sh' on UNIX, 'cmd.exe' on Windows, The shell should understand the -c switch on UNIX or /s /c on Windows. On Windows, command line parsing should be compatible with cmd.exe.)* 57 | - `timeout` Number *(Default: 0)* 58 | - `maxBuffer` Number largest amount of data (in bytes) allowed on stdout or stderr - if exceeded child process is killed **(Default: 200*1024)** 59 | - `killSignal` String *(Default: 'SIGTERM')* 60 | - `uid` Number Sets the user identity of the process. (See [setuid(2)](http://man7.org/linux/man-pages/man2/setuid.2.html).) 61 | - `gid` Number Sets the group identity of the process. (See [setgid(2)](http://man7.org/linux/man-pages/man2/setgid.2.html).) 62 | 63 | If the configuration is instead a simple `string`, it will be 64 | interpreted as a full command itself: 65 | 66 | ```javascript 67 | exec: { 68 | echo_something: 'echo "This is something"' 69 | } 70 | ``` 71 | 72 | ### Command Functions 73 | 74 | If you plan on doing advanced stuff with grunt-exec, you'll most likely be using 75 | functions for the `command` property of your exec targets. This section details 76 | a couple of helpful tips about command functions that could help make your life 77 | easier. 78 | 79 | #### Passing arguments from the command line 80 | 81 | Command functions can be called with arbitrary arguments. Let's say we have the 82 | following exec target that echoes a formatted name: 83 | 84 | ```javascript 85 | exec: { 86 | echo_name: { 87 | cmd: function(firstName, lastName) { 88 | var formattedName = [ 89 | lastName.toUpperCase(), 90 | firstName.toUpperCase() 91 | ].join(', '); 92 | 93 | return 'echo ' + formattedName; 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | In order to get `SIMPSON, HOMER` echoed, you'd run 100 | `grunt exec:echo_name:homer:simpson` from the command line. 101 | 102 | ### Accessing `grunt` object 103 | 104 | All command functions are called in the context of the `grunt` object that they 105 | are being ran with. This means you can access the `grunt` object through `this`. 106 | 107 | ### Example 108 | 109 | The following examples are available in grunt-exec's Gruntfile. 110 | 111 | ```javascript 112 | grunt.initConfig({ 113 | exec: { 114 | remove_logs: { 115 | command: 'rm -f *.log', 116 | stdout: false, 117 | stderr: false 118 | }, 119 | list_files: { 120 | cmd: 'ls -l **' 121 | }, 122 | list_all_files: 'ls -la', 123 | echo_grunt_version: { 124 | cmd: function() { return 'echo ' + this.version; } 125 | }, 126 | echo_name: { 127 | cmd: function(firstName, lastName) { 128 | var formattedName = [ 129 | lastName.toUpperCase(), 130 | firstName.toUpperCase() 131 | ].join(', '); 132 | 133 | return 'echo ' + formattedName; 134 | } 135 | } 136 | } 137 | }); 138 | ``` 139 | 140 | Testing 141 | ------- 142 | 143 | ``` 144 | $ cd grunt-exec 145 | $ npm test 146 | ``` 147 | 148 | Issues 149 | ------ 150 | 151 | Found a bug? Create an issue on GitHub. 152 | 153 | https://github.com/jharding/grunt-exec/issues 154 | 155 | Versioning 156 | ---------- 157 | 158 | For transparency and insight into the release cycle, releases will be numbered 159 | with the follow format: 160 | 161 | `..` 162 | 163 | And constructed with the following guidelines: 164 | 165 | * Breaking backwards compatibility bumps the major 166 | * New additions without breaking backwards compatibility bumps the minor 167 | * Bug fixes and misc changes bump the patch 168 | 169 | For more information on semantic versioning, please visit http://semver.org/. 170 | 171 | License 172 | ------- 173 | 174 | Original Copyright (c) 2012-2014 [Jake Harding](http://thejakeharding.com) 175 | Copyright (c) 2016 grunt-exec 176 | Licensed under the [MIT License](http://www.opensource.org/licenses/mit-license.php). 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-exec", 3 | "description": "Grunt task for executing shell commands.", 4 | "version": "3.0.0", 5 | "homepage": "https://github.com/jharding/grunt-exec", 6 | "author": { 7 | "name": "Jake Harding", 8 | "email": "jacob.s.harding@gmail.com" 9 | }, 10 | "contributors": [ 11 | "Graeme Wicksted " 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/jharding/grunt-exec.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/jharding/grunt-exec/issues" 19 | }, 20 | "license": "MIT", 21 | "main": "Gruntfile.js", 22 | "engines": { 23 | "node": ">=0.8.0" 24 | }, 25 | "scripts": { 26 | "test": "node test/test.js" 27 | }, 28 | "peerDependencies": { 29 | "grunt": ">=0.4" 30 | }, 31 | "devDependencies": { 32 | "grunt": "^1.0.0", 33 | "grunt-contrib-jshint": "^1.1.0" 34 | }, 35 | "keywords": [ 36 | "grunt", 37 | "gruntplugin", 38 | "shell", 39 | "exec", 40 | "execute", 41 | "spawn" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /tasks/exec.js: -------------------------------------------------------------------------------- 1 | // grunt-exec 2 | // ========== 3 | // * GitHub: https://github.com/jharding/grunt-exec 4 | // * Original Copyright (c) 2012 Jake Harding 5 | // * Copyright (c) 2017 grunt-exec 6 | // * Licensed under the MIT license. 7 | 8 | // grunt-exe 2.0.0+ simulates the convenience of child_process.exec with the capabilities of child_process.spawn 9 | // this was done primarily to preserve colored output from applications such as npm 10 | // a lot of work was done to simulate the original behavior of both child_process.exec and grunt-exec 11 | // as such there may be unintended consequences so the major revision was bumped 12 | // a breaking change was made to the 'maxBuffer kill process' scenario so it is treated as an error and provides more detail (--verbose) 13 | // stdout and stderr buffering & maxBuffer constraints are removed entirely where possible 14 | // new features: detached (boolean), argv0 (override the executable name passed to the application), shell (boolean or string) 15 | // fd #s greater than 2 not yet supported (ipc piping) which is spawn-specific and very rarely required 16 | // TODO: support stdout and stderr Buffer objects passed in 17 | // TODO: stdin/stdout/stderr string as file name => open the file and read/write from it 18 | 19 | module.exports = function(grunt) { 20 | var cp = require('child_process') 21 | , f = require('util').format 22 | , _ = grunt.util._ 23 | , log = grunt.log 24 | , verbose = grunt.verbose; 25 | 26 | grunt.registerMultiTask('exec', 'Execute shell commands.', function() { 27 | 28 | var callbackErrors = false; 29 | 30 | var defaultOut = log.write; 31 | var defaultError = log.error; 32 | 33 | var defaultCallback = function(err, stdout, stderr) { 34 | if (err) { 35 | callbackErrors = true; 36 | defaultError('Error executing child process: ' + err.toString()); 37 | } 38 | }; 39 | 40 | var data = this.data 41 | , execOptions = data.options !== undefined ? data.options : {} 42 | , stdout = data.stdout !== undefined ? data.stdout : true 43 | , stderr = data.stderr !== undefined ? data.stderr : true 44 | , stdin = data.stdin !== undefined ? data.stdin : false 45 | , stdio = data.stdio 46 | , callback = _.isFunction(data.callback) ? data.callback : defaultCallback 47 | , callbackArgs = data.callbackArgs !== undefined ? data.callbackArgs : [] 48 | , sync = data.sync !== undefined ? data.sync : false 49 | , exitCodes = data.exitCode || data.exitCodes || 0 50 | , command 51 | , childProcess 52 | , args = [].slice.call(arguments, 0) 53 | , done = this.async(); 54 | 55 | // https://github.com/jharding/grunt-exec/pull/30 56 | exitCodes = _.isArray(exitCodes) ? exitCodes : [exitCodes]; 57 | 58 | // allow for command to be specified in either 59 | // 'command' or 'cmd' property, or as a string. 60 | command = data.command || data.cmd || (_.isString(data) && data); 61 | 62 | if (!command) { 63 | defaultError('Missing command property.'); 64 | return done(false); 65 | } 66 | 67 | if (data.cwd && _.isFunction(data.cwd)) { 68 | execOptions.cwd = data.cwd.apply(grunt, args); 69 | } else if (data.cwd) { 70 | execOptions.cwd = data.cwd; 71 | } 72 | 73 | // default to current process cwd 74 | execOptions.cwd = execOptions.cwd || process.cwd(); 75 | 76 | // manually supported (spawn vs exec) 77 | // 200*1024 is default maxBuffer of child_process.exec 78 | // NOTE: must be < require('buffer').kMaxLength or a RangeError will be triggered 79 | var maxBuffer = data.maxBuffer || execOptions.maxBuffer || (200*1024); 80 | 81 | // timeout manually supportted (spawn vs exec) 82 | execOptions.timeout = execOptions.timeout || data.timeout || 0; 83 | // kill signal manually supportted (spawn vs exec) 84 | execOptions.killSignal = execOptions.killSignal || data.killSignal || 'SIGTERM'; 85 | 86 | // support shell scripts like 'npm.cmd' by default (spawn vs exec) 87 | var shell = (typeof data.shell === 'undefined') ? execOptions.shell : data.shell; 88 | execOptions.shell = (typeof shell === 'string') ? shell : (shell === false ? false : true); 89 | 90 | // kept in data.encoding in case it is set to 'buffer' for final callback 91 | data.encoding = data.encoding || execOptions.encoding || 'utf8'; 92 | 93 | stdio = stdio || execOptions.stdio || undefined; 94 | if (stdio === 'inherit') { 95 | stdout = 'inherit'; 96 | stderr = 'inherit'; 97 | stdin = 'inherit'; 98 | } else if (stdio === 'pipe') { 99 | stdout = 'pipe'; 100 | stderr = 'pipe'; 101 | stdin = 'pipe'; 102 | } else if (stdio === 'ignore') { 103 | stdout = 'ignore'; 104 | stderr = 'ignore'; 105 | stdin = 'ignore'; 106 | } 107 | 108 | if (_.isFunction(command)) { 109 | command = command.apply(grunt, args); 110 | } 111 | 112 | if (!_.isString(command)) { 113 | defaultError('Command property must be a string.'); 114 | return done(false); 115 | } 116 | 117 | verbose.subhead(command); 118 | 119 | // manually parse args into array (spawn vs exec) 120 | var splitArgs = function(command) { 121 | // Regex Explanation Regex 122 | // --------------------------------------------------------------------- 123 | // 0-* spaces \s* 124 | // followed by either: 125 | // [NOT: a space, half quote, or double quote] 1-* times [^\s'"]+ 126 | // followed by either: 127 | // [half quote or double quote] in the future (?=['"]) 128 | // or 1-* spaces \s+ 129 | // or end of string $ 130 | // or half quote ['] 131 | // followed by 0-*: 132 | // [NOT: a backslash, or half quote] [^\\'] 133 | // or a backslash followed by any character \\. 134 | // followed by a half quote ['] 135 | // or double quote ["] 136 | // followed by 0-*: 137 | // [NOT: a backslash, or double quote] [^\\"] 138 | // or a backslash followed by any character \\. 139 | // followed by a double quote ["] 140 | // or end of string $ 141 | var pieces = command.match(/\s*([^\s'"]+(?:(?=['"])|\s+|$)|(?:(?:['](?:([^\\']|\\.)*)['])|(?:["](?:([^\\"]|\\.)*)["]))|$)/g); 142 | var args = []; 143 | var next = false; 144 | 145 | for (var i = 0; i < pieces.length; i++) { 146 | var piece = pieces[i]; 147 | if (piece.length > 0) { 148 | if (next || args.length === 0 || piece.charAt(0) === ' ') { 149 | args.push(piece.trim()); 150 | } else { 151 | var last = args.length - 1; 152 | args[last] = args[last] + piece.trim(); 153 | } 154 | next = piece.endsWith(' '); 155 | } 156 | } 157 | 158 | // NodeJS on Windows does not have this issue 159 | if (process.platform !== 'win32') { 160 | args = [args.join(' ')]; 161 | } 162 | 163 | return args; 164 | }; 165 | 166 | var args = splitArgs(command); 167 | command = args[0]; 168 | 169 | if (args.length > 1) { 170 | args = args.slice(1); 171 | } else { 172 | args = []; 173 | } 174 | 175 | // only save stdout and stderr if a custom callback is used 176 | var bufferedOutput = callback !== defaultCallback; 177 | 178 | // different stdio behavior (spawn vs exec) 179 | var stdioOption = function(value, integerValue, inheritValue) { 180 | return value === integerValue ? integerValue 181 | : value === 'inherit' ? inheritValue 182 | : bufferedOutput ? 'pipe' : value === 'pipe' || value === true || value === null || value === undefined ? 'pipe' 183 | : 'ignore'; /* value === false || value === 'ignore' */ 184 | } 185 | 186 | execOptions.stdio = [ 187 | stdioOption(stdin, 0, process.stdin), 188 | stdioOption(stdout, 1, process.stdout), 189 | stdioOption(stderr, 2, process.stderr) 190 | ]; 191 | 192 | var encoding = data.encoding; 193 | var bufferedStdOut = bufferedOutput && execOptions.stdio[1] === 'pipe'; 194 | var bufferedStdErr = bufferedOutput && execOptions.stdio[2] === 'pipe'; 195 | var stdOutLength = 0; 196 | var stdErrLength = 0; 197 | var stdOutBuffers = []; 198 | var stdErrBuffers = []; 199 | 200 | if (bufferedOutput && !Buffer.isEncoding(encoding)) { 201 | if (encoding === 'buffer') { 202 | encoding = 'binary'; 203 | } else { 204 | grunt.fail.fail('Encoding "' + encoding + '" is not a supported character encoding!'); 205 | done(false); 206 | } 207 | } 208 | 209 | if (verbose) { 210 | stdioDescriptions = execOptions.stdio.slice(); 211 | for (var i = 0; i < stdioDescriptions.length; i++) { 212 | stdioDescription = stdioDescriptions[i]; 213 | if (stdioDescription === process.stdin) { 214 | stdioDescriptions[i] = 'process.stdin'; 215 | } else if (stdioDescription === process.stdout) { 216 | stdioDescriptions[i] = 'process.stdout'; 217 | } else if (stdioDescription === process.stderr) { 218 | stdioDescriptions[i] = 'process.stderr'; 219 | } 220 | } 221 | 222 | verbose.writeln('buffer : ' + (bufferedOutput ? 223 | (bufferedStdOut ? 'stdout=enabled' : 'stdout=disabled') 224 | + ';' + 225 | (bufferedStdErr ? 'stderr=enabled' : 'stderr=disabled') 226 | + ';' + 227 | 'max size=' + maxBuffer 228 | : 'disabled')); 229 | verbose.writeln('timeout : ' + (execOptions.timeout === 0 ? 'infinite' : '' + execOptions.timeout + 'ms')); 230 | verbose.writeln('killSig : ' + execOptions.killSignal); 231 | verbose.writeln('shell : ' + execOptions.shell); 232 | verbose.writeln('command : ' + command); 233 | verbose.writeln('args : [' + args.join(',') + ']'); 234 | verbose.writeln('stdio : [' + stdioDescriptions.join(',') + ']'); 235 | verbose.writeln('cwd : ' + execOptions.cwd); 236 | //verbose.writeln('env path : ' + process.env.PATH); 237 | verbose.writeln('exitcodes:', exitCodes.join(',')); 238 | } 239 | 240 | if (sync) 241 | { 242 | childProcess = cp.spawnSync(command, args, execOptions); 243 | } 244 | else { 245 | childProcess = cp.spawn(command, args, execOptions); 246 | } 247 | 248 | if (verbose) { 249 | verbose.writeln('pid : ' + childProcess.pid); 250 | } 251 | 252 | var killChild = function (reason) { 253 | defaultError(reason); 254 | process.kill(childProcess.pid, execOptions.killSignal); 255 | //childProcess.kill(execOptions.killSignal); 256 | done(false); // unlike exec, this will indicate an error - after all, it did kill the process 257 | }; 258 | 259 | if (execOptions.timeout !== 0) { 260 | var timeoutProcess = function() { 261 | killChild('Timeout child process'); 262 | }; 263 | setInterval(timeoutProcess, execOptions.timeout); 264 | } 265 | 266 | var writeStdOutBuffer = function(d) { 267 | var b = !Buffer.isBuffer(d) ? new Buffer(d.toString(encoding)) : d; 268 | if (stdOutLength + b.length > maxBuffer) { 269 | if (verbose) { 270 | verbose.writeln("EXCEEDING MAX BUFFER: stdOut " + stdOutLength + " buffer " + b.length + " maxBuffer " + maxBuffer); 271 | } 272 | killChild("stdout maxBuffer exceeded"); 273 | } else { 274 | stdOutLength += b.length; 275 | stdOutBuffers.push(b); 276 | } 277 | 278 | // default piping behavior 279 | if (stdout !== false && data.encoding !== 'buffer') { 280 | defaultOut(d); 281 | } 282 | }; 283 | 284 | var writeStdErrBuffer = function(d) { 285 | var b = !Buffer.isBuffer(d) ? new Buffer(d.toString(encoding)) : d; 286 | if (stdErrLength + b.length > maxBuffer) { 287 | if (verbose) { 288 | verbose.writeln("EXCEEDING MAX BUFFER: stdErr " + stdErrLength + " buffer " + b.length + " maxBuffer " + maxBuffer); 289 | } 290 | killChild("stderr maxBuffer exceeded"); 291 | } else { 292 | stdErrLength += b.length; 293 | stdErrBuffers.push(b); 294 | } 295 | 296 | // default piping behavior 297 | if (stderr !== false && data.encoding !== 'buffer') { 298 | defaultError(d); 299 | } 300 | }; 301 | 302 | if (execOptions.stdio[1] === 'pipe') { 303 | var pipeOut = bufferedStdOut ? writeStdOutBuffer : defaultOut; 304 | // Asynchronous + Synchronous Support 305 | if (sync) { pipeOut(childProcess.stdout); } 306 | else { childProcess.stdout.on('data', function (d) { pipeOut(d); }); } 307 | } 308 | 309 | if (execOptions.stdio[2] === 'pipe') { 310 | var pipeErr = bufferedStdErr ? writeStdErrBuffer : defaultError; 311 | // Asynchronous + Synchronous Support 312 | if (sync) { pipeOut(childProcess.stderr); } 313 | else { childProcess.stderr.on('data', function (d) { pipeErr(d); }); } 314 | } 315 | 316 | // Catches failing to execute the command at all (eg spawn ENOENT), 317 | // since in that case an 'exit' event will not be emitted. 318 | // Asynchronous + Synchronous Support 319 | if (sync) { 320 | if (childProcess.error != null) 321 | { 322 | defaultError(f('Failed with: %s', error.message)); 323 | done(false); 324 | } 325 | } 326 | else { 327 | childProcess.on('error', function (err) { 328 | defaultError(f('Failed with: %s', err)); 329 | done(false); 330 | }); 331 | } 332 | 333 | // Exit Function (used for process exit callback / exit function) 334 | var exitFunc = function (code) { 335 | if (callbackErrors) { 336 | defaultError('Node returned an error for this child process'); 337 | return done(false); 338 | } 339 | 340 | var stdOutBuffer = undefined; 341 | var stdErrBuffer = undefined; 342 | 343 | if (bufferedStdOut) { 344 | stdOutBuffer = new Buffer(stdOutLength); 345 | var offset = 0; 346 | for (var i = 0; i < stdOutBuffers.length; i++) { 347 | var buf = stdOutBuffers[i]; 348 | buf.copy(stdOutBuffer, offset); 349 | offset += buf.length; 350 | } 351 | 352 | if (data.encoding !== 'buffer') { 353 | stdOutBuffer = stdOutBuffer.toString(encoding); 354 | } 355 | } 356 | 357 | if (bufferedStdErr) { 358 | stdErrBuffer = new Buffer(stdErrLength); 359 | var offset = 0; 360 | for (var i = 0; i < stdErrBuffers.length; i++) { 361 | var buf = stdErrBuffers[i]; 362 | buf.copy(stdErrBuffer, offset); 363 | offset += buf.length; 364 | } 365 | 366 | if (data.encoding !== 'buffer') { 367 | stdErrBuffer = stdErrBuffer.toString(encoding); 368 | } 369 | } 370 | 371 | if (exitCodes.indexOf(code) < 0) { 372 | defaultError(f('Exited with code: %d.', code)); 373 | if (callback) { 374 | var err = new Error(f('Process exited with code %d.', code)); 375 | err.code = code; 376 | 377 | callback(err, stdOutBuffer, stdErrBuffer, callbackArgs); 378 | } 379 | return done(false); 380 | } 381 | 382 | verbose.ok(f('Exited with code: %d.', code)); 383 | 384 | if (callback) { 385 | callback(null, stdOutBuffer, stdErrBuffer, callbackArgs); 386 | } 387 | 388 | done(); 389 | } 390 | 391 | // Asynchronous + Synchronous Support 392 | if (sync) { 393 | exitFunc(childProcess.status); 394 | } 395 | else { 396 | childProcess.on('exit', exitFunc); 397 | } 398 | 399 | }); 400 | }; 401 | -------------------------------------------------------------------------------- /test/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | exec: { 4 | test1: { 5 | cmd: 'echo bruce willis was dead> test1' 6 | } 7 | , test2: { 8 | cmd: function() { return 'echo grunt@' + this.version + '> test2'; } 9 | } 10 | , test3: { 11 | cmd: function(answerToLife, tacoThoughts) { 12 | var text = [ 13 | 'the answer to life is ' + answerToLife 14 | , 'thoughts on tacos? ' + tacoThoughts 15 | ].join(', '); 16 | 17 | return 'echo ' + text + '> test3'; 18 | } 19 | } 20 | , test4: { 21 | cmd: function(){ 22 | return 'echo you can use callback, and error, stdout, stderr can be' + 23 | ' used as arguments'; 24 | } 25 | , callback: function(error, stdout, stderr){ 26 | var fs = require('fs') 27 | , path = require('path') 28 | , outputPath = path.resolve(process.cwd(), 'test4'); 29 | 30 | console.log('outputPath : ' + outputPath); 31 | console.log('stderr: ' + stderr); 32 | 33 | try { 34 | fs.writeFileSync(outputPath, stdout, 'utf-8'); 35 | } catch (err) { 36 | console.error(err.stack); 37 | } 38 | } 39 | } 40 | , test5: { 41 | cmd: 'node -e "process.exit(8);"' 42 | , exitCodes: 8 43 | , shell: true 44 | } 45 | , test6: { 46 | cmd: 'node -e "process.exit(9);"' 47 | , exitCodes: [8, 9] 48 | , shell: true 49 | } 50 | , test7: 'echo you do not even need an object> test7' 51 | , test8: { 52 | cmd: 'node -e "console.log(\'synchronous echo 1\'); process.exit(0);"', 53 | sync: true, 54 | shell: true 55 | } 56 | , test9: { 57 | cmd: 'node -e "setTimeout(function () { console.log(\'synchronous echo 2, wait 3 seconds\'); process.exit(0); }, 3000);"', 58 | sync: true, 59 | shell: true 60 | } 61 | , test10: { 62 | cmd: 'node -e "console.log(\'synchronous echo 3\'); process.exit(0);"', 63 | sync: true, 64 | shell: true 65 | } 66 | } 67 | }); 68 | 69 | grunt.loadTasks('../tasks'); 70 | }; 71 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var grunt = require('grunt') 2 | , lf = grunt.util.linefeed 3 | , path = require('path') 4 | , fs = require('fs') 5 | , assert = require('assert') 6 | , testDir = path.join(process.cwd(), 'test') 7 | , opts = { gruntfile: path.join(testDir, 'Gruntfile.js') } 8 | , tasks = [ 9 | 'exec:test1' 10 | , 'exec:test2' 11 | , 'exec:test3:42:love' 12 | , 'exec:test4' 13 | , 'exec:test5' 14 | , 'exec:test6' 15 | , 'exec:test7' 16 | , 'exec:test8' 17 | , 'exec:test9' 18 | , 'exec:test10' 19 | ]; 20 | 21 | grunt.tasks(tasks, opts, function() { 22 | var tests = [ 23 | { name: 'test1', expected: 'bruce willis was dead' + lf } 24 | , { name: 'test2' , expected: 'grunt@' + grunt.version + lf } 25 | , { 26 | name: 'test3' 27 | , expected: [ 28 | 'the answer to life is 42', 'thoughts on tacos? love' 29 | ].join(', ') + lf 30 | } 31 | , { 32 | name: 'test4' 33 | , expected:'you can use callback, and error, stdout, stderr can be' + 34 | ' used as arguments' + lf 35 | } 36 | , { name: 'test7', expected: 'you do not even need an object' + lf } 37 | ] 38 | , outputPath; 39 | 40 | tests.forEach(function(test) { 41 | outputPath = path.join(testDir, test.name); 42 | assert.equal(fs.readFileSync(outputPath, 'utf8'), test.expected); 43 | 44 | // clean up 45 | fs.unlinkSync(outputPath); 46 | 47 | grunt.log.ok(test.name +' passed'); 48 | }); 49 | }); 50 | --------------------------------------------------------------------------------