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.