You might have seen .sh files in many popular GitHub repositories. Even some of you might have used shell file to install node's latest version in Ubuntu. What the hell are those? Let's get started
What is Shell? 🐚
Shell is a command line tool through which you can interact with the kernel. Through shell commands many crucial things can be done like accessing the hardware resource, spawning child processes and many more.
What is NodeJS? ☊
Node is a javascript runtime, usually used to create a backend server. Initially JavaScript could only run on the browser. But the curiosity of one man named Ryan Dahl changed it. JavaScript is slower than statically typed languages. But V8 engine developed by Google improved the javascript performance.
Ways to execute shell scripts 🚴♂️
You can run shell script by creating a file with .sh extension. bash, zshrc, and more are modifications on top of shell. Unix like systems have bash shell as default. Windows has command shell and powershell.
Why execute shell scripts in Node? 🤔
Say you need to use a library or tool which is only available as CLI. You don't have any npm module to do that specific task. What you would do now? Don't worry I have a solution for you.
The first time I used it 🐣
In one of the projects I had to access a CLI tool installed on the docker container using Node server. I was using Cura engine a CLI tool installed in my docker container running ubuntu. The basic function of the CLI tool was to convert STL 3d object to G-code. For those of you who don't know what a G-code file is, it is a code which serves as instructions for a 3d printer to print a model. It contains instructions on how to move and how much material to drop, which material to choose etc.
So in my server I was taking a STL file as multipart data and returning a G-code file as response.
Let's try a fun example 🚀
First install Node.js if you don't have it already. Let's create a node project.
Install emoj globally so that we can call it via command line. Create a directory and then change your directory into that. Install nodemon globally as well so we don't have to reload the server manually.
npm i -g emoj nodemon
Initialise the node project with all default parameters.
npm i init -y
Create a server.js file. The main logic of our code will be in this file.
touch server.js
Add an npm script to run the server.
"scripts": {
"start": "nodemon server.js"
}
Now let's create a node server. Add the code below in server.js file.
import express from 'express'
const app = express()
const PORT = 3000;
app.get('/', (req, res) => {
res.send('hello world!');
});
app.listen(PORT, () => {
console.log(`server running on port: ${PORT}`);
})
Run the server and try localhost:3000/ in your browser. You will see hello world! if there is no error.
npm start
Now let's code the most fun part. We will use exec function that node.js provides.
The function of exec is beautifully described in node.js documentation. I would like to quote it here.
Spawns a shell then executes the command within that shell, buffering any generated output.
You can check out the node.js documentation to learn more about exec.
const { exec } = require("child_process");
Add the fun request in server.js before app.listen.
app.get('/fun', async (req, res) => {
const { commandText } = req.query;
console.log(req.query)
await exec(`emoj ${commandText}`, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return res.send('some error happened: 😭');
}
return res.status(200).send(stdout);
});
});
Now the final server.js file looks like below.
const express = require('express');
const app = express()
const { exec } = require("child_process");
const PORT = 3000;
// app.use(express.urlencoded());
app.get('/', (req, res) => {
res.send('hello world!');
});
app.get('/fun', async (req, res) => {
const { commandText } = req.query;
console.log(req.query)
await exec(`emoj ${commandText}`, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return res.send('some error happened: 😭');
}
return res.status(200).send(stdout);
});
});
app.listen(PORT, () => {
console.log(`server running on port: ${PORT}`);
});
To test our fun route send a get request via any tool like Postman or use browser itself. For postman create a parameter named commandText and pass some cool text.
localhost:3000/fun?commandText=winter is coming
Disclaimer
Passing input arguments taken from user directly to exec function can be dangerous. In a way it is similar to how SQL injection happens. Thanks @antongolub for the feedback.
Consider using following libraries to abstract out risks mentioned above.
github.com/google/zx
github.com/shelljs/shelljs
Outro 💚
Congratulations! You just executed shell command from node.js. I hope you found this article useful. Incase you want to connect with me and discuss about anything feel free to connect with me on LinkedIn💕
If you are an organisation and want a freelance tech content engineer feel free to connect with me
Top comments (11)
I think it's important to add a disclaimer: please never use
cp
api like this. This is extremely unsafe. String literal w/o arg boxing, symbol escaping, etc, provides any RCE.See how bash-in-js concept is implemented in similar projects.
Thanks @antongolub I didn't knew. The libraries you mentioned seem cool. I would check them out. Why is using child process like this unsafe didn't understood well. Any article ont that?
Ok, here's a RCE example)
Key tip: you need to understand the boundaries of the arguments and escape the characters that can violate them.
Thanks now I get it. Passing user input in such commands can be dangerous. Similar to the way SQL injection attacks happen by I'll formatted arguments.
I would surely put some disclaimer on that.
Nice article😍
Thanks Sumana 😄
Great tutorial.
A good read, followed you looking forward to more such articles 💕
Thanks Aniket mean a lot
Do connect with me on LinkedIn