Write better bash scripts with Zx

Photo by Markus Winkler on Unsplash
Photo by Markus Winkler on Unsplash

Photo by Markus Winkler on Unsplash

As a developer, we usually want to automate the most tasks possible to be productive, deliver faster, and stay focused on what really brings value to the end user.

Those tasks include project scaffolding, a deployment script, database synchronization, etc. We use the Bash language to write these scripts, which works well.

Today we will see how to use Node.js to write a script and run it. The advantage is that we have access to high-level utilities and functions to perform tasks that are usually harder to achieve with Bash.

Prerequisites

To follow this post, you must install these tools on your computer:

Use case

We will write a script to back up the database of our web application running in the production environment is one of the most common use cases. As you guess, my blog has a database (MySQL), and it is paramount to back it up from time to time to avoid data loss in case of a server incident.

The server runs on Ubuntu, but often I need to back up the database of a Web application from my local computer, which is a Macbook Pro so, I wanted my script to be used on both operating systems.

Below is the code I wrote that works well for the case.


#!/bin/bash

if [ $1 == "linux" ]; then
  export PATH=/bin:/usr/bin:/usr/local/bin
elif [ $1 == "macos" ]; then
  export PATH=/bin:/usr/bin:/usr/local/mysql/bin
else
  echo "Only Linux and MacOS are supported!"
  exit 1
fi

TODAY=`date +"%d_%m_%Y"`
 
DB_BACKUP_PATH=$2
MYSQL_HOST=$3
MYSQL_PORT=$4
MYSQL_USER=$5
MYSQL_PASSWORD=$6
DATABASE_NAME=$7

echo "${DATABASE_NAME}-${TODAY}.sql.gz"
 
mkdir -p ${DB_BACKUP_PATH}
# echo "Backup started for database - ${DATABASE_NAME}"
 
mysqldump -h ${MYSQL_HOST} \
   -P ${MYSQL_PORT} \
   -u ${MYSQL_USER} \
   -p${MYSQL_PASSWORD} \
   ${DATABASE_NAME} | gzip > ${DB_BACKUP_PATH}/${DATABASE_NAME}-${TODAY}.sql.gz

if [ $? -eq 0 ]; then
  echo "Database backup performed successfully!"
else
  echo "Fail to backup the database!"
  exit 1
fi

Database's backup script written in bash

Run the script

To execute this script, create a file with the .sh extension, open the file, paste the code below and finally save. Make the file executable to run it as a binary application.


touch backup.sh
nano backup.sh
chmod +x backup.sh

When executing the file, we need to pass 7 arguments; otherwise, the script will not work; this is the syntax:


./backup.sh <os_platform> <save_directory> <db_host> <db_port> <db_user> <db_password> <db_password>

Example:


./backup.sh macos ~/Desktop localhost 3306 teco password my-blog

After running this, you will see a gzipped file in your save directory (Desktop, in my case).

The problem

The script works as expected, but I have some issues with the following:

  • It requires many arguments, and it is easy to confuse when writing them. Is the second argument the host? Is the fifth argument the password? Etc.
  • As a web developer, I can spend a long time without needing to write a script so, every time I have to, I realize I forgot certain syntaxes of Bash language.
  • I took too much time to perform some tasks due to the language not being developer-friendly.

These problems can be solved with Bash but, there is an easier way I want to show you.

This is where Zx comes into action.

Zx making a developer happy.

About Zx

Zx is a tool that makes possible use of Javascript to write scripts. It provides useful wrappers around the child_process module of Node.js.

Here are some advantages of using it:

  • Enjoy features of a high-level language, which makes it easier to write scripts.
  • Scripts are more understandable since it is written in a widely used language.
  • Developers can use the same language they use in daily jobs to write scripts: Less friction.
  • Interactive scripts made easy.
  • You can execute a script located in a remote server.

Installation

Run the command below to install the package globally and verify the command is available in the shell:


npm i -g zx

zx -v

Let's create a file hello.mjs with the code below:


#!/usr/bin/env zx

const firstName = await question('What is your first name? ');

console.log(`Hello ${firstName}`);

Save the file, make it executable, then run it with Zx.

A hello script with Zx
A hello script with Zx

Rewrite the backup script with Zx

Here is the code of the backup.sh file written with Zx (backup.mjs):


#!/usr/bin/env zx

// console.log(process.argv)
const [, , , osPlatform, saveDirectory, dbHost, dbPort, dbUser, dbPassword, dbName ] = process.argv;

