Automating your personal workflow with zx

Taylor Ray Howard
In the weeds
Published in
7 min readMar 9, 2023
Image by Gerald Altman

What is zx?

zx is a library from Google that allows you to write JavaScript for more complex bash scripts without the complexities of dealing with the NodeJS standard library.

#!/usr/bin/env zx

await $`cat package.json | grep name`

let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`

await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])

let name = 'foo bar'
await $`mkdir /tmp/${name}`

Why use zx?

Many of us have a repeatable set of steps that makes up our workflow. For me it boils down to two things when starting a new ticket:

  1. Pull main, update local database and dependencies
  2. Switch to new branch

Wouldn’t it be great if I could easily write a language I already know with easy interoperability with bash? I ran across zx, and it seemed to fix the bill. For me, I just want to run some bash in my JavaScript and have an easy way to digest the output. zx offers that simply by just awaiting a function call with the $ and a template string. Let’s say I wanted to ls my current directory, all I need to do is await the command like so

// Note this must be a template string
const result = await $`ls`;

console.log(result);

/*
ProcessOutput {
stdout: 'file1.js\nfile2.js\nfile3.js\ntest.mjs\n',
stderr: '',
signal: null,
exitCode: 0
}
*/

The beauty of zx lies in the simplicity. I get the stdout output of my command, the stderr output of my command, the signal, and the exist code. I can accomplish a lot with that.

How to use zx?

Like I mentioned above. We’ve got inputs and outputs of bash with the power of JavaScript. The possibilities are endless! But let’s just try to solve a couple of the points we had initially before solving something more exciting.

0. Installation

npm i -g zx

Requirement: Node version >= 16.0.0

  1. Pulling main and updating local environment

My task here is simple with 2 steps. I want to 1) switch to main and 2) do a git pull . Let’s see how that looks. Create a file named start-new-card.mjs

#!/usr/bin/env zx

console.log('Changing branch to main');
// This assumes you're working in the directory you want to be in. Try out cd('path') if you want to use a working directory.
await $`git checkout main`
console.log('Calling git pull');
await $`git pull`

If you don’t know what the first line is, check out the Shebang operator and it’ll make more sense. Basically, it’s just telling the shell how to understand the file. Save that file and make it executable with a chmod +x start-new-card.mjs I would highly recommend only doing this with sources that you trust. Now we can run it ./start-new-card.mjs and you should get something like this

./start-new-card.mjs                                                                                                             ✔  10337  14:14:31
Changing branch to main
$ git checkout main
Already on 'main'
M start-new-card.mjs
Your branch is up to date with 'origin/main'.
Calling git pull
$ git pull
Already up to date.

We can see the command we ran, the ouput of our command, and our console.log calls as well. You can see in my output we can see the stdout of the git checkout main command as well where it says Already on 'main.' You can mute these outputs with the flag --quiet, but we’ll keep them on.

Now that we’ve got our git branch updated, we can update our local environment. This will be specific to your use-case, but I’m working primarily with Ruby on Rails day-to-day so I’ll use that as an example. If you’re using dotnet maybe you’ll need to do a dotnet restore in case there were new packages or maybe you’re in NodeJS and you need to run a yarn/npm install or maybe you have a bunch of things you need to do and could create a whole zx script for it. Here, I’m just going to call a script we already have.

#!/usr/bin/env zx

console.log('Changing branch to main');
// This assumes you're working in the directory you want to be in. Try out cd('path') if you want to use a working directory.
await $`git checkout main`

console.log('Calling git pull');
await $`git pull`;

console.log('Updating local dependencies');
await $`./script/local/update.sh`;

That’s it for problem 1. We’ve got ourselves in the state of “we’re on the most up to date main branch with an updated local environment” which sets us up nicely to switch to a new branch.

2. Switch to a new branch

Using what we have so far, this shouldn’t be too tough. We need to take in an argument from the user in the script so that we can name our branch. I typically make a request to the Jira API to find my “In Progress” ticket and give it the same name as the card id, but we’ll start with just taking in an argument.

Before, we were running the program with

./start-new-card.mjs

and now we can call it with

./start-new-card.mjs BRANCH_NAMAE

to pass in an argument. To get that argument in our JavaScript we need to use process.argv, but it has a gotcha. You might think that it’s an array with your args, but there’s 3 special args always there. process.argv[0] will always be your node path, process.argv[1] will always be your zx path, and process.argv[2] will always be the path of the currently-running script; thus all of the passed-in arguments will start at process.argv[3]. Let’s try modifying our script to just console.log the args.

#!/usr/bin/env zx

console.log('Changing branch to main');
// This assumes you're working in the directory you want to be in. Try out cd('path') if you want to use a working directory.
await $`git checkout main`
console.log('Calling git pull');
await $`git pull`

console.log(process.argv[0], process.argv[1], process.argv[2], process.argv[3])

If you run ./start-new-card.mjs arg1 arg2 you should see some output like this

./start-new-card.mjs arg1 arg2                                                                                                   ✔  10435  07:17:44
Changing branch to main
$ git checkout main
Already on 'main'
M start-new-card.mjs
Your branch is up to date with 'origin/main'.
Calling git pull
$ git pull
Already up to date.
/Users/taylor.howard/.nodenv/versions/14.16.0/bin/node /Users/taylor.howard/.nodenv/versions/14.16.0/bin/zx ./start-new-card.mjs arg1

Your paths might be different, but this proves that the first 3 arguments are not useful for us here. Now we can take in that argument and create a git branch with it.

#!/usr/bin/env zx

console.log('Changing branch to main');
// This assumes you're working in the directory you want to be in. Try out cd('path') if you want to use a working directory.
await $`git checkout main`
console.log('Calling git pull');
await $`git pull`

const branchName = process.argv[3];
await $`git checkout -b ${branchName}`;

But what if we forgot to include the branch name? zx will throw an error for us, but we can prevent that.

#!/usr/bin/env zx

console.log('Changing branch to main');
// This assumes you're working in the directory you want to be in. Try out cd('path') if you want to use a working directory.
await $`git checkout main`
console.log('Calling git pull');
await $`git pull`

const askQuestion = () => question('What should the branch be named?\n')

let branchName = process.argv[3] ?? await askQuestion();
while (!branchName) {
branchName = await askQuestion();
}

await $`git checkout -b ${branchName}`;

Now we force the user to either provide a branch name via arguments, or we’ll prompt them when it’s time. Neat!

If we run the script with an argument, we’ll see that it switches branches successfully to the passed-in branch.

./start-new-card.mjs test                                                                                                   ✔  10446  07:24:43
Changing branch to main
$ git checkout main
Switched to branch 'main'
M start-new-card.mjs
Your branch is up to date with 'origin/main'.
Calling git pull
$ git pull
Already up to date.
$ git checkout -b test
Switched to a new branch 'test'

And if we forget, we’ll be prompted and forced to give a branch name.

./start-new-card.mjs                                                                                                             ✔  10451  07:26:44
Changing branch to main
$ git checkout main
Already on 'main'
M start-new-card.mjs
Your branch is up to date with 'origin/main'.
Calling git pull
$ git pull

Already up to date.
What should the branch be named?

What should the branch be named?

What should the branch be named?
test2
$ git checkout -b test2
Switched to a new branch 'test2'

The blank answers to our question caused a re-prompting, which helps us by requiring the correct state before moving forward.

3. Adding them to our path

Now that we’ve got this useful workflow tool, I want to be able to run it. We can aid in this process by adding our folder to our path. I’m using zsh, so I just appended this to my .zshrc.

export PATH="/Users/taylor.howard/path/to/folder:$PATH"

And now after we restart zsh, we have access to our script.

start-new-card.mjs                                                                                                              ✔  10001  08:02:44
Changing branch to main
$ git checkout main
Switched to branch 'main'
M start-new-card.mjs
Your branch is up to date with 'origin/main'.
Calling git pull
$ git pull
Already up to date.
What should the branch be named?

Conclusion

Here, we can see that zx is extremely powerful. It offers a simple way to interface between bash and JavaScript, which allows us to solve our repetitive workflow problems easily.

--

--