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 the value to the end-user. Those tasks can be project scaffolding, a deployment script, database synchronization, etc. We use the Bash language to write these scripts, and it 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.

Use case

Write a script to backup 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 backup it from time to time to avoid data loss in case of a server incident. The server has Ubuntu as the operating system. Often I need to back up the database of the web application I'm working on in 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

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 be able 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. Here 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).

Problem

The script works as expected, but I have some issue with:

  • It requires many arguments, and it easy to confuse when writing them. Is the second argument is the host? Is the fifth argument is 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 that is not developer-friendly.

These problems can be solved with Bash but, there is easier I want to you.
Here is where Zx comes into action 🤩.

Zx making a developer happy 🎉

About Zx

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

Here are some advantages to use it:

  • Enjoy features of a high-level language, which makes it easier to write scripts.
  • Scripts are more understandable since it is written with 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

Zx requires Node.js to be installed so, make sure you have it installed before continuing:

Install the package globally:

npm i -g zx

zx -v
Install Zx globally and make sure installation went successfully.

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.

Hello script with Zx 🎉

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.

Database backup script ran with Zx.

Improvements

Now our script works with Zx. Let's use some features provided by Zx to improve the 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 the 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);
}

Note that 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 green color and when an error occurred, use the red color. 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

We reached the end of this tutorial and as you see, write a script is now easier and better. I expect to see the time I take to write my script decrease drastically with this tool. I recommend checking the repository to find other useful commands I didn't cover.

Thank you, and see you at the next tutorial.