Although NodeJs has an extensive library of modules, there are certain times when a CLI command is not only more convenient, but also more efficient to complete certain tasks. This module introduces the basic concepts and code to run CLI commands from a NodeJS script.
The term ’process’ is technical. A process is a program in execution. A program is nothing more than just the specifications of how something is done, like a cookbook. However, a process is an instance of the instructions being followed, like a person following a cookbook to actually cook food.
As you can imagine, when your script is running, it is in a process. When a program in execution needs to start another program, it has two choices. The first one is to give up the original program, and the new program gets to reuse the same process.
However, if the purpose is to run the new program, and then to continue with the existing process, then a new process (the child process) needs to be started to run the new program.
The child_process module includes many methods to handle a child process.
Depending on the CLI command and what it is supposed to do, it may take a command some time to complete. Since we are using the context of an Express script, "busy waiting" for a command to finish means a single HTTP request can end up hogging time unnecessarily and making the Express server unresponsive.
As a result, the asynchronous method is often preferred when an Express script executes a CLI command.
A "shell" is a utility program that provides a rich context for any CLI command to run in. While a shell has a slight overhead, it is very minimal. The advantage of having a NodeJS script to execute a CLI command within the context of a shell is that most commands that run interactively can be specified the same way when run from a NodeJS script.
Furthermore, a shell, such as BASH (the default shell on many Linux systems) offers its own built-in commands and control structures. For example, the CLI command cd is actually a built-in command of BASH (most other shells offer the same command).
One of the conveniences offered by a shell is the PATH environment variable. This environment variable makes it easy to run commands that are not built-in to a shell (external commands). For example, du is not a built-in command, but it is within the PATH of a shell. Without this environment variable, it becomes necessary to find the path of every external command.
The du command shows the storage consumed by a folder structure. Depending on the depth of folder hierarchy as well as the number of files included, du can take a long time to execute.
Now we are getting to the details of how to run a CLI command in a shelled environment from a NodeJS script.
You can download this script.
While simple, this sample program illustrates the most basic use of asynchronous exec. When this script runs, note how the method exec returns right away. Only when the CLI command du is completed did the function execHandler gets called (by node itself) to report any error.
stdout is a string parameters that is the actual output of the command, whereas stderr is a string parameter that is the "error output" of the command. The parameter error reports back issues of attempting to the command.
In other words, if there is a problem starting the command, the error parameter will not be null. If the command runs, but encounters problems, then stderr will contain error messages from the command itself. stdout is the standard (normal) output of running the command.
Next, we parametrize and make a function so that it is easy to specify the execution of any CLI command.
You can download this script.
This is a slightly dressed up version of the previous one. Note how the output can now specify what command was run because execHandler is defined within shelledCommand, gaining access to all the parameters of shelledCommand.
With this version, the three pieces of information after running a CLI command, error, stdout, and stderr are visible only within execHandler. It would be nice to be able to pass these parameters along to code that is outside of shelledCommand. This is where promises can be useful.
When a promise is resolved by having the resolve call back function invoked, the handler specified in the then method of the process has access to the resolved value.
You can download this script.
In this revision, shelledCommand has one new level of function called whatToDo. whatToDo captions the action to perform when a Promise object is created and returned from shelledCommand. whatToDo only has one thing to do: to call the exec method of child_process. However, because whatToDo is the action call-back of a Promise, it is automatically passed two call-back functions. One is called resolve, which is a function to be called when the action associated with whatToDo (the exec) has finished and execHandler is called.
Instead of using console.log within execHandler, execHandler calls the resolve call-back function given to whatToDo. resolve can only use one value, but there are four pieces of information. This is why an object is created to pass all four pieces of information along.
The chain of operations is rather complex in this program:
Note that the named functions whatToDo, execHandler and afterShelledCommand are often anonymous in commercial code. They are named in this sample program for clarify and ease of reference.
The use of Promises is great, but it fragments the code of a sequence of operations. At this point, shelledCommand is exactly what we need because it returns a Promise. We need to define a main function that is async so that we can throw all the sequential logic that may need to utilize asynchronous operations into it.
You can download this script.
Note how afterShelledCommand is no longer necessary. This function, as a call-back function, was necessary because it is needed by the then method of a Promise to specify what to do with a resolve value. Using await, however, the use of Promise is hidden because the await expression returns the resolve value of the Promise object specified right after the word await. Execution is also blocked until the said Promise resolves.
This last revision of the sample programs contains the finished form of shelledCommand as well as the template of how it can used in an async function. This sets up the basic method of running CLI commands from async-enabled Express handlers for clarify, efficiency and responsiveness.