DEV Community

Cover image for Meet zx: A Better Way to Write Scripts with Node.js
Alex Lakatos 🥑
Alex Lakatos 🥑

Posted on • Originally published at alexlakatos.com on

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/bash

echo "$1 + $2 = $(($1 + $2))"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 zx

console.log(`${process.argv[0]} + ${process.argv[1]} = ${process.argv[0] + process.argv[1]}`)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 zx

console.log(`${process.argv[3]} + ${process.argv[4]} = ${process.argv[3] + process.argv[4]}`)
Enter fullscreen mode Exit fullscreen mode

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 zx

console.log(`${process.argv[3]} + ${process.argv[4]} = ${parseInt(process.argv[3], 10) + parseInt(process.argv[4], 10)}`)
Enter fullscreen mode Exit fullscreen mode

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 zx

let 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))
}
Enter fullscreen mode Exit fullscreen mode

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.

Computer Pun in Terminal via zx

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.

Top comments (10)

Collapse
 
vonheikemen profile image
Heiker

I saw that the other day, I must say I like a lot because it looks similar to a thing I did a while ago. But there is a "missed oportunity" in this tool. The implementation of $ is not really cross-platform, which is quite a shame. Also, it would be nice if they provide an argument parser like minimist or arg.

Collapse
 
lakatos88 profile image
Alex Lakatos 🥑

Yeah, I think it's still very new, and might mature in the future. I have high hopes for it.

I don't usually think about shell scripts as accepting arguments, I usually build CLIs for that use case, with things like oclif or js-fire. But when I was trying to use arguments in zx even if for the sake of my example, I did miss a better way to parse arguments than process.argv. Hopefully it's something they'll add in the near future.

Collapse
 
vonheikemen profile image
Heiker

Looking throught the issues it does appear like the maintainer wants to keep things as simple as possible. Doesn't look too eager to change things. Anyway, is still a good project.

You know I just remembered something: one can actually install this tool directly from github. So, people could fork this project, tweak a little bit and then install the fork like this.

npm install --global github:<some-user-name>/zx
Enter fullscreen mode Exit fullscreen mode

That would be an interesting use of this npm install feature.

Thread Thread
 
lakatos88 profile image
Alex Lakatos 🥑

Oh, that's a really good idea. Especially since the work required would be minimal, for example using minimist for arguments would imply adding a few lines for importing the package and exposing it within the scope of zx.

Collapse
 
siddharthshyniben profile image
Siddharth

This was a funny one! Especially the math part 😂

Collapse
 
lakatos88 profile image
Alex Lakatos 🥑

Aww, thank you! I do what I can 😅

Collapse
 
siddharthshyniben profile image
Siddharth

I don't think I could ever write like that 😃

Collapse
 
frondor profile image
Federico Vázquez

Sorry I don't get it, but how is that different from executing node ./my-script-file.js ?

Collapse
 
lakatos88 profile image
Alex Lakatos 🥑

That means I have to have a package.json next to my-script-file.js, and install dependencies. It also doesn't have access to $(unix-command), I'd have to run exec() in my script file, and then worry about sanitizing those commands.

zx has a bunch of defaults and modules out of the box, so it conveniently lets me make my scripts portable and easier to write without having to worry about a bunch of the boilerplate and legwork I would need if I did node ./my-script-file.js. It's the same argument I guess of using any library over writing it from scratch yourself instead. Convenience. It also comes with the same drawbacks of using libraries, mostly trust issues.

Collapse
 
tusharsadhwani profile image
Tushar Sadhwani

If you're interested, I wrote a python version that's much cleaner and easier to use (imo) 😉 Available here