nscript
Version:
Javascript powered shell scripts
286 lines (250 loc) • 8.16 kB
JavaScript
var shell = require('./shell.js');
var spawn = require('./spawn.js');
var fs = require('fs');
var path = require('path');
var buffer = require('buffer');
var stream = require('stream');
var toArray = require('./utils.js').toArray;
var utils = require('./utils.js');
var extend = utils.extend;
var Fiber = require('fibers');
var Future = require('fibers/future');
var command = module.exports = function() {
/*
Basic command structure
*/
var cmd = function run(/* args */) {
return cmd.argsSplat(arguments).spawn().wait();
};
// TODO: remove bound args?
cmd._args = cmd.boundArgs = toArray(arguments);
cmd._options = {
throwOnError: true,
silent: false
};
cmd.options = function(newOptions) {
var clone = cmd.clone();
extend(clone._options, newOptions);
return clone;
};
cmd.args = function(/* args */) {
var clone = cmd.clone();
clone._args = clone._args.concat(toArray(arguments));
return clone;
};
cmd.argsSplat = function(args) {
return cmd.args.apply(null, toArray(args));
};
cmd.clone = function() {
var clone = command();
clone._args = [].concat(cmd._args);
clone._options = extend({}, cmd._options);
return clone;
};
cmd.run = cmd; //e.g. silent()('ls') === silent().run('ls');
/*
Options and convenience methods
*/
cmd.silent = function() {
utils.expectArgs(arguments, 0);
return cmd.options({silent: true});
};
cmd.relax = function() {
utils.expectArgs(arguments, 0);
return cmd.options({throwOnError: false});
};
cmd.env = function(key, value) {
var newEnv = cmd._options.env || {};
if (arguments.length == 2)
newEnv[key] = value;
else
utils.extend(newEnv, /*object*/ key);
return cmd.options({env: newEnv});
};
cmd.code = function(/* args */) {
return cmd.argsSplat(arguments).spawn().code();
};
cmd.test = function(/* args */) {
return cmd.argsSplat(arguments).spawn().test();
};
cmd.get = function(/* args */) {
return cmd.argsSplat(arguments).spawn().get();
};
cmd.getError = function(/* args */) {
return cmd.argsSplat(arguments).spawn().getError();
};
cmd.getLines = function(/* args */) {
return cmd.argsSplat(arguments).spawn().getLines();
};
/*
Process input handling
*/
cmd.input = function(input) {
utils.expectArgs(arguments, 1);
if (cmd._options.stdin)
throw new Error("Input for the next process invocation has been set already!");
if (typeof(input) == "number" || (input && input.pipe))
return cmd.options({stdin: input});
// FEATURE: if function, evaluate lazy on each read, so that 'yes' script
// http://stackoverflow.com/questions/16038705/how-to-wrap-a-buffer-as-a-stream2-readable-stream#16039177
var bufferStream = new stream.Readable();
bufferStream._read = function () {
this.push('' + input);
this.push(null);
};
return cmd.options({stdin: bufferStream});
};
cmd.read = function(filename) {
utils.expectArgs(arguments, 1);
if (cmd._options.stdin)
throw new Error("Input for the next process invocation has been set already!");
filename = spawn.expandArgument(filename, false);
if (shell.verbose())
console.warn('< ' + filename);
return cmd.options({stdin: fs.openSync(filename, 'r')});
};
/*
Process output handling
*/
cmd.pipe = function(/* args */) {
utils.expectArgs(arguments, -1);
return cmd.spawn().pipe.apply(null, arguments);
};
cmd.write = function(filename) {
utils.expectArgs(arguments, 1);
// returns exit code
return cmd.spawn().write(filename).wait();
};
cmd.append = function(filename) {
utils.expectArgs(arguments, 1);
// returns exit code
return cmd.spawn().append(filename).wait();
};
cmd.writeError = function(filename) {
utils.expectArgs(arguments, 1);
// returns streams for convenient chaining
return cmd.spawn().writeError(filename);
};
cmd.appendError = function(filename) {
utils.expectArgs(arguments, 1);
// returns streams for convenient chaining
return cmd.spawn().appendError(filename);
};
cmd.spawn = function() {
utils.expectArgs(arguments, 0);
var streamsConsumed = [null, false, false];
var options = extend({}, cmd._options); //clone
var self = {};
var commandAndArgs = cmd._args;
var child = spawn.spawn(commandAndArgs, options);
process.nextTick(function() {
//make sure the output streams go somewhere
for(var i = 1; i < 3; i++) if (!streamsConsumed[i]) {
if (cmd._options.silent)
child.stdio[i].resume();
else {
child.stdio[i].pipe(i == 1 ? process.stdout : process.stderr);
}
}
});
function outputToFile(streamIdx, flags, filename) {
utils.expectArgs(arguments, 3);
streamsConsumed[streamIdx] = true;
if (shell.verbose())
console.warn((streamIdx == 2 ? '2' : '') + (flags == 'w' ? '>' : '>>') + ' ' + filename);
var out = fs.createWriteStream(filename, {flags: flags});
child.stdio[streamIdx].pipe(out);
return self;
}
function pipeHelper(streamIdx, cmd) {
utils.expectArgs(arguments, -1);
streamsConsumed[streamIdx] = true;
if (!cmd.boundArgs)
cmd = command(cmd);
// Set the input of the target command to the output of this command
// And start the command, and return the streams
return cmd.input(child.stdio[streamIdx]).argsSplat(toArray(arguments).splice(2)).spawn();
}
function getHelper(streamIdx) {
utils.expectArgs(arguments, 1);
streamsConsumed[streamIdx] = true;
var buf = "";
child.stdio[streamIdx].on('data', function(d) {
buf += d;
});
self.wait();
return buf;
}
return extend(self, {
pid : child.pid,
process : child,
get: getHelper.bind(self, 1),
getError: getHelper.bind(self, 2),
getLines: function() {
return getHelper(1).split(/\r?\n/g);
},
pipe: pipeHelper.bind(self, 1),
pipeError: pipeHelper.bind(self, 2),
write: outputToFile.bind(self, 1,'w'),
append: outputToFile.bind(self, 1,'a'),
writeError: outputToFile.bind(self, 2,'w'),
appendError: outputToFile.bind(self, 2,'a'),
wait: function() {
utils.expectArgs(arguments, 0);
if (!Fiber.current)
throw utils.buildError('NotInNscriptFunction', "Could not spawn '" + commandAndArgs.join(" ") + "; the current program stack is not wrapped in an nscript function. Please wrap your current invocation in '\n<shell.>nscript(function() {\n\t<your commands>\n}\n'");
var future = new Future();
var spawnError;
child.on('error', function(err) {
spawnError = err;
});
child.on('close', function(exitCode) {
future.return(exitCode);
});
var status = shell.lastExitCode = future.wait();
if (options.throwOnError) {
if (status > 0)
throw utils.buildError('NonZeroExitError', "Command '" + commandAndArgs.join(" ") + "' failed with status: " + status);
else if (status < 0)
throw utils.buildError('FailedToStartError', "Command '" + commandAndArgs.join(" ") + "' failed to start, got: '" + spawnError.message + "'. \nDoes '" + commandAndArgs[0] + "' exist (in the current dir or on PATH), is it readable and marked as executable?");
}
return status;
},
test: function() { return self.code() === 0; },
code: function() {
options.throwOnError = false;
return self.wait();
},
onClose: function(cb) {
utils.expectArgs(arguments, 1);
child.on('close', cb);
return self;
},
onOut: function(cb) { // TODO: rename to onData & onErrorData?
utils.expectArgs(arguments, 1);
child.stdout.on('data', cb);
return self;
},
// FEATURE: onLine, like onOut, but for each line
onError: function(cb) {
utils.expectArgs(arguments, 1);
child.stderr.on('data', cb);
return self;
},
toString: function() {
return "[Process '" + args + "']";
}
});
};
cmd.detach = function(/* args */) {
var child = cmd.options({detached: true}).argsSplat(arguments).spawn().process;
child.unref(); //do not wait for child to exit
console.warn("[+] " + child.pid);
return child.pid;
};
cmd.toString = function() {
return "[Command " + cmd._args.join(" ") + "]";
};
//FEATURE: cmd.background: run stuff in background, but kill it when the process exits
return cmd;
};