| name | zx-shell-scripting |
| description | Write advanced shell automation scripts with Google zx, a Node.js tool that wraps subprocess execution with ergonomic template literals. Use when asked to write a shell script, bash script, or automation script in Node.js; automate CLI workflows; spawn subprocesses; pipe commands; process command output; replace a bash script with modern JavaScript; write cross-platform automation; run terminal commands programmatically; or when the user mentions zx, $`command`, shell scripting, subprocess management, or Node.js scripting. Covers: subprocess execution, piping, parallel execution, error handling with nothrow and retry, streaming output, jc JSON parsing integration, and shell one-liner idioms translated to zx. |
| compatibility | Requires: Node.js 16+, zx 8+; optional: jc (pip3 install jc or brew install jc), jq |
| metadata | {"version":"1.0","integrations":"zx,jc,jq,chalk,fs-extra"} |
zx Shell Scripting
Advanced shell automation with Google zx — subprocess execution with the ergonomics of modern JavaScript.
When to Use
- Writing shell scripts, bash scripts, or automation in Node.js
- Spawning subprocesses from a JS/TS project
- Piping commands, capturing output, processing CLI results
- Replacing fragile bash with type-safe, testable Node.js
- Parallel command execution, retry logic, streaming output
- Parsing CLI output as structured JSON (via
jc)
File Modes
#!/usr/bin/env zx
const out = await $`ls -la`
import { $, cd, glob, retry, spinner, within } from 'zx'
Key Patterns
Basic Execution and Output
const branch = await $`git branch --show-current`;
console.log(branch.stdout.trim());
console.log(`${branch}`);
const files = await $`find . -name "*.ts"`.lines();
const data = await $`curl -s https://api.example.com/data`.json();
Auto-Escaping (Shell Injection Defense)
const dir = '/path/with spaces';
const ext = '*.log';
await $`find ${dir} -name ${ext}`;
Piping
const errors = await $`grep -r "ERROR" ./logs`
.pipe($`sort`)
.pipe($`uniq -c`)
.lines();
import { createReadStream } from 'fs';
const result = await $`wc -l`.pipe(createReadStream('big-file.txt'));
Parallel Execution
const [stats, procs, disk] = await Promise.all([$`uptime`, $`ps aux | wc -l`, $`df -h /`]);
const results = await Promise.all(['host1', 'host2', 'host3'].map(h => $`ping -c 1 ${h}`.nothrow()));
const reachable = results.filter(r => r.exitCode === 0);
Error Handling
const result = await $`which docker`.nothrow();
if (!result.ok) {
echo('docker not found, skipping container steps');
}
try {
await $`npm test`;
} catch (err) {
console.error(`Tests failed (exit ${err.exitCode}):\n${err.stderr}`);
process.exit(1);
}
const resp = await retry(5, '2s', () => $`curl -sf https://api.example.com/health`);
Scoped Context
await within(async () => {
$.cwd = '/tmp/build';
$.env = { ...process.env, NODE_ENV: 'production' };
await $`npm run build`;
});
const $$ = $({ quiet: true, nothrow: true, cwd: '/var/log' });
const auth = await $$`tail -n 50 auth.log`;
Streaming Output
for await (const line of $`tail -f /var/log/app.log`) {
if (line.includes('ERROR')) process.stderr.write(`[ALERT] ${line}\n`);
if (line.includes('FATAL')) break;
}
jc Integration — Parse CLI Output as JSON
const procs = await $`ps axu`.pipe($`jc --ps`).json();
const zombies = procs.filter(p => p.stat === 'Z');
const mounts = await $`jc df -h`.json();
const conns = await $`ss -tnp`.pipe($`jc --ss`).json();
const listening = conns.filter(c => c.state === 'LISTEN');
Configuration
$.shell = '/bin/bash';
$.prefix = 'set -euo pipefail;';
$.verbose = false;
$.quiet = true;
$.cwd = process.cwd();
$.timeout = 30_000;
$.env = process.env;
$.preferLocal = true;
await $`noisy-cmd`.quiet();
await $`might-fail`.nothrow();
await $`slow-op`.timeout(60_000);
Gotchas
- Auto-escaping: interpolated values are shell-escaped — adding extra quotes breaks things
set -euo pipefail is ON by default via $.prefix — scripts abort on any error or unset variable
.toString() / string coercion returns trimmed stdout (same as .valueOf()); use .stdout for raw stdout, .stderr for stderr
cd() mutates global process.cwd() — use within() for scoped directory changes to avoid side effects
- ProcessPromise executes immediately — the subprocess starts when
$ is called, not when you await it
.mjs files get all zx globals without import; .ts or .js files must import { ... } from 'zx'
$.quiet = true suppresses all output globally — prefer .quiet() on individual commands
jc requires Python — pip3 install jc or brew install jc; verify with jc --version before use
- Bash globs in
$ strings are still dangerous — always prefix with ./ (not *), use read -d "" with find -print0, and quote "$file" inside loops; prefer zx's glob() for recursive matching (see references/filename-safety.md)
Reference Index
Load these files when you need deeper detail:
| File | Load When |
|---|
references/api-reference.md | Need full API — ProcessPromise methods, ProcessOutput properties, all globals, config table |
references/shell-patterns.md | Need real-world patterns — file ops, networking, jc cookbook, secrets, one-liners |
references/error-handling.md | Need error patterns — signals, retry backoff, graceful shutdown, unhandled rejections |
references/filename-safety.md | Handling filenames with spaces/newlines/dashes/control chars safely (CWE-78, CWE-73) |
references/bash-internals.md | Pure-bash alternatives to external processes — parameter expansion, file ops, IFS, traps |