[One Package Per Day] Why We Use zx to Replace Bash Scripts

By hientd, at: July 7, 2025, 11:12 a.m.

Estimated Reading Time: __READING_TIME__ minutes

[One Package Per Day] Why We Use zx to Replace Bash Scripts
[One Package Per Day] Why We Use zx to Replace Bash Scripts

If you’ve ever stared at a Bash script and thought, “There’s got to be a better way,” welcome to the club.

 

At Glinteco, we maintain a lot of internal automation, dev setup scripts, deployment flows, backups, Docker orchestration helpers. For a while, we defaulted to Bash. But as scripts got more complex, we started feeling the limits: quoting hell, poor error handling, lack of structure. Enter zx, a Node.js-based tool that makes writing shell scripts actually enjoyable.

 

Let’s walk through why we’re now using zx for most of our scripting needs.

 

What is zx?

 

Created by the team at Google, zx is a library that lets you write shell scripts in JavaScript (or TypeScript). Think of it as Node.js with superpowers like awaitable shell commands, built-in utilities like fs, os, and even globs.

 

Instead of:

#!/bin/bash
echo "Hello, $1"

 

You write:

 

#!/usr/bin/env zx
console.log(`Hello, ${process.argv[2]}`);

 

And suddenly your scripts are readable, testable, and maintainable.

 

Why We Ditched Bash for zx

 

1. Readable and Familiar Syntax

 

Most of our team is already fluent in JavaScript and TypeScript. With zx, we’re no longer fighting syntax oddities like:

 

if [[ -z "$VAR" ]]; then ...

 

Now we write:

 

if (!process.env.VAR) { ... }

 

2. Async/Await for Free

 

Ever tried to parallelize Bash processes with &, wait for them, and handle failures cleanly? We have. It sucked.

 

With zx, we do:

 

await Promise.all([
  $`command1`,
  $`command2`
]);

 

Boom. Easy concurrency, built-in.

 

3. Safer and Cleaner

 

With Bash, one unescaped space or forgotten quote can nuke your system. In zx, the `$`` string interpolation safely escapes arguments by default:

 

const branch = 'main';
await $`git checkout ${branch}`;

 

No more worrying about injection or weird characters.

 

4. Standard Library Goodness

 

You get:

 

  • fs, path, os modules
     

  • chalk, globby, yaml, etc.
     

  • Top-level await

 

Want to read a config and use it? Easy:

 

import fs from 'fs/promises';
const config = JSON.parse(await fs.readFile('./config.json', 'utf8'));

 

5. Cross-Platform Friendly

 

Bash is not always Bash. (Looking at you, macOS vs Linux vs Git Bash on Windows.)

 

With Node + zx, your scripts run the same everywhere Node runs. And that’s a big deal when onboarding devs or automating CI/CD pipelines.

 

Real Use Case: Our Dev Setup Script

 

Before:

 

#!/bin/bash
echo "Setting up..."
cd backend || exit 1
pip install -r requirements.txt
cd ../frontend || exit 1
npm install

 

After:

 

#!/usr/bin/env zx

await $`cd backend && pip install -r requirements.txt`
await $`cd ../frontend && npm install`

console.log(' Setup complete!');

 

We even added checks like missing Python or Node versions and threw user-friendly errors, all easier in JS.

 

Downsides?

 

Let’s be honest, there are a few trade-offs.

 

  • You need Node.js installed.
     

  • Performance is slightly slower than Bash for very tiny scripts.
     

  • You can’t run it where Node isn’t allowed (e.g., strict CI containers).

 

But in 95% of our use cases, the benefits far outweigh the drawbacks.

 

Final Thoughts

 

We still use Bash for tiny one-liners. But for anything over 10 lines, zx wins (hands down). It’s readable, powerful, and lets us write scripts without feeling like we’re sacrificing our sanity.

 

If you’re scripting a lot and your team is already comfortable with JavaScript or TypeScript, try zx. You’ll probably never go back.

 

Tag list:
- modern shell scripting
- zx tutorial
- javascript shell scripting
- bash script replacement
- nodejs zx
- zx vs bash

Subscribe

Subscribe to our newsletter and never miss out lastest news.