DEV Community

Cover image for Executing Shell Scripts with NodeJS
Aabhas Sao
Aabhas Sao

Posted on • Edited on

Executing Shell Scripts with NodeJS

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.
shell

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


Enter fullscreen mode Exit fullscreen mode

Initialise the node project with all default parameters.



npm i init -y


Enter fullscreen mode Exit fullscreen mode

Create a server.js file. The main logic of our code will be in this file.



touch server.js


Enter fullscreen mode Exit fullscreen mode

Add an npm script to run the server.



  "scripts": {
    "start": "nodemon server.js"
  }


Enter fullscreen mode Exit fullscreen mode

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}`);
})


Enter fullscreen mode Exit fullscreen mode

Run the server and try localhost:3000/ in your browser. You will see hello world! if there is no error.



  npm start


Enter fullscreen mode Exit fullscreen mode

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");


Enter fullscreen mode Exit fullscreen mode

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);
  });
});



Enter fullscreen mode Exit fullscreen mode

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}`);
});


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Fun route output

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)

Collapse
 
antongolub profile image
Anton Golub • Edited

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.

Collapse
 
aabhassao profile image
Aabhas Sao

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?

Collapse
 
antongolub profile image
Anton Golub

Ok, here's a RCE example)

const arg = '"hello" && echo "rm -rf ./ may be here"'
const cmd = `echo ${arg}`
Enter fullscreen mode Exit fullscreen mode

Key tip: you need to understand the boundaries of the arguments and escape the characters that can violate them.

Thread Thread
 
aabhassao profile image
Aabhas Sao

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.

Thread Thread
 
aabhassao profile image
Aabhas Sao

I would surely put some disclaimer on that.

Collapse
 
sumana2001 profile image
Sumana Basu

Nice article😍

Collapse
 
aabhassao profile image
Aabhas Sao

Thanks Sumana 😄

Collapse
 
andrewbaisden profile image
Andrew Baisden

Great tutorial.

Collapse
 
aniket762 profile image
Aniket Pal

A good read, followed you looking forward to more such articles 💕

Collapse
 
aabhassao profile image
Aabhas Sao

Thanks Aniket mean a lot

Collapse
 
aabhassao profile image
Aabhas Sao

Do connect with me on LinkedIn