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))"
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 zx
console.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 zx
console.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 zx
console.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 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))
}
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.
Top comments (10)
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 likeminimist
orarg
.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
orjs-fire
. But when I was trying to use arguments inzx
even if for the sake of my example, I did miss a better way to parse arguments thanprocess.argv
. Hopefully it's something they'll add in the near future.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.
That would be an interesting use of this
npm install
feature.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 ofzx
.This was a funny one! Especially the math part 😂
Aww, thank you! I do what I can 😅
I don't think I could ever write like that 😃
Sorry I don't get it, but how is that different from executing
node ./my-script-file.js
?That means I have to have a
package.json
next tomy-script-file.js
, and install dependencies. It also doesn't have access to$(unix-command)
, I'd have to runexec()
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 didnode ./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.If you're interested, I wrote a python version that's much cleaner and easier to use (imo) 😉 Available here