Skip to content

zx Cheat Sheet

Overview

zx is a tool from Google that makes it easy to write shell scripts using JavaScript or TypeScript. It provides useful wrappers around child_process, offers built-in support for common utilities like fetch, chalk, fs, os, and yaml, and lets you execute shell commands using tagged template literals. The result is shell scripts that are more readable and maintainable than pure bash.

zx scripts use the .mjs extension by default and run with Node.js. Each shell command returns a ProcessOutput object with stdout, stderr, and exit code. zx also supports piping, command chaining, retries, and concurrent execution, making it ideal for automation tasks, CI/CD scripts, and developer tooling.

Installation

# Install globally
npm install -g zx

# Or use without installing via npx
npx zx script.mjs

# Or add to a project
npm install zx

# Install with bun
bun add -g zx

Running Scripts

# Run a zx script
zx script.mjs

# Run a TypeScript script
zx script.ts

# Run from stdin
echo 'await $`ls -la`' | zx

# Run a remote script
zx https://example.com/script.mjs

# Run with verbose output
zx --verbose script.mjs

# Run in quiet mode
zx --quiet script.mjs

Core Usage

Basic Command Execution

#!/usr/bin/env zx

// Execute a shell command
await $`echo "Hello, World!"`

// Capture output
let result = await $`ls -la`
console.log(result.stdout)

// Use variables in commands (automatically escaped)
let name = "my-project"
await $`mkdir -p ${name}`
await $`cd ${name} && git init`

// Multi-line commands
await $`
  echo "Starting build"
  npm install
  npm run build
`

Working with Output

#!/usr/bin/env zx

// Get stdout as string
let branch = (await $`git branch --show-current`).stdout.trim()
console.log(`Current branch: ${branch}`)

// Get stderr
let result = await $`npm install 2>&1`
console.log(result.stderr)

// Get exit code
let { exitCode } = await $`test -f package.json`.nothrow()
if (exitCode === 0) {
  console.log("package.json exists")
}

// Parse JSON output
let packages = JSON.parse((await $`npm list --json --depth=0`).stdout)

Error Handling

#!/usr/bin/env zx

// Try-catch for failed commands
try {
  await $`git push origin main`
} catch (error) {
  console.error(`Push failed: ${error.stderr}`)
  console.error(`Exit code: ${error.exitCode}`)
}

// nothrow() — don't throw on non-zero exit
let result = await $`grep -r "TODO" src/`.nothrow()
if (result.exitCode !== 0) {
  console.log("No TODOs found")
}

// Retry on failure
await retry(3, () => $`curl -f https://api.example.com/health`)

Piping

#!/usr/bin/env zx

// Pipe commands
let count = await $`find . -name "*.js"`.pipe($`wc -l`)
console.log(`JavaScript files: ${count.stdout.trim()}`)

// Chain multiple pipes
await $`cat access.log`
  .pipe($`grep "ERROR"`)
  .pipe($`sort`)
  .pipe($`uniq -c`)
  .pipe($`sort -rn`)
  .pipe($`head -10`)

Built-in Utilities

UtilityDescription
$Execute shell commands
cd()Change working directory
fetch()HTTP requests (node-fetch)
question()Interactive user input
sleep()Async sleep
echo()Print to stdout
stdin()Read from stdin
retry()Retry failed operations
spinner()Show a loading spinner
chalkTerminal string styling
fsFile system operations (fs-extra)
osOS information
pathPath utilities
yamlYAML parse/stringify
globFile globbing
whichFind executables in PATH
argvParsed CLI arguments (minimist)

Examples

#!/usr/bin/env zx

// Change directory
cd("/tmp")
await $`pwd`  // /tmp

// HTTP fetch
let resp = await fetch("https://api.github.com/repos/google/zx")
let data = await resp.json()
console.log(`Stars: ${data.stargazers_count}`)

// Interactive input
let name = await question("Project name: ")
let useTS = await question("Use TypeScript? (y/n) ")

// Sleep
await sleep(2000) // 2 seconds

// File operations
await fs.writeJson("config.json", { name, typescript: useTS === "y" })
let config = await fs.readJson("config.json")

// Glob files
let files = await glob("src/**/*.{ts,tsx}")
console.log(`Found ${files.length} TypeScript files`)

// Spinner
await spinner("Installing dependencies...", () => $`npm install`)

// YAML
let yamlContent = yaml.parse(await fs.readFile("config.yaml", "utf8"))

Configuration

Script Configuration

#!/usr/bin/env zx

// Set shell (default: bash)
$.shell = "/bin/zsh"

// Set verbose mode
$.verbose = true

// Set quiet mode (suppress command output)
$.quiet = true

// Set environment variables
$.env = { ...process.env, NODE_ENV: "production" }

// Set default cwd
$.cwd = "/path/to/project"

// Set command prefix
$.prefix = "set -euo pipefail;"

// Set timeout for commands (ms)
$.timeout = 30000

Package.json Integration

{
  "scripts": {
    "deploy": "zx scripts/deploy.mjs",
    "setup": "zx scripts/setup.mjs",
    "release": "zx scripts/release.mjs"
  },
  "devDependencies": {
    "zx": "^8.0.0"
  }
}

Advanced Usage

Concurrent Execution

#!/usr/bin/env zx

// Run commands in parallel
let [lint, test, typecheck] = await Promise.all([
  $`npm run lint`.nothrow(),
  $`npm run test`.nothrow(),
  $`npm run typecheck`.nothrow(),
])

// Process results
let results = [
  { name: "Lint", ...lint },
  { name: "Test", ...test },
  { name: "Typecheck", ...typecheck },
]

for (let r of results) {
  console.log(`${r.name}: ${r.exitCode === 0 ? "PASS" : "FAIL"}`)
}

Complex Deployment Script

#!/usr/bin/env zx

$.verbose = true

let env = argv.env || "staging"
let branch = (await $`git branch --show-current`).stdout.trim()
let isDirty = (await $`git status --porcelain`).stdout.trim().length > 0

if (isDirty) {
  console.log(chalk.red("Working directory is dirty. Commit or stash changes."))
  process.exit(1)
}

if (env === "production" && branch !== "main") {
  console.log(chalk.red("Production deploys must be from main branch"))
  process.exit(1)
}

console.log(chalk.blue(`Deploying ${branch} to ${env}...`))

await spinner("Running tests...", () => $`npm test`)
await spinner("Building...", () => $`npm run build`)
await spinner("Deploying...", () => $`rsync -avz dist/ server:/var/www/${env}/`)

console.log(chalk.green(`Deployed to ${env} successfully!`))

TypeScript Support

// script.ts
import { $ } from "zx"

interface DeployConfig {
  env: string
  region: string
  replicas: number
}

const config: DeployConfig = {
  env: "production",
  region: "us-east-1",
  replicas: 3,
}

await $`kubectl set replicas=${config.replicas} --namespace=${config.env}`

CLI Argument Parsing

#!/usr/bin/env zx

// argv is parsed by minimist
// zx script.mjs --name=myapp --port 3000 --verbose

let name = argv.name || "default-app"
let port = argv.port || 8080
let verbose = argv.verbose || false

if (verbose) $.verbose = true

console.log(`Starting ${name} on port ${port}`)
await $`docker run -p ${port}:${port} ${name}`

Troubleshooting

IssueSolution
command not found: zxInstall globally: npm i -g zx or use npx zx
Variables not escaped properlyUse template literals ${var} inside $\…“; zx auto-escapes
Command hangsSet a timeout: $.timeout = 30000
Colors not showingEnsure terminal supports ANSI colors; don’t pipe to file
TypeScript errorsEnsure zx types are available; use import { $ } from 'zx'
Permission deniedAdd shebang #!/usr/bin/env zx and chmod +x script.mjs
fetch not definedUpgrade to zx v7+; fetch is built-in
Stdin not workingUse let input = await stdin() to read piped input
Exit code not capturedUse .nothrow() to prevent throwing on non-zero exits