grunt-exec
Version:
Grunt task for executing shell commands.
401 lines (343 loc) • 14.4 kB
JavaScript
// grunt-exec
// ==========
// * GitHub: https://github.com/jharding/grunt-exec
// * Original Copyright (c) 2012 Jake Harding
// * Copyright (c) 2017 grunt-exec
// * Licensed under the MIT license.
// grunt-exe 2.0.0+ simulates the convenience of child_process.exec with the capabilities of child_process.spawn
// this was done primarily to preserve colored output from applications such as npm
// a lot of work was done to simulate the original behavior of both child_process.exec and grunt-exec
// as such there may be unintended consequences so the major revision was bumped
// a breaking change was made to the 'maxBuffer kill process' scenario so it is treated as an error and provides more detail (--verbose)
// stdout and stderr buffering & maxBuffer constraints are removed entirely where possible
// new features: detached (boolean), argv0 (override the executable name passed to the application), shell (boolean or string)
// fd #s greater than 2 not yet supported (ipc piping) which is spawn-specific and very rarely required
// TODO: support stdout and stderr Buffer objects passed in
// TODO: stdin/stdout/stderr string as file name => open the file and read/write from it
module.exports = function(grunt) {
var cp = require('child_process')
, f = require('util').format
, _ = grunt.util._
, log = grunt.log
, verbose = grunt.verbose;
grunt.registerMultiTask('exec', 'Execute shell commands.', function() {
var callbackErrors = false;
var defaultOut = log.write;
var defaultError = log.error;
var defaultCallback = function(err, stdout, stderr) {
if (err) {
callbackErrors = true;
defaultError('Error executing child process: ' + err.toString());
}
};
var data = this.data
, execOptions = data.options !== undefined ? data.options : {}
, stdout = data.stdout !== undefined ? data.stdout : true
, stderr = data.stderr !== undefined ? data.stderr : true
, stdin = data.stdin !== undefined ? data.stdin : false
, stdio = data.stdio
, callback = _.isFunction(data.callback) ? data.callback : defaultCallback
, callbackArgs = data.callbackArgs !== undefined ? data.callbackArgs : []
, sync = data.sync !== undefined ? data.sync : false
, exitCodes = data.exitCode || data.exitCodes || 0
, command
, childProcess
, args = [].slice.call(arguments, 0)
, done = this.async();
// https://github.com/jharding/grunt-exec/pull/30
exitCodes = _.isArray(exitCodes) ? exitCodes : [exitCodes];
// allow for command to be specified in either
// 'command' or 'cmd' property, or as a string.
command = data.command || data.cmd || (_.isString(data) && data);
if (!command) {
defaultError('Missing command property.');
return done(false);
}
if (data.cwd && _.isFunction(data.cwd)) {
execOptions.cwd = data.cwd.apply(grunt, args);
} else if (data.cwd) {
execOptions.cwd = data.cwd;
}
// default to current process cwd
execOptions.cwd = execOptions.cwd || process.cwd();
// manually supported (spawn vs exec)
// 200*1024 is default maxBuffer of child_process.exec
// NOTE: must be < require('buffer').kMaxLength or a RangeError will be triggered
var maxBuffer = data.maxBuffer || execOptions.maxBuffer || (200*1024);
// timeout manually supportted (spawn vs exec)
execOptions.timeout = execOptions.timeout || data.timeout || 0;
// kill signal manually supportted (spawn vs exec)
execOptions.killSignal = execOptions.killSignal || data.killSignal || 'SIGTERM';
// support shell scripts like 'npm.cmd' by default (spawn vs exec)
var shell = (typeof data.shell === 'undefined') ? execOptions.shell : data.shell;
execOptions.shell = (typeof shell === 'string') ? shell : (shell === false ? false : true);
// kept in data.encoding in case it is set to 'buffer' for final callback
data.encoding = data.encoding || execOptions.encoding || 'utf8';
stdio = stdio || execOptions.stdio || undefined;
if (stdio === 'inherit') {
stdout = 'inherit';
stderr = 'inherit';
stdin = 'inherit';
} else if (stdio === 'pipe') {
stdout = 'pipe';
stderr = 'pipe';
stdin = 'pipe';
} else if (stdio === 'ignore') {
stdout = 'ignore';
stderr = 'ignore';
stdin = 'ignore';
}
if (_.isFunction(command)) {
command = command.apply(grunt, args);
}
if (!_.isString(command)) {
defaultError('Command property must be a string.');
return done(false);
}
verbose.subhead(command);
// manually parse args into array (spawn vs exec)
var splitArgs = function(command) {
// Regex Explanation Regex
// ---------------------------------------------------------------------
// 0-* spaces \s*
// followed by either:
// [NOT: a space, half quote, or double quote] 1-* times [^\s'"]+
// followed by either:
// [half quote or double quote] in the future (?=['"])
// or 1-* spaces \s+
// or end of string $
// or half quote [']
// followed by 0-*:
// [NOT: a backslash, or half quote] [^\\']
// or a backslash followed by any character \\.
// followed by a half quote [']
// or double quote ["]
// followed by 0-*:
// [NOT: a backslash, or double quote] [^\\"]
// or a backslash followed by any character \\.
// followed by a double quote ["]
// or end of string $
var pieces = command.match(/\s*([^\s'"]+(?:(?=['"])|\s+|$)|(?:(?:['](?:([^\\']|\\.)*)['])|(?:["](?:([^\\"]|\\.)*)["]))|$)/g);
var args = [];
var next = false;
for (var i = 0; i < pieces.length; i++) {
var piece = pieces[i];
if (piece.length > 0) {
if (next || args.length === 0 || piece.charAt(0) === ' ') {
args.push(piece.trim());
} else {
var last = args.length - 1;
args[last] = args[last] + piece.trim();
}
next = piece.endsWith(' ');
}
}
// NodeJS on Windows does not have this issue
if (process.platform !== 'win32') {
args = [args.join(' ')];
}
return args;
};
var args = splitArgs(command);
command = args[0];
if (args.length > 1) {
args = args.slice(1);
} else {
args = [];
}
// only save stdout and stderr if a custom callback is used
var bufferedOutput = callback !== defaultCallback;
// different stdio behavior (spawn vs exec)
var stdioOption = function(value, integerValue, inheritValue) {
return value === integerValue ? integerValue
: value === 'inherit' ? inheritValue
: bufferedOutput ? 'pipe' : value === 'pipe' || value === true || value === null || value === undefined ? 'pipe'
: 'ignore'; /* value === false || value === 'ignore' */
}
execOptions.stdio = [
stdioOption(stdin, 0, process.stdin),
stdioOption(stdout, 1, process.stdout),
stdioOption(stderr, 2, process.stderr)
];
var encoding = data.encoding;
var bufferedStdOut = bufferedOutput && execOptions.stdio[1] === 'pipe';
var bufferedStdErr = bufferedOutput && execOptions.stdio[2] === 'pipe';
var stdOutLength = 0;
var stdErrLength = 0;
var stdOutBuffers = [];
var stdErrBuffers = [];
if (bufferedOutput && !Buffer.isEncoding(encoding)) {
if (encoding === 'buffer') {
encoding = 'binary';
} else {
grunt.fail.fail('Encoding "' + encoding + '" is not a supported character encoding!');
done(false);
}
}
if (verbose) {
stdioDescriptions = execOptions.stdio.slice();
for (var i = 0; i < stdioDescriptions.length; i++) {
stdioDescription = stdioDescriptions[i];
if (stdioDescription === process.stdin) {
stdioDescriptions[i] = 'process.stdin';
} else if (stdioDescription === process.stdout) {
stdioDescriptions[i] = 'process.stdout';
} else if (stdioDescription === process.stderr) {
stdioDescriptions[i] = 'process.stderr';
}
}
verbose.writeln('buffer : ' + (bufferedOutput ?
(bufferedStdOut ? 'stdout=enabled' : 'stdout=disabled')
+ ';' +
(bufferedStdErr ? 'stderr=enabled' : 'stderr=disabled')
+ ';' +
'max size=' + maxBuffer
: 'disabled'));
verbose.writeln('timeout : ' + (execOptions.timeout === 0 ? 'infinite' : '' + execOptions.timeout + 'ms'));
verbose.writeln('killSig : ' + execOptions.killSignal);
verbose.writeln('shell : ' + execOptions.shell);
verbose.writeln('command : ' + command);
verbose.writeln('args : [' + args.join(',') + ']');
verbose.writeln('stdio : [' + stdioDescriptions.join(',') + ']');
verbose.writeln('cwd : ' + execOptions.cwd);
//verbose.writeln('env path : ' + process.env.PATH);
verbose.writeln('exitcodes:', exitCodes.join(','));
}
if (sync)
{
childProcess = cp.spawnSync(command, args, execOptions);
}
else {
childProcess = cp.spawn(command, args, execOptions);
}
if (verbose) {
verbose.writeln('pid : ' + childProcess.pid);
}
var killChild = function (reason) {
defaultError(reason);
process.kill(childProcess.pid, execOptions.killSignal);
//childProcess.kill(execOptions.killSignal);
done(false); // unlike exec, this will indicate an error - after all, it did kill the process
};
if (execOptions.timeout !== 0) {
var timeoutProcess = function() {
killChild('Timeout child process');
};
setInterval(timeoutProcess, execOptions.timeout);
}
var writeStdOutBuffer = function(d) {
var b = !Buffer.isBuffer(d) ? new Buffer(d.toString(encoding)) : d;
if (stdOutLength + b.length > maxBuffer) {
if (verbose) {
verbose.writeln("EXCEEDING MAX BUFFER: stdOut " + stdOutLength + " buffer " + b.length + " maxBuffer " + maxBuffer);
}
killChild("stdout maxBuffer exceeded");
} else {
stdOutLength += b.length;
stdOutBuffers.push(b);
}
// default piping behavior
if (stdout !== false && data.encoding !== 'buffer') {
defaultOut(d);
}
};
var writeStdErrBuffer = function(d) {
var b = !Buffer.isBuffer(d) ? new Buffer(d.toString(encoding)) : d;
if (stdErrLength + b.length > maxBuffer) {
if (verbose) {
verbose.writeln("EXCEEDING MAX BUFFER: stdErr " + stdErrLength + " buffer " + b.length + " maxBuffer " + maxBuffer);
}
killChild("stderr maxBuffer exceeded");
} else {
stdErrLength += b.length;
stdErrBuffers.push(b);
}
// default piping behavior
if (stderr !== false && data.encoding !== 'buffer') {
defaultError(d);
}
};
if (execOptions.stdio[1] === 'pipe') {
var pipeOut = bufferedStdOut ? writeStdOutBuffer : defaultOut;
// Asynchronous + Synchronous Support
if (sync) { pipeOut(childProcess.stdout); }
else { childProcess.stdout.on('data', function (d) { pipeOut(d); }); }
}
if (execOptions.stdio[2] === 'pipe') {
var pipeErr = bufferedStdErr ? writeStdErrBuffer : defaultError;
// Asynchronous + Synchronous Support
if (sync) { pipeOut(childProcess.stderr); }
else { childProcess.stderr.on('data', function (d) { pipeErr(d); }); }
}
// Catches failing to execute the command at all (eg spawn ENOENT),
// since in that case an 'exit' event will not be emitted.
// Asynchronous + Synchronous Support
if (sync) {
if (childProcess.error != null)
{
defaultError(f('Failed with: %s', error.message));
done(false);
}
}
else {
childProcess.on('error', function (err) {
defaultError(f('Failed with: %s', err));
done(false);
});
}
// Exit Function (used for process exit callback / exit function)
var exitFunc = function (code) {
if (callbackErrors) {
defaultError('Node returned an error for this child process');
return done(false);
}
var stdOutBuffer = undefined;
var stdErrBuffer = undefined;
if (bufferedStdOut) {
stdOutBuffer = new Buffer(stdOutLength);
var offset = 0;
for (var i = 0; i < stdOutBuffers.length; i++) {
var buf = stdOutBuffers[i];
buf.copy(stdOutBuffer, offset);
offset += buf.length;
}
if (data.encoding !== 'buffer') {
stdOutBuffer = stdOutBuffer.toString(encoding);
}
}
if (bufferedStdErr) {
stdErrBuffer = new Buffer(stdErrLength);
var offset = 0;
for (var i = 0; i < stdErrBuffers.length; i++) {
var buf = stdErrBuffers[i];
buf.copy(stdErrBuffer, offset);
offset += buf.length;
}
if (data.encoding !== 'buffer') {
stdErrBuffer = stdErrBuffer.toString(encoding);
}
}
if (exitCodes.indexOf(code) < 0) {
defaultError(f('Exited with code: %d.', code));
if (callback) {
var err = new Error(f('Process exited with code %d.', code));
err.code = code;
callback(err, stdOutBuffer, stdErrBuffer, callbackArgs);
}
return done(false);
}
verbose.ok(f('Exited with code: %d.', code));
if (callback) {
callback(null, stdOutBuffer, stdErrBuffer, callbackArgs);
}
done();
}
// Asynchronous + Synchronous Support
if (sync) {
exitFunc(childProcess.status);
}
else {
childProcess.on('exit', exitFunc);
}
});
};