Meet zx: A Better Way to Write Scripts with Node.js

Bash is great and all, but it’s not something I’ll pick up in a day. I was looking for something a little bit more convenient to write scripts in. While looking, I’ve stumbled upon this little utility from Google called zx. And it's a better way to write scripts using JavaScript.

I thought I’d give zx a try. It comes with a bunch of things out of the box, like chalk and fetch. I know, Node.js already lets me write scripts, but dealing with a bunch of the crap around escaping and sanitizing inputs was painful.

The Script Way

Before I talk about all the great things zx promised, let's talk about the basics of writing and using scripts first.

Scripts are all text files and need to start with a shebang at the top (also known as sha-bang, hashbang, pound-bang or hash-pling). The shebang tells the operating system to interpret the rest of the file using that interpreter directive, effectively starting the interpreter and passing the text file along as a parameter.

So, when scripts start with #!/bin/bash or #!/bin/sh, the OS actually runs $ /bin/bash /path/to/script behind the scenes every time you execute the script.

Before you can execute the script, you need to declare it in the system as executable. On Unix systems (macOS included), running $ chmod +x ./script.sh or $ chmod 775 ./script.sh will do the trick.

After you’ve given permissions to your script to be executed, you can run it with $ ./script.sh.

Bash Scripts

A Bash script starts with the bash shebang, followed by a lot of black magic. 😅 For example, to add two numbers that are given as command-line arguments, a script looks like this:

#!/bin/bashecho "$1 + $2 = $(($1 + $2))"

To run it, save it as add.sh and then run the following commands in your Terminal:

$ chmod +x ./add.sh
$ ./add.sh 5 7

The output is going to be 5 + 7 = 12.

It looks pretty simple if you’ve figured out that $index is the command-line argument. I've had to look that up while learning shell scripting.

zx Scripts

Before you can use zx to run scripts, you'll need to install it globally via npm, with $ npm i -g zx. Why didn't you need to install bash? Because bash comes installed by default with Unix systems.

Similarly to all other scripts, a zx script will start with a shebang. This time, a little more complicated, the zx shebang. Followed by a lot of JavaScript. Let's try to recreate the above shell script that adds two numbers given as command-line arguments.

#!/usr/bin/env zxconsole.log(`${process.argv[0]} + ${process.argv[1]} = ${process.argv[0] + process.argv[1]}`)

To run it, save it as add.mjs and then run the following commands in your Terminal:

$ chmod +x ./add.mjs
$ ./add.mjs 5 7

The output is going to be /Users/laka/.nvm/versions/node/v16.1.0/bin/node + /usr/local/bin/zx = /Users/laka/.nvm/versions/node/v16.1.0/bin/node/usr/local/bin/zx 😅. And that's because process.argv, another Node.js wonder, gets called with three extra arguments before you get to 5 and 7. Let's re-write the script to account for that:

#!/usr/bin/env zxconsole.log(`${process.argv[3]} + ${process.argv[4]} = ${process.argv[3] + process.argv[4]}`)

If you run the script now with $ ./add.mjs 5 7, the output is going to be 5 + 7 = 57. Because JavaScript 🤦. And JavaScript thinks those are strings and concatenates them instead of doing math. Re-writing the script again to deal with numbers instead of strings, it looks like:

#!/usr/bin/env zxconsole.log(`${process.argv[3]} + ${process.argv[4]} = ${parseInt(process.argv[3], 10) + parseInt(process.argv[4], 10)}`)

The Bash script looked a lot cleaner, right? I agree. And if I ever need to add two numbers from the command line, a Bash script would be a way better option! Bash doesn’t shine in a lot of other areas, though. Like parsing JSON files. I gave up trying to figure how to parse JSON files halfway through the StackOverflow post explaining it. But this is where zx shines.

I already know how to parse JSON in JavaScript. And here is what the zx script for it looks like, using the built-in fetch module:

#!/usr/bin/env zxlet response = await fetch('https://raw.githubusercontent.com/AlexLakatos/computer-puns/main/puns.json')if (response.ok) {
let puns = await response.json()
let randomPun = Math.floor(Math.random() * puns.length) console.log(chalk.red(puns[randomPun].pun))
console.log(chalk.green(puns[randomPun].punchline))
}

Because I was fancy and used the built-in chalk module, this zx script outputs a random pun from https://puns.dev in the command-line.

Building something similar in shell had me rage-quit halfway through the process. And that's OK. Finding the right tool for the job is what this post was all about.

Originally published at https://alexlakatos.com on May 11, 2021.

Developer Avocado 🥑 Manager @fidelhq by day, @mozillareps & @moztechspeakers by night. I do things for T-Shirts. Made http://puns.dev. 1/2 @DevRelAvocados