This content originally appeared on SitePoint and was authored by Simon Plenderleith
In this article, we’ll learn what Google’s zx library provides, and how we can use it to write shell scripts with Node.js. We’ll then learn how to use the features of zx by building a command-line tool that helps us bootstrap configuration for new Node.js projects.
Writing Shell Scripts: the Problem
Creating a shell script — a script that’s executed by a shell such as Bash or zsh — can be a great way of automating repetitive tasks. Node.js seems like an ideal choice for writing a shell script, as it provides us with a number of core modules, and allows us to import any library we choose. It also gives us access to the language features and built-in functions provided by JavaScript.
But if you’ve tried writing a shell script to run under Node.js, you’ve probably found it’s not quite as smooth as you’d like. You need to write special handling for child processes, take care of escaping command line arguments, and then end up messing around with stdout
(standard output) and stderr
(standard error). It’s not especially intuitive, and can make shell scripting quite awkward.
The Bash shell scripting language is a popular choice for writing shell scripts. There’s no need to write code to handle child processes, and it has built-in language features for working with stdout
and stderr
. But it isn’t so easy to write shell scripts with Bash either. The syntax can be quite confusing, making it difficult to implement logic, or to handle things like prompting for user input.
The Google’s zx library helps make shell scripting with Node.js efficient and enjoyable.
Requirements for following along
There are a few requirements for following along with this article:
- Ideally, you should be familiar with the basics of JavaScript and Node.js.
- You’ll need to be comfortable running commands in a terminal.
- You’ll need to have Node.js >= v14.13.1 installed.
All of the code in this article is available on GitHub.
How Does Google’s zx Work?
Google’s zx provides functions that wrap up the creation of child processes, and the handling of stdout
and stderr
from those processes. The primary function we’ll be working with is the $
function. Here’s an example of it in action:
import { $ } from "zx";
await $`ls`;
And here’s the output from executing that code:
$ ls
bootstrap-tool
hello-world
node_modules
package.json
README.md
typescript
The JavaScript syntax in the example above might look a little funky. It’s using a language feature called tagged template literals. It’s functionally the same as writing await $("ls")
.
Google’s zx provides several other utility functions to make shell scripting easier, such as:
cd()
. This allows us to change our current working directory.question()
. This is a wrapper around the Node.js readline module. It makes it straightforward to prompt for user input.
As well as the utility functions that zx provides, it also makes several popular libraries available to us, such as:
- chalk. This library allows us to add color to the output from our scripts.
- minimist. A library that parses command-line arguments. They’re then exposed under an
argv
object. - fetch. A popular Node.js implementation of the Fetch API. We can use it to make HTTP requests.
- fs-extra. A library that exposes the Node.js core fs module, as well as a number of additional methods to make it easier to work with a file system.
Now that we know what zx gives us, let’s create our first shell script with it.
Hello World with Google’s zx
First, let’s create a new project:
mkdir zx-shell-scripts
cd zx-shell-scripts
npm init --yes
Then we can install the zx
library:
npm install --save-dev zx
Note: the zx
documentation suggests installing the library globally with npm. By installing it as a local dependency of our project instead, we can ensure that zx is always installed, as well as control the version that our shell scripts use.
Top-level await
In order to use top-level await
in Node.js — await
outside of an async
function — we need to write our code in ECMAScript (ES) Modules, which support top-level await
. We can indicate that all modules in a project are ES modules by adding "type": "module"
in our package.json
, or we can set the file extension of individual scripts to .mjs
. We’ll be using the .mjs
file extension for the examples in this article.
Running a command and capturing its output
Let’s create a new script named hello-world.mjs
. We’ll add a shebang line, which tells the operating system (OS) kernel to run the script with the node
program:
#! /usr/bin/env node
Now we’ll add some code that uses zx to run a command.
In the following code, we’re running a command to execute the ls program. The ls
program will list the files in the current working directory (the directory which the script is in). We’ll capture the standard output from the command’s process, store it in a variable and then log it out to the terminal:
// hello-world.mjs
import { $ } from "zx";
const output = (await $`ls`).stdout;
console.log(output);
Note: the zx
documentation suggests putting /usr/bin/env zx
in the shebang line of our scripts, but we’re using /usr/bin/env node
instead. This is because we’ve installed zx
as a local dependency of our project. We’re then explicitly importing the functions and objects that we want to use from the zx
package. This helps make it clear where the dependencies used in our script are coming from.
We’ll then use chmod to make the script executable:
chmod u+x hello-world.mjs
Let’s run our script:
./hello-world.mjs
We should now see the following output:
$ ls
hello-world.mjs
node_modules
package.json
package-lock.json
README.md
hello-world.mjs
node_modules
package.json
package-lock.json
README.md
You’ll notice a few things in the output from our shell script:
- The command we ran (
ls
) is included in the output. - The output from the command is displayed twice.
- There’s an extra new line at the end of the output.
zx
operates in verbose
mode by default. It will output the command you pass to the $
function and also output the standard output from that command. We can change this behavior by adding in the following line of code before we run the ls
command:
$.verbose = false;
Most command line programs, such as ls
, will output a new line character at the end of their output to make the output more readable in the terminal. This is good for readability, but as we’re storing the output in a variable, we don’t want this extra new line. We can get rid of it with the JavaScript String#trim() function:
- const output = (await $`ls`).stdout;
+ const output = (await $`ls`).stdout.trim();
If we run our script again, we’ll see things look much better:
hello-world.mjs
node_modules
package.json
package-lock.json
Using Google’s zx with TypeScript
If we want to write shell scripts that use zx
in TypeScript, there are a couple of minor differences we need to account for.
Note: the TypeScript compiler provides a number of configuration options that allow us to adjust how it compiles our TypeScript code. With that in mind, the following TypeScript configuration and code are designed to work under most versions of TypeScript.
First, let’s install the dependencies we’ll need to run our TypeScript code:
npm install --save-dev typescript ts-node
The ts-node package provides a TypeScript execution engine, allowing us to transpile and run TypeScript code.
We need to create a tsconfig.json
file containing the following configuration:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs"
}
}
Let’s now create a new script named hello-world-typescript.ts
. First, we’ll add a shebang line that tells our OS kernel to run the script with the ts-node
program:
#! ./node_modules/.bin/ts-node
In order to use the await
keyword in our TypeScript code, we need to wrap it in an immediately invoked function expression (IIFE), as recommended in the zx documentation:
// hello-world-typescript.ts
import { $ } from "zx";
void (async function () {
await $`ls`;
})();
We then need to make the script executable so that we can execute it directly:
chmod u+x hello-world-typescript.ts
When we run the script:
./hello-world-typescript.ts
… we should see the following output:
$ ls
hello-world-typescript.ts
node_modules
package.json
package-lock.json
README.md
tsconfig.json
Writing scripts with zx
in TypeScript is similar to using JavaScript, but requires a little extra configuration and wrapping of our code.
Continue reading How to Write Shell Scripts in Node with Google’s zx Library on SitePoint.
This content originally appeared on SitePoint and was authored by Simon Plenderleith
Simon Plenderleith | Sciencx (2021-12-13T15:00:02+00:00) How to Write Shell Scripts in Node with Google’s zx Library. Retrieved from https://www.scien.cx/2021/12/13/how-to-write-shell-scripts-in-node-with-googles-zx-library/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.