zx is Google's tool for writing shell scripts in JavaScript/TypeScript. It wraps child_process, auto-escapes arguments, and provides sensible defaults — giving you the power of the JavaScript ecosystem in your scripts.
js
#!/usr/bin/env zx
await $`cat package.json | grep name`
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
const name = 'foo & bar'
await $`mkdir /tmp/${name}` // No quotes needed — auto-escaped
Bash is great for simple tasks, but when scripts grow complex, a full programming language helps. zx adds helpful wrappers around child_process, escapes arguments, and gives sensible defaults. Think: bash + JavaScript in one script.
Also triggers when users ask about running shell commands in JavaScript, converting bash scripts to zx, executing remote scripts, Markdown scripts, or TypeScript shell scripts.
bash
npm install zx
Write scripts as .mjs files (supports top-level await). Add #!/usr/bin/env zx shebang or run via CLI:
bash
zx ./script.mjs # Direct execution
npx zx ./script.mjs # Via npx
node --import zx/globals # As Node.js loader
All functions ( $, cd, fetch, etc.) are globally available in zx scripts without imports. For explicit imports (better VS Code autocomplete):
js
import 'zx/globals'
$`command` — Execute Shell Commands#The tagged template literal is the heart of zx. Everything in ${...} is auto-escaped and quoted.
js
// Async (standard) — returns ProcessPromise
const output = await $`ls -la`
// Sync variant — returns ProcessOutput directly
const dir = $.sync`pwd`
// Arrays are flattened
const flags = ['--oneline', '--decorate', '--color']
await $`git log ${flags}`
// Non-zero exit codes throw ProcessOutput
try {
await $`exit 1`
} catch (p) {
console.log(`Exit: ${p.exitCode}, Error: ${p.stderr}`)
}
$({...})#Create custom $ instances with preset options — chainable and composable:
js
const $$ = $({ verbose: false, env: { NODE_ENV: 'production' } })
const pwd = $$.sync`pwd`
// Presets are chainable
const $1 = $({ nothrow: true })
const $2 = $1({ sync: true }) // Both nothrow + sync applied
text
$`cmd` ProcessPromise (extends Promise)
├── .pipe() Stream piping
├── .kill() Terminate process
├── .text() Output as string
├── .json() Output as parsed JSON
├── .lines() Output split by lines
├── .nothrow() Suppress errors for this command
├── .quiet() Suppress output for this command
├── .timeout() Auto-kill after duration
├── .stdio() Configure I/O
├── .exitCode Promise<exit code>
├── .stdout Readable stream
├── .stderr Readable stream
├── .stdin Writable stream
├── .pid / .cmd Process metadata
└── await → ProcessOutput
├── .stdout string
├── .stderr string
├── .exitCode number
├── .signal string|null
├── .text() / .json() / .lines() / .buffer() / .blob()
└── .ok boolean (when nothrow)
When writing zx scripts, use this decision tree:
GoalApproachRun a commandawait $`cmd` Run synchronously$.sync`cmd` Pipe output.pipe($`next`) / .pipe('file.txt')Handle errors gracefully$({nothrow: true}) / .nothrow()Set timeout$({timeout: '30s'}) / .timeout('30s')Parse JSON output(await $`cmd`).json()Real-time streamingfor await (const line of $`cmd`)Retry on failureretry(5, () => $`cmd`)User promptquestion('Name: ')Progress indicatorawait spinner('Working...', () => $`cmd`)Change directorycd('/path') or within(() => { $.cwd = '/tmp' })Temp files/dirstmpfile() / tmpdir()Parse CLI argsargv.flag or minimist(process.argv.slice(2))Load .env filedotenv.config('.env')
js
const results = await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
js
$.nothrow = true
const repos = ['zx', 'webpod']
const clones = repos.map(n => $`git clone https://github.com/google/${n}`)
const results = await Promise.all(clones)
const errors = results.filter(o => !o.ok).map(o => o.stderr.trim())
console.log('Errors:', errors.join('\n'))
js
// Chain commands like bash pipes
const greeting = await $`printf "hello"`
.pipe($`awk '{printf $1", world!"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
// Pipe to file
await $`echo "Hello!"`.pipe('/tmp/output.txt')
// Real-time output to terminal
await $`echo 1; sleep 1; echo 2; sleep 1; echo 3`.pipe(process.stdout)
js
// Split one source to multiple consumers
const p = $`some-command`
const [o1, o2] = await Promise.all([
p.pipe`log`,
p.pipe`extract`,
])
// Merge multiple sources
const $h = $({ halt: true })
const p1 = $`echo foo`
const p2 = $h`echo a && sleep 0.1 && echo b`
const p3 = $h`echo c && sleep 0.1 && echo d`
const cat = $h`cat`
p1.pipe(cat); p2.pipe(cat); p3.pipe(cat)
await cat.run()
js
const p = $`echo '{"foo":"bar"}\nline2'`
await p.json() // { foo: 'bar' }
await p.lines() // ['{"foo":"bar"}', 'line2']
await p.text() // '{"foo":"bar"}\nline2\n'
js
for await (const line of $`git log --oneline --max-count=5`) {
console.log(line)
}
zx defaults to bash. Switch shells as needed:
js
import { useBash, usePowerShell, usePwsh } from 'zx'
usePowerShell() // PowerShell.exe
usePwsh() // PowerShell v7+
useBash() // Back to bash
// Manual override
$.shell = '/bin/zsh'
On Windows, consider using WSL or Git Bash for bash support, or switch to PowerShell via usePowerShell().
HelperPurposeExamplecd()Change directorycd('/tmp')``fetch()HTTP requests, supports .pipe()``fetch('https://api.example.com')``question()Interactive user inputquestion('Name: ')``sleep()Delay executionawait sleep(1000)``echo()Print to stdoutecho`Status: ${p}` ```stdin()`Read stdin`JSON.parse(await stdin())within()Isolated async config contextwithin(() => { $.cwd = '/tmp' })retry()`Retry with delay/backoff`retry(5, () => $` curl url `)spinner()CLI progress indicatorawait spinner(() => $long-cmd)glob()`File glob matching (globby)`glob('**/*.js')which()Find executable pathawait which('node')ps`Cross-platform process list`ps.lookup({ command: 'node' })tmpdir()Temp directorytmpdir('sub')tmpfile()`Temp file`tmpfile('f.txt', 'content')argvParsed CLI argumentsargv.verbose``dotenv.env file loadingdotenv.config('.env')`
Exposed npm packages: chalk (colors), fs (fs-extra), os, path, YAML (yaml), minimist.
$ options, cd(), fetch(), question(), sleep(), echo(), stdin(), within(), retry(), spinner(), glob(), which(), ps, kill(), tmpdir(), tmpfile(), minimist, argv, chalk, fs, os, path, YAML, dotenv, quote(), useBash(), usePowerShell(), usePwsh()$.options: shell, prefix, postfix, quote, verbose, quiet, env, cwd, timeout, nothrow, detached, preferLocal, spawn, kill, log, input, signal, stdio, halt, delimiter, defaults