index.js 7.1 KB
/*
 * Jake JavaScript build tool
 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/


let util = require('util'); // Native Node util module
let spawn = require('child_process').spawn;
let EventEmitter = require('events').EventEmitter;
let logger = require('./logger');
let file = require('./file');
let Exec;

const _UUID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');

let parseArgs = function (argumentsObj) {
  let args;
  let arg;
  let cmds;
  let callback;
  let opts = {
    interactive: false,
    printStdout: false,
    printStderr: false,
    breakOnError: true
  };

  args = Array.prototype.slice.call(argumentsObj);

  cmds = args.shift();
  // Arrayize if passed a single string command
  if (typeof cmds == 'string') {
    cmds = [cmds];
  }
  // Make a copy if it's an actual list
  else {
    cmds = cmds.slice();
  }

  // Get optional callback or opts
  while((arg = args.shift())) {
    if (typeof arg == 'function') {
      callback = arg;
    }
    else if (typeof arg == 'object') {
      opts = Object.assign(opts, arg);
    }
  }

  // Backward-compat shim
  if (typeof opts.stdout != 'undefined') {
    opts.printStdout = opts.stdout;
    delete opts.stdout;
  }
  if (typeof opts.stderr != 'undefined') {
    opts.printStderr = opts.stderr;
    delete opts.stderr;
  }

  return {
    cmds: cmds,
    opts: opts,
    callback: callback
  };
};

/**
  @name jake
  @namespace jake
*/
let utils = new (function () {
  /**
    @name jake.exec
    @static
    @function
    @description Executes shell-commands asynchronously with an optional
    final callback.
    `
    @param {String[]} cmds The list of shell-commands to execute
    @param {Object} [opts]
      @param {Boolean} [opts.printStdout=false] Print stdout from each command
      @param {Boolean} [opts.printStderr=false] Print stderr from each command
      @param {Boolean} [opts.breakOnError=true] Stop further execution on
      the first error.
      @param {Boolean} [opts.windowsVerbatimArguments=false] Don't translate
      arguments on Windows.
    @param {Function} [callback] Callback to run after executing  the
    commands

    @example
    let cmds = [
          'echo "showing directories"'
        , 'ls -al | grep ^d'
        , 'echo "moving up a directory"'
        , 'cd ../'
        ]
      , callback = function () {
          console.log('Finished running commands.');
        }
    jake.exec(cmds, {stdout: true}, callback);
   */
  this.exec = function (a, b, c) {
    let parsed = parseArgs(arguments);
    let cmds = parsed.cmds;
    let opts = parsed.opts;
    let callback = parsed.callback;

    let ex = new Exec(cmds, opts, callback);

    ex.addListener('error', function (msg, code) {
      if (opts.breakOnError) {
        fail(msg, code);
      }
    });
    ex.run();

    return ex;
  };

  this.createExec = function (a, b, c) {
    return new Exec(a, b, c);
  };

  // From Math.uuid.js, https://github.com/broofa/node-uuid
  // Robert Kieffer (robert@broofa.com), MIT license
  this.uuid = function (length, radix) {
    var chars = _UUID_CHARS
      , uuid = []
      , r
      , i;

    radix = radix || chars.length;

    if (length) {
      // Compact form
      i = -1;
      while (++i < length) {
        uuid[i] = chars[0 | Math.random()*radix];
      }
    } else {
      // rfc4122, version 4 form

      // rfc4122 requires these characters
      uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
      uuid[14] = '4';

      // Fill in random data.  At i==19 set the high bits of clock sequence as
      // per rfc4122, sec. 4.1.5
      i = -1;
      while (++i < 36) {
        if (!uuid[i]) {
          r = 0 | Math.random()*16;
          uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
        }
      }
    }

    return uuid.join('');
  };

})();

Exec = function () {
  let parsed = parseArgs(arguments);
  let cmds = parsed.cmds;
  let opts = parsed.opts;
  let callback = parsed.callback;

  this._cmds = cmds;
  this._callback = callback;
  this._config = opts;
};

util.inherits(Exec, EventEmitter);

Object.assign(Exec.prototype, new (function () {

  let _run = function () {
    let self = this;
    let sh;
    let cmd;
    let args;
    let next = this._cmds.shift();
    let config = this._config;
    let errData = '';
    let shStdio;
    let handleStdoutData = function (data) {
      self.emit('stdout', data);
    };
    let handleStderrData = function (data) {
      let d = data.toString();
      self.emit('stderr', data);
      // Accumulate the error-data so we can use it as the
      // stack if the process exits with an error
      errData += d;
    };

    // Keep running as long as there are commands in the array
    if (next) {
      let spawnOpts = {};
      this.emit('cmdStart', next);

      // Ganking part of Node's child_process.exec to get cmdline args parsed
      if (process.platform == 'win32') {
        cmd = 'cmd';
        args = ['/c', next];
        if (config.windowsVerbatimArguments) {
          spawnOpts.windowsVerbatimArguments = true;
        }
      }
      else {
        cmd = '/bin/sh';
        args = ['-c', next];
      }

      if (config.interactive) {
        spawnOpts.stdio = 'inherit';
        sh = spawn(cmd, args, spawnOpts);
      }
      else {
        shStdio = [
          process.stdin
        ];
        if (config.printStdout) {
          shStdio.push(process.stdout);
        }
        else {
          shStdio.push('pipe');
        }
        if (config.printStderr) {
          shStdio.push(process.stderr);
        }
        else {
          shStdio.push('pipe');
        }
        spawnOpts.stdio = shStdio;
        sh = spawn(cmd, args, spawnOpts);
        if (!config.printStdout) {
          sh.stdout.addListener('data', handleStdoutData);
        }
        if (!config.printStderr) {
          sh.stderr.addListener('data', handleStderrData);
        }
      }

      // Exit, handle err or run next
      sh.on('exit', function (code) {
        let msg;
        if (code !== 0) {
          msg = errData || 'Process exited with error.';
          msg = msg.trim();
          self.emit('error', msg, code);
        }
        if (code === 0 || !config.breakOnError) {
          self.emit('cmdEnd', next);
          setTimeout(function () { _run.call(self); }, 0);
        }
      });

    }
    else {
      self.emit('end');
      if (typeof self._callback == 'function') {
        self._callback();
      }
    }
  };

  this.append = function (cmd) {
    this._cmds.push(cmd);
  };

  this.run = function () {
    _run.call(this);
  };

})());

utils.Exec = Exec;
utils.file = file;
utils.logger = logger;

module.exports = utils;