// 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;