331 lines
8.7 KiB
JavaScript
331 lines
8.7 KiB
JavaScript
|
// jshint -W053
|
||
|
// Ignore warning about 'new String()'
|
||
|
'use strict';
|
||
|
|
||
|
var os = require('os');
|
||
|
var fs = require('fs');
|
||
|
var glob = require('glob');
|
||
|
var shell = require('..');
|
||
|
var _to = require('./to');
|
||
|
var _toEnd = require('./toEnd');
|
||
|
|
||
|
var DEFAULT_ERROR_CODE = 1;
|
||
|
|
||
|
// Module globals
|
||
|
var config = {
|
||
|
silent: false,
|
||
|
fatal: false,
|
||
|
verbose: false,
|
||
|
noglob: false,
|
||
|
globOptions: {},
|
||
|
maxdepth: 255
|
||
|
};
|
||
|
exports.config = config;
|
||
|
|
||
|
var state = {
|
||
|
error: null,
|
||
|
errorCode: 0,
|
||
|
currentCmd: 'shell.js',
|
||
|
tempDir: null
|
||
|
};
|
||
|
exports.state = state;
|
||
|
|
||
|
delete process.env.OLDPWD; // initially, there's no previous directory
|
||
|
|
||
|
var platform = os.type().match(/^Win/) ? 'win' : 'unix';
|
||
|
exports.platform = platform;
|
||
|
|
||
|
function log() {
|
||
|
if (!config.silent)
|
||
|
console.error.apply(console, arguments);
|
||
|
}
|
||
|
exports.log = log;
|
||
|
|
||
|
// Shows error message. Throws if config.fatal is true
|
||
|
function error(msg, _code, _continue) {
|
||
|
if (typeof _code === 'boolean') {
|
||
|
_continue = _code;
|
||
|
_code = DEFAULT_ERROR_CODE;
|
||
|
}
|
||
|
if (typeof _code !== 'number')
|
||
|
_code = DEFAULT_ERROR_CODE;
|
||
|
|
||
|
if (state.errorCode === 0)
|
||
|
state.errorCode = _code;
|
||
|
|
||
|
if (state.error === null)
|
||
|
state.error = '';
|
||
|
var log_entry = state.currentCmd + ': ' + msg;
|
||
|
if (state.error === '')
|
||
|
state.error = log_entry;
|
||
|
else
|
||
|
state.error += '\n' + log_entry;
|
||
|
|
||
|
if(config.fatal)
|
||
|
throw new Error(log_entry);
|
||
|
|
||
|
if (msg.length > 0)
|
||
|
log(log_entry);
|
||
|
|
||
|
if(!_continue) {
|
||
|
throw {
|
||
|
msg: 'earlyExit',
|
||
|
retValue: (new ShellString('', state.error, state.errorCode))
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
exports.error = error;
|
||
|
|
||
|
//@
|
||
|
//@ ### ShellString(str)
|
||
|
//@
|
||
|
//@ Examples:
|
||
|
//@
|
||
|
//@ ```javascript
|
||
|
//@ var foo = ShellString('hello world');
|
||
|
//@ ```
|
||
|
//@
|
||
|
//@ Turns a regular string into a string-like object similar to what each
|
||
|
//@ command returns. This has special methods, like `.to()` and `.toEnd()`
|
||
|
var ShellString = function (stdout, stderr, code) {
|
||
|
var that;
|
||
|
if (stdout instanceof Array) {
|
||
|
that = stdout;
|
||
|
that.stdout = stdout.join('\n');
|
||
|
if (stdout.length > 0) that.stdout += '\n';
|
||
|
} else {
|
||
|
that = new String(stdout);
|
||
|
that.stdout = stdout;
|
||
|
}
|
||
|
that.stderr = stderr;
|
||
|
that.code = code;
|
||
|
that.to = function() {wrap('to', _to, {idx: 1}).apply(that.stdout, arguments); return that;};
|
||
|
that.toEnd = function() {wrap('toEnd', _toEnd, {idx: 1}).apply(that.stdout, arguments); return that;};
|
||
|
['cat', 'head', 'sed', 'sort', 'tail', 'grep', 'exec'].forEach(function (cmd) {
|
||
|
that[cmd] = function() {return shell[cmd].apply(that.stdout, arguments);};
|
||
|
});
|
||
|
return that;
|
||
|
};
|
||
|
|
||
|
exports.ShellString = ShellString;
|
||
|
|
||
|
// Return the home directory in a platform-agnostic way, with consideration for
|
||
|
// older versions of node
|
||
|
function getUserHome() {
|
||
|
var result;
|
||
|
if (os.homedir)
|
||
|
result = os.homedir(); // node 3+
|
||
|
else
|
||
|
result = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
|
||
|
return result;
|
||
|
}
|
||
|
exports.getUserHome = getUserHome;
|
||
|
|
||
|
// Returns {'alice': true, 'bob': false} when passed a string and dictionary as follows:
|
||
|
// parseOptions('-a', {'a':'alice', 'b':'bob'});
|
||
|
// Returns {'reference': 'string-value', 'bob': false} when passed two dictionaries of the form:
|
||
|
// parseOptions({'-r': 'string-value'}, {'r':'reference', 'b':'bob'});
|
||
|
function parseOptions(opt, map) {
|
||
|
if (!map)
|
||
|
error('parseOptions() internal error: no map given');
|
||
|
|
||
|
// All options are false by default
|
||
|
var options = {};
|
||
|
for (var letter in map) {
|
||
|
if (map[letter][0] !== '!')
|
||
|
options[map[letter]] = false;
|
||
|
}
|
||
|
|
||
|
if (!opt)
|
||
|
return options; // defaults
|
||
|
|
||
|
var optionName;
|
||
|
if (typeof opt === 'string') {
|
||
|
if (opt[0] !== '-')
|
||
|
return options;
|
||
|
|
||
|
// e.g. chars = ['R', 'f']
|
||
|
var chars = opt.slice(1).split('');
|
||
|
|
||
|
chars.forEach(function(c) {
|
||
|
if (c in map) {
|
||
|
optionName = map[c];
|
||
|
if (optionName[0] === '!')
|
||
|
options[optionName.slice(1, optionName.length-1)] = false;
|
||
|
else
|
||
|
options[optionName] = true;
|
||
|
} else {
|
||
|
error('option not recognized: '+c);
|
||
|
}
|
||
|
});
|
||
|
} else if (typeof opt === 'object') {
|
||
|
for (var key in opt) {
|
||
|
// key is a string of the form '-r', '-d', etc.
|
||
|
var c = key[1];
|
||
|
if (c in map) {
|
||
|
optionName = map[c];
|
||
|
options[optionName] = opt[key]; // assign the given value
|
||
|
} else {
|
||
|
error('option not recognized: '+c);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
error('options must be strings or key-value pairs');
|
||
|
}
|
||
|
return options;
|
||
|
}
|
||
|
exports.parseOptions = parseOptions;
|
||
|
|
||
|
// Expands wildcards with matching (ie. existing) file names.
|
||
|
// For example:
|
||
|
// expand(['file*.js']) = ['file1.js', 'file2.js', ...]
|
||
|
// (if the files 'file1.js', 'file2.js', etc, exist in the current dir)
|
||
|
function expand(list) {
|
||
|
if (!Array.isArray(list)) {
|
||
|
throw new TypeError('must be an array');
|
||
|
}
|
||
|
var expanded = [];
|
||
|
list.forEach(function(listEl) {
|
||
|
// Don't expand non-strings
|
||
|
if (typeof listEl !== 'string') {
|
||
|
expanded.push(listEl);
|
||
|
} else {
|
||
|
var ret = glob.sync(listEl, config.globOptions);
|
||
|
// if glob fails, interpret the string literally
|
||
|
expanded = expanded.concat(ret.length > 0 ? ret : [listEl]);
|
||
|
}
|
||
|
});
|
||
|
return expanded;
|
||
|
}
|
||
|
exports.expand = expand;
|
||
|
|
||
|
// Normalizes _unlinkSync() across platforms to match Unix behavior, i.e.
|
||
|
// file can be unlinked even if it's read-only, see https://github.com/joyent/node/issues/3006
|
||
|
function unlinkSync(file) {
|
||
|
try {
|
||
|
fs.unlinkSync(file);
|
||
|
} catch(e) {
|
||
|
// Try to override file permission
|
||
|
if (e.code === 'EPERM') {
|
||
|
fs.chmodSync(file, '0666');
|
||
|
fs.unlinkSync(file);
|
||
|
} else {
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.unlinkSync = unlinkSync;
|
||
|
|
||
|
// e.g. 'shelljs_a5f185d0443ca...'
|
||
|
function randomFileName() {
|
||
|
function randomHash(count) {
|
||
|
if (count === 1)
|
||
|
return parseInt(16*Math.random(), 10).toString(16);
|
||
|
else {
|
||
|
var hash = '';
|
||
|
for (var i=0; i<count; i++)
|
||
|
hash += randomHash(1);
|
||
|
return hash;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 'shelljs_'+randomHash(20);
|
||
|
}
|
||
|
exports.randomFileName = randomFileName;
|
||
|
|
||
|
// extend(target_obj, source_obj1 [, source_obj2 ...])
|
||
|
// Shallow extend, e.g.:
|
||
|
// extend({A:1}, {b:2}, {c:3}) returns {A:1, b:2, c:3}
|
||
|
function extend(target) {
|
||
|
var sources = [].slice.call(arguments, 1);
|
||
|
sources.forEach(function(source) {
|
||
|
for (var key in source)
|
||
|
target[key] = source[key];
|
||
|
});
|
||
|
|
||
|
return target;
|
||
|
}
|
||
|
exports.extend = extend;
|
||
|
|
||
|
// Common wrapper for all Unix-like commands
|
||
|
function wrap(cmd, fn, options) {
|
||
|
return function() {
|
||
|
var retValue = null;
|
||
|
|
||
|
state.currentCmd = cmd;
|
||
|
state.error = null;
|
||
|
state.errorCode = 0;
|
||
|
|
||
|
try {
|
||
|
var args = [].slice.call(arguments, 0);
|
||
|
|
||
|
if (config.verbose) {
|
||
|
args.unshift(cmd);
|
||
|
console.error.apply(console, args);
|
||
|
args.shift();
|
||
|
}
|
||
|
|
||
|
if (options && options.notUnix) {
|
||
|
retValue = fn.apply(this, args);
|
||
|
} else {
|
||
|
if (args[0] instanceof Object && args[0].constructor.name === 'Object') {
|
||
|
args = args; // object count as options
|
||
|
} else if (args.length === 0 || typeof args[0] !== 'string' || args[0].length <= 1 || args[0][0] !== '-') {
|
||
|
args.unshift(''); // only add dummy option if '-option' not already present
|
||
|
}
|
||
|
|
||
|
args = args.reduce(function(accum, cur) {
|
||
|
if (Array.isArray(cur)) {
|
||
|
return accum.concat(cur);
|
||
|
} else {
|
||
|
accum.push(cur);
|
||
|
return accum;
|
||
|
}
|
||
|
}, []);
|
||
|
// Convert ShellStrings to regular strings
|
||
|
args = args.map(function(arg) {
|
||
|
if (arg instanceof Object && arg.constructor.name === 'String') {
|
||
|
return arg.toString();
|
||
|
} else
|
||
|
return arg;
|
||
|
});
|
||
|
// Expand the '~' if appropriate
|
||
|
var homeDir = getUserHome();
|
||
|
args = args.map(function(arg) {
|
||
|
if (typeof arg === 'string' && arg.slice(0, 2) === '~/' || arg === '~')
|
||
|
return arg.replace(/^~/, homeDir);
|
||
|
else
|
||
|
return arg;
|
||
|
});
|
||
|
if (!config.noglob && options && typeof options.idx === 'number')
|
||
|
args = args.slice(0, options.idx).concat(expand(args.slice(options.idx)));
|
||
|
try {
|
||
|
retValue = fn.apply(this, args);
|
||
|
} catch (e) {
|
||
|
if (e.msg === 'earlyExit')
|
||
|
retValue = e.retValue;
|
||
|
else throw e;
|
||
|
}
|
||
|
}
|
||
|
} catch (e) {
|
||
|
if (!state.error) {
|
||
|
// If state.error hasn't been set it's an error thrown by Node, not us - probably a bug...
|
||
|
console.error('shell.js: internal error');
|
||
|
console.error(e.stack || e);
|
||
|
process.exit(1);
|
||
|
}
|
||
|
if (config.fatal)
|
||
|
throw e;
|
||
|
}
|
||
|
|
||
|
state.currentCmd = 'shell.js';
|
||
|
return retValue;
|
||
|
};
|
||
|
} // wrap
|
||
|
exports.wrap = wrap;
|
||
|
|
||
|
function _readFromPipe(that) {
|
||
|
return that instanceof String ? that.toString() : '';
|
||
|
}
|
||
|
exports.readFromPipe = _readFromPipe;
|