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
| Utility | Description |
|---|---|
$ | 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 |
chalk | Terminal string styling |
fs | File system operations (fs-extra) |
os | OS information |
path | Path utilities |
yaml | YAML parse/stringify |
glob | File globbing |
which | Find executables in PATH |
argv | Parsed 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
| Issue | Solution |
|---|---|
command not found: zx | Install globally: npm i -g zx or use npx zx |
| Variables not escaped properly | Use template literals ${var} inside $\…“; zx auto-escapes |
| Command hangs | Set a timeout: $.timeout = 30000 |
| Colors not showing | Ensure terminal supports ANSI colors; don’t pipe to file |
| TypeScript errors | Ensure zx types are available; use import { $ } from 'zx' |
| Permission denied | Add shebang #!/usr/bin/env zx and chmod +x script.mjs |
fetch not defined | Upgrade to zx v7+; fetch is built-in |
| Stdin not working | Use let input = await stdin() to read piped input |
| Exit code not captured | Use .nothrow() to prevent throwing on non-zero exits |