DEV Community

Cover image for How to execute shell commands in js
Alex Dhaenens
Alex Dhaenens

Posted on

How to execute shell commands in js

For one of my side projects, I needed to be able to execute certain shell commands with JavaScript. That project is an Electron–React project, in which – thanks to the Electron framework – I have full access to the Node.js API and therefor the Node.js runtime. For the readers that are interested in Electron or in combining Electron and React you can find my blogs on those topics respectively here and here.
Although it is possible to execute shell commands in JavaScript, there are two important remarks: first that executing shell commands uses the Node.js API, so be aware that it only works in an environment that has access to that API (meaning a normal browser runtime won’t work). Secondly, some shell commands require administration permissions, so in those cases you’ll need to make sure that the process running your JavaScript code has such permissions.

The API

The Node.js API has a module called child_process that offers functions to spawn child processes both in ansynchronous as asynchronous manner. One of those functions that are available is the exec function. It has the following signature:

exec(command[, options][, callback])
Enter fullscreen mode Exit fullscreen mode

With the parameters: command as a string, options as an object with various options (see the documentation for more information) and a callback function. The function itself returns a reference to the spawned process (but it is not needed for executing shell commands).

With the exec function we can create a custom function with two different callbacks:

const { exec } = require('child_process');

export const executeCommand = (cmd, successCallback, errorCallback) => {
  exec(cmd, (error, stdout, stderr) => {
    if (error) {
     // console.log(`error: ${error.message}`);
      if (errorCallback) {
        errorCallback(error.message);
      }
      return;
    }
    if (stderr) {
      //console.log(`stderr: ${stderr}`);
      if (errorCallback) {
        errorCallback(stderr);
      }
      return;
    }
    //console.log(`stdout: ${stdout}`);
    if (successCallback) {
      successCallback(stdout);
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Although not required, using such a function is much handier and cleaner as you can use a different callback functions for success and error. In addition, there is a single point where you can turn on or off logging for all your commands.

Creating command functions

Since we got our base function for executing commands we can create now different functions for the different commands the code needs to execute. Depending which operating system you are targeting, it can be possible that other (shell)commands are needed (e.g. the dir command on windows and the ls command on linux). For the sake of an example you can get the current git branch with the following git command:

git -C “folder” rev-parse --abbrev-ref HEAD
Enter fullscreen mode Exit fullscreen mode

We can create a custom function for this, accepting a folder and two callbacks to execute:

export const getGitBranchCommand = (folder, success, error) => {
  executeCommand(
    `git -C ${folder} rev-parse --abbrev-ref HEAD`,
    branch => success(branch),
    errormsg => error(errormsg)
  );
}; 
Enter fullscreen mode Exit fullscreen mode

This functino will either call the success callback with the output of the shell command (which is the branch name) or call the error callback with the message the command returned when failing.

Some shell commands print a lot of text to the stout stream, so for those commands you’ll need to apply a regex to parse the data you want from that output.

Combining with state frameworks

A lot of applications use a state framework to keep the current state of your app. Chances are that you, the reader, use such a framework in your project and you’ll want to store the result of the commands you executed in that state. In my example I’m using Redux but you can follow a similar approach for other frameworks.
By using the getGitBranchCommand shown above you can create a new function specific for the Redux framework:

export const getGitBranch = (folder, dispatch) => {
  getGitBranchCommand(folder, branch =>
    dispatch(setFocusProjectGitBranch(branch), () => {})
  );
};
Enter fullscreen mode Exit fullscreen mode

Now you have a function that accepts a folder and the dispatch function (needed for dispatching actions in redux). This function can now be used anywhere in your application. In the code snippet above I’ve used the setFocusProjectGitBranch function, which is an action creator (if you don't know what that is, no worries it is Redux specific). Also, on a side node, the error callback is an empty function since I don’t need the error message (yet).

Architecture summary

I would like to summarize the blogpost by discussing the architecture used:

getGitBranch(folder,dispatch) 
=> getGitBranchCommand(folder, success, error) 
=> executeCommand(cmd, successCallback, errorCallback) 
=> exec(command[, options][, callback])
Enter fullscreen mode Exit fullscreen mode

The executeCommand is a general function for executing any command using the exec function from the child_processes node module. That function is used by the getGitBranchCommand, a function specific designed for getting the git branch. Lastly the highest function, is the one exposed to my whole application and is a state management framework dependent function. It executes the earlier mentioned getGitBranchCommand function and stores the result in the state, by using the state management’s api.

Using this architecture has the benefit that, when you would reuse the code in another project but with another state management framework, you only need to replace the getGitBranch function. Additionally, if you would for example support other operating systems, which could require different commands to do the same thing, you’ll only need to replace the getGitBranchCommand function.

Latest comments (2)

Collapse
 
dirkhaar profile image
DirkHaar

Shouldn't it read

exec(command[, options[, callback]])
instead of
exec(command[, options][, callback])

?

Explanation:
if you only add one parameter, doesn't it mean everything - normal message - will be routed to the callback device/file, but you can no say "only output errors" as the syntax representation scheme suggests?

Collapse
 
alexsalnikov profile image
alex • Edited

hi, good question,
but no - original syntax is correct, check-out following references:


@alexdhaenens, thanx for the concise blog-post (: