alxolr's blog

the road to node.js mastery

Broadcast same shell command in multiple terminals

Broadcast same shell command in multiple terminals

Intro

In this tutorial, we will create a simple node.js program that will broadcast one command in a list of terminals. The goal of the tutorial is to show the power of node.js streams, and how can we use them to create powerful CLI tools.

Problem

Working in a microservice environment means doing a lot of repetitive work in a bunch of terminals like:

  • rebase/merge, tag creation (git operations)
  • update new version of common library
  • audit and fixing of security vulnerabilities
  • running unit tests

So this problem scale as the number of microservices you have to manage grows. All the time, I caught myself forgetting to update or release one of them.

Solution

Create a node application that will spawn the command I am about to run in a list of different paths.

There is also a small trick I would like to support. Piping capacities of Linux and be able to run a chain of commands like:

cat package.json | grep express

[serv1]> cat package.json | grep express
    "express": "^4.17.1",
    "@types/express": "^4.17.2",

[serv2> cat package.json | grep express
    "express": "^4.17.1",
    "@types/express": "^4.17.2",

[serv3]> cat package.json | grep express
    "express": "^4.17.1",

Code solution

'use strict';

const { spawn } = require('child_process');
const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const shells = [
  {
    name: 'serv1',
    path: '/home/alxolr/microservices/serv1'
  },
  {
    name: 'serv2',
    path: '/home/alxolr/microservices/serv2'
  },
  {
    name: 'serv3',
    path: '/home/alxolr/microservices/serv3'
  },
];

function run() {
  rl.question('shila> ', answer => {
    if (answer.toLowerCase() === 'exit') {
      // add the possibility to exit from the script using exit commmand
      process.exit(1);
    }

    let commands = [];
    if (answer.indexOf('|')) {
      commands = extractPipedStrings(answer);
    } else {
      commands.push(answer);
    }

    let finished = shells.length;
    for (let shell of shells) {
      const stream = computePipedStream(commands, shell.path);
      stream.stdout.on('data', data => {
        console.log('[%s]> %s\n%s', shell.name, answer, data);
      });

      stream.stderr.pipe(process.stderr);
      stream.stdout.on('end', handleFinish);

      function handleFinish() {
        finished--;
        if (finished <= 0) {
          run();
        }
      }
    }
  });
}

run(); // start our bash a like terminal

/**
 *
 * @param {string[]} commands shell commands to chain
 * @param {string} cwd path to specific terminal to run the command
 */
function computePipedStream(commands, cwd) {
  const pipedStream = commands.reduce((pipedStream, command, index) => {
    if (index === 0) {
      // First command will start our stream chain
      return spawnCommand(command, cwd);
    } else {
      const stream = spawnCommand(command, cwd);

      // piping the output of previous command in the input of current one
      pipedStream.stdout.pipe(stream.stdin);

      return stream;
    }
  }, null);

  return pipedStream;
}

/**
 * @param {string} command shell command
 * @param {string} cwd shell path
 */
function spawnCommand(command, cwd) {
  const [fn, args] = extractCommand(command);
  const stream = spawn(fn, args, {
    cwd
  });

  return stream;
}

/**
 * @param {String} str command to be runned
 */
function extractCommand(str) {
  const commandExtractRex = /([-\/\w,.+?\d@]+)|([\w\/.,+?\d@]+)|(".+?")/gm;
  const [command, ...args] = str.trim().match(commandExtractRex);

  return [command, args];
}

/**
 * @param {String} str commands string piped by ' | '
 */
function extractPipedStrings(str) {
  return str.split(' | ');
}

Improvements

This little program is ultra helpful when you need to run the same command in multiple terminals, but it can be improved.

Improvement ideas:

  • create a YAML configuration file where we pass the services configuration
  • ability to run a specific command only in one terminal not in all at once

For these capacities, I recommend an existing tool handy for microservice development called fuge or tmux.

If you find my content useful, please share it, follow me on twitter @alxolr, or subscribe to get an email notification when a new article is released.

×