let mysqlDumpLocation;

if (osPlatform === "linux") {
    mysqlDumpLocation = "/bin:/usr/bin:/usr/local/bin";
} else if (osPlatform === "macos") {
    mysqlDumpLocation = "/bin:/usr/bin:/usr/local/mysql/bin";
} else {
    console.log("Only Linux and MacOS are supported!");
    process.exit(1);
}

process.env.PATH = [process.env.PATH, mysqlDumpLocation].join(':');

const today = new Intl.DateTimeFormat("en-GB")
    .format(new Date())
    .replace(/\//g, '_');

console.log(`${dbName}-${today}.sql.gz`);

await $`mkdir -p ${saveDirectory}`;

try {
    await $`mysqldump -h ${dbHost} \\
   -P ${dbPort} \\
   -u ${dbUser} \\
   -p${dbPassword} \\
   ${dbName} | gzip > ${saveDirectory}/${dbName}-${today}.sql.gz`;

    console.log("Database backup performed successfully!")
} catch (e) {
    console.log("Fail to backup the database!");
    process.exit(1);
}

Save the file, make it executable, then run it with Zx.

Execute the database backup script with Zx.
Execute the database backup script with Zx.

Improvements

Now our script works with Zx. Let's use some features that Zx provides to improve the user experience.

Disable command output

If you pay attention to the previous picture, you realize the command is printed in the console before being executed. It makes it hard to find the message printed consciously.

To disable this behavior, add the line below at the top of the file after the shebang definition:


#!/usr/bin/env zx

$.verbose = false;

// .......... existing code here ..........

Disable command output in Zx

Detect platform

Now we have access to Node.js modules, and there is a module that provides information about the operating system. We can use it to guess the OS platform, so we don't have to pass it as an argument.


const [, , , saveDirectory, dbHost, dbPort, dbUser, dbPassword, dbName ] = process.argv;

let mysqlDumpLocation;
const osPlatform = os.platform();

if (osPlatform === "linux") {
    mysqlDumpLocation = "/bin:/usr/bin:/usr/local/bin";
} else if (osPlatform === "darwin") {
    mysqlDumpLocation = "/bin:/usr/bin:/usr/local/mysql/bin";
} else {
    console.log("Only Linux and MacOS are supported!");
    process.exit(1);
}

💡
The OS platform for macOS is darwin.

Color the message output

When the backup is done successfully, I want the message to be printed with a green color, and when an error occurs, the red color is used.

Zx has this feature built on top of the node package called chalk. To use it, just wrap your message. chalk.<yourColor>()


console.log(chalk.green("Database backup performed successfully!"));

console.log(chalk.red("Fail to backup the database!"));

Interactive script

We are still passing six arguments when running the script. The goal is to ask these arguments at the runtime. Zx provides a function called question() to ask and retrieve user input. Let's update our code to use it:


#!/usr/bin/env zx

$.verbose = false;

const saveDirectory = await question("Path to save the file? [current directory]: ");
const dbHost = await question("Database host? [localhost]: ");
const dbPort = await question("Database port? [3306]: ");
const dbUser = await question("Database username? ");
const dbPassword = await question("Database password? ");
const dbName = await question("Database name? ");

const finalSaveDirectory = saveDirectory || "./";

await $`mkdir -p ${finalSaveDirectory}`;

// .......... existing code ..........

try {
    await $`mysqldump -h ${dbHost || "localhost"} \\
   -P ${dbPort || 3306} \\
   -u ${dbUser} \\
   -p${dbPassword} \\
   ${dbName} | gzip > ${finalSaveDirectory}/${dbName}-${today}.sql.gz`;

    console.log(chalk.green("Database backup performed successfully!"))
} catch (e) {
    console.log(chalk.red("Fail to backup the database!"));
    process.exit(1);
}

Retrieve database information from user input with Zx

Save the file and run with no argument. You will have the output below:

Interactive script execution with Zx.
Interactive script execution with Zx.

Other useful functions:

We didn't cover all the functions provided by Zx; here are some interesting ones:

  • fetch(): a wrapper around the node-fetch package.
  • sleep(): pause the execution for a specific time.
  • cd(): change the directory.
  • stdin(): returns the stdin as a string.

Wrap up

Writing a script is now easier and better in terms of developer experience. If you are Node.js, you can expect to see the time you take to write scripts decrease drastically with this package because you don't need to switch to another language.

I recommend checking the Zx repository to find other useful commands I didn't cover.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.