Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I execute shell commands in sequence?

Tags:

shell

node.js

I have a list of shell commands I want to execute with nodejs:

// index.js
var commands = ["npm install", "echo 'hello'"];

var exec = require('child_process').exec;

for (var i = 0; i < commands.length; i++) {
    exec(commands[i], function(err, stdout) {
        console.log(stdout);
    });
}

When I run this, the commands are executed in the reverse order. Why is this happening? How do i execute the commands in sequence?

Better yet, is there a way to execute shell commands without using nodejs? I find its async handling of the shell a little cumbersome.

NOTE:

I know that libraries like shelljs exist. I'm trying to do this with base nodejs only.

like image 743
dopatraman Avatar asked Apr 30 '16 19:04

dopatraman


2 Answers

Your for loop is executing all your asynchronous operation in parallel at once because exec() is non-blocking. The order they will complete depends upon their execution time and will not be determinate. If you truly want them to be sequenced, then you have to execute one, wait for it to call it's completion callback and then execute the next one.

You can't use a traditional for loop to "wait" on an asynchronous operation to complete in Javascript in order to execute them sequentially. Instead, you have to make the iteration manually where you kick off the next iteration in the completion callback of the previous one. My usual way of doing that is with a counter and a local function called next() like this:

Manual Async Iteration

var commands = ["npm install", "echo 'hello'"];

var exec = require('child_process').exec;

function runCommands(array, callback) {

    var index = 0;
    var results = [];

    function next() {
       if (index < array.length) {
           exec(array[index++], function(err, stdout) {
               if (err) return callback(err);
               // do the next iteration
               results.push(stdout);
               next();
           });
       } else {
           // all done here
           callback(null, results);
       }
    }
    // start the first iteration
    next();
}

runCommands(commands, function(err, results) {
    // error or results here
});

ES6 Promises

Since promises have been standardized in ES6 and are built into node.js now, I like to use Promises for my async operations:

var exec = require('child_process').exec;

function execPromise = function(cmd) {
    return new Promise(function(resolve, reject) {
        exec(cmd, function(err, stdout) {
            if (err) return reject(err);
            resolve(stdout);
        });
    });
}

var commands = ["npm install", "echo 'hello'"];

commands.reduce(function(p, cmd) {
    return p.then(function(results) {
        return execPromise(cmd).then(function(stdout) {
            results.push(stdout);
            return results;
        });
    });
}, Promise.resolve([])).then(function(results) {
    // all done here, all results in the results array
}, function(err) {
    // error here
});

Bluebird Promises

Using the Bluebird promise library, this would be even simpler:

var Promise = require('bluebird');
var execP = Promise.promisify(require('child_process').exec);

var commands = ["npm install", "echo 'hello'"];
Promise.mapSeries(commands, execP).then(function(results) {
    // all results here
}, function(err) {
    // error here
});
like image 55
jfriend00 Avatar answered Sep 30 '22 17:09

jfriend00


Opt.1: Use the '...Sync' version of the function if it exists

In this case there is already an execSync function:

child_process.execSync(command[, options])

Opt.2: Generators magic!

For a more general purpose, nowadays you could use e.g. this 'generator' pattern to 'deasync' any async function inside them, very useful for any sequential OS script.

Here an example of how to use readline async function in a sync fashion in node.js v6+ (I think also v4+)

var main = (function* () {
  var rl = require('readline')
          .createInterface({input: process.stdin, output: process.stdout });
  // the callback uses the iterator '.next()' to resume the 'yield'
  a = yield rl.question('do you want this? ', r=>main.next(r))  
  b = yield rl.question('are you sure? ', r=>main.next(r))      
  rl.close()
  console.log(a,b)
})()          // <- generator executed, iterator 'main' created
main.next()   // <- start iterator, run till the first 'yield'
like image 32
drodsou Avatar answered Sep 30 '22 18:09

drodsou