Node.js has a built-in child_process
module. It provides methods that allow us to write scripts that run a command in a child process. These commands can run any programs that are installed on the machine that we’re running the script on.
What is execa?
The execa library provides a wrapper around the child_process
module for ease of use. This popular library was created and is maintained by prolific open-source developer Sindre Sorhus, and it’s downloaded millions of times every week.
In this article, we’re going to learn about the benefits of the execa
library and how we can start using it. We’ll also dive into the basics of error handling and process control, as well as look at the options we have for handling child process output.
To follow along, you’ll need:
- Familiarity with the basics of JavaScript and Node.js
- To be comfortable running commands in a terminal
- To have Node.js >/= v14.13.1 installed
- To be running Node.js on either macOS, Linux, or WSL on Windows (Windows Subsystem for Linux)
All the code in this article is available on GitHub.
The benefits of using execa
There are several benefits that execa provides over the built-in Node.js child_process
module.
First, execa exposes a promise-based API. This means we can use async/await in our code instead of needing to create callback functions as we do with the asynchronous child_process
module methods. If we need it, there is also an execaSync
method for running commands synchronously.
Execa also conveniently escapes and quotes any command arguments that we pass to it. For example, if we’re passing a string with spaces or quotes, execa will take care of escaping them for us.
It’s common for programs to include a new line at the end of their output. This is convenient for readability on the command line, but not helpful in a scripting context, so execa automatically strips these new lines for us by default.
We probably don’t want child processes hanging around if the process that is executing our script (node
) dies for any reason. Execa takes care of child process clean-up for us automatically, ensuring we don’t end up with “zombie” processes.
Another benefit of using execa is that it brings its support for running child processes with Node.js on Windows. It uses the popular cross-spawn package, which works around known issues with Node.js’ child_process.spawn
method on Windows.
Getting started with execa
Open up your terminal and create a new directory named tutorial
, then change into that directory:
mkdir tutorial cd tutorial
Now, let’s create a new package.json
file:
npm init --yes
Next, install the execa library:
npm install execa
Using a pure ES module package in Node.js
Node.js supports two different module types:
- CommonJS Modules (CJS): uses
module.exports
to export functions and objects andrequire()
to load them in another module - ECMAScript Modules (ESM): uses
export
to export functions and objects andimport
to load them in another module
Execa becomes a pure ESM package with its v6.0.0 release. This means we must use a version of Node.js that has support for ES modules in order to use this package.
For our own code, we can show Node.js that all modules in our project are ES modules by adding "type": "module"
in our package.json
, or we can set the file extension of individual scripts to .mjs
.
Let’s update our package.json
file:
{ "name": "tutorial", + "type": "module",
We can now import the execa method from the execa
package in any scripts we create:
import { execa } from "execa";
Although ES modules are seeing adoption in Node.js packages and applications, CommonJS modules are still the most widely used module type. If you’re unable to use ES modules in your codebase for any reason, you’ll need to use the import
method in your CommonJS modules:
async function run() { const { execa } = await import("execa"); // Code which uses `execa` here. } run();
Note that we need to wrap the call to the import
function in an async
function, as CommonJS modules do not support top-level await.
Running a command with execa
Now we’re going to create a script that uses execa to run the following command:
echo "execa is pretty neat!"
The echo
program prints out the string of text that is passed to it.
Let’s create a new file named run.js
. In this script, we’ll import the execa
method from the execa package:
// run.js import { execa } from "execa";
Then, we’ll run the echo
command with the execa
method:
// run.js const { stdout } = await execa("echo", ["execa is pretty neat!"]);
The promise returned by execa
resolves with an object. You’ll notice in the code above that the stdout
property is being unpacked from this object and assigned to a variable.
Let’s log the stdout
variable so we can see what its value is:
// run.js console.log({ stdout });
Now we can run the script:
node run.js
We should see the following output:
{ stdout: 'execa is pretty neat!' }
What is stdout
?
Programs have access to “standard streams”. In this tutorial, we’ll work with two of them:
stdout
: standard output is the stream that a program writes its output data tostderr
: standard error is the stream that a program writes error messages and debugs data to
When we run a program, these standard streams are typically connected to the parent process. If we’re running a program in our terminal, this means data sent by the program to the stdout
and stderr
streams will be received and displayed by the terminal.
When we run the scripts in this tutorial, the stdout
and stderr
streams of the child process are connected to the parent process, node
, allowing us to access any data the child process sends to them.
Working with execa
Now that we have a script that can run a command in a child process, we’re going to dive into the basics of working with execa.
Capturing stderr
Let’s change the line in run.js
where the execa
method is called to unpack the stderr
property from the object:
- const { stdout } = await execa("echo", ["execa is pretty neat!"]); + const { stdout, stderr } = await execa("echo", ["execa is pretty neat!"]);
Then update the console.log
line:
- console.log({ stdout }); + console.log({ stdout, stderr });
Now, let’s run the script again:
node run.js
We should see the following output:
{ stdout: 'execa is pretty neat!', stderr: '' }
You’ll notice that the value of stderr
is an empty string (''
). This is because the echo
command has written no data to the standard error stream.
The ls(1) program lists information about files and directories. If a file does not exist, it will write an error message to the standard error stream.
Let’s replace the command which we’re executing in run.js
:
- const { stdout, stderr } = await execa("echo", ["execa is pretty neat!"]); + const { stdout, stderr } = await execa("ls", ["missing-file.txt"]);
When we run the script (node run.js
), we should now see the following output:
Error: Command failed with exit code 2: ls missing-file.txt ... { shortMessage: 'Command failed with exit code 2: ls missing-file.txt', command: 'ls missing-file.txt', escapedCommand: 'ls missing-file.txt', exitCode: 2, signal: undefined, signalDescription: undefined, stdout: '', stderr: "ls: cannot access 'missing-file.txt': No such file or directory", failed: true, timedOut: false, isCanceled: false, killed: false }
Running this command with the execa
method has thrown an error. This is because the child process’ exit code was not 0
. An exit code of 0
commonly shows that a process ran successfully, and any other value indicates that there was a problem. The manual page for ls
defines the following exit codes and their meaning:
Exit status: 0 if OK, 1 if minor problems (e.g., cannot access subdirectory), 2 if serious trouble (e.g., cannot access command-line argument).
The error object thrown by the execa
method contains a stderr
property with the data that was written by ls
to the standard error stream.
We now need to implement error handling so that if the command fails, we handle it gracefully, rather than letting it crash our script.
Note: programs will often run successfully, but also write debug messages to stderr
.
Error handling in execa
We can wrap the command in a try...catch
statement and output a custom error message, like this:
// run.js try { const { stdout, stderr } = await execa("ls", ["missing-file.txt"]); console.log({ stdout, stderr }); } catch (error) { console.error( `ERROR: The command failed. stderr: ${error.stderr} (${error.exitCode})` ); }
Now, when we run our script (node run.js
), we should see the following output:
ERROR: The command failed. stderr: ls: cannot access 'missing-file.txt': No such file or directory (2)
Canceling a child process
Once we’ve started executing a command, we might want to cancel the process, e.g., if it takes longer than expected to complete. Execa provides a cancel
method that we can call to send a SIGTERM
signal to the child process.
Let’s replace all the code in run.js
apart from the import
statement. We’ll use the sleep program to create a child process that runs for five seconds:
// run.js const subprocess = execa("sleep", ["5s"]);
You’ll notice there is no await
keyword in front of this function call. This is because we want to first define a function that will run after one second and cancel the child process:
// run.js setTimeout(() => { subprocess.cancel(); }, 1000);
Then we can await
the subprocess
promise inside a try...catch
block:
// run.js try { const { stdout, stderr } = await subprocess; console.log({ stdout, stderr }); } catch (error) { if (error.isCanceled) { console.error(`ERROR: The command took too long to run.`) } else { console.error(error); } }
When the subprocess.cancel
method has been called, the isCanceled
property on the error object is set to true
. This allows us to determine the cause of the error and display a specific error message.
Now, when we run our script (node run.js
), we should see the following output:
ERROR: The command took too long to run.
If we need to force a child process to end, we can call the subprocess.kill
method instead of subprocess.cancel
.
Piping output from a child process with execa
The stdout
and stderr
properties in the object returned by the execa
method are readable streams. We’ve already seen how we can save the data that is sent to these streams in variables. We can pipe readable streams into writeable streams, for example, to see the output of the child process as it runs.
Let’s remove the call to setTimeout
in run.js
:
- setTimeout(() => { - subprocess.cancel(); - }, 1000);
Let’s change our command to use echo
again:
- const subprocess = execa("sleep", ["5s"]); + const subprocess = execa("echo", ["is it me you're looking for?"]);
Then, we’ll pipe the stdout
stream from the child process into the stdout
stream of our parent process (node
):
subprocess.stdout.pipe(process.stdout);
Now, when we run the script, we should see the following output:
is it me you're looking for? { stdout: "is it me you're looking for?", stderr: '' }
Our script now outputs the data from the child process’ stderr
stream as it runs, then logs out the values of the stderr
and stdout
variables to which we’ve saved the streams’ data.
Redirecting output to a file
Instead of piping the child process’ stdout
data to the stdout
stream of our parent process, we could write it to a file instead.
First, we need to import
the built-in Node.js fs module:
// run.js import * as fs from "fs";
Then we can replace the existing call to the pipe
method:
- subprocess.stdout.pipe(process.stdout); + subprocess.stdout.pipe(fs.createWriteStream("stdout.txt"));
This creates an fs.WriteStream
instance where data from the subprocess.stdout
readable stream will be piped to.
When we run our script, we should see the following output:
{ stdout: "is it me you're looking for?", stderr: '' }
We should also see that a file named stdout.txt
has been created, containing the data from the child process’ stdout
stream:
$ cat stdout.txt is it me you're looking for?
Conclusion
In this article, we’ve learned how to use execa to run commands from our Node.js scripts. We’ve seen the benefits it provides over the built-in Node.js child_process
module and learned the basic patterns that we need to apply to use execa effectively.
All the code in this article is available on GitHub.
The post Running commands with execa in Node.js appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/NBKj1G5
via Read more