Writes and executes JavaScript-based shell scripts using Google's zx library. Use when writing shell scripts, automation, build tools, file processing, CLI tools, deployment scripts, data pipelines, or batch operations. Also covers piping, streams, parallel execution, retries, cross-platform scripting, built-in fs utilities, and minimist argument parsing.
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Use this skill when writing scripts with Google's zx library — the tool that makes shell scripting with JavaScript/TypeScript productive and safe. Read only the reference file(s) needed for the task.
All zx scripts use the lite entry point for portability. Scripts are standalone .mjs files — no project setup required.
#!/usr/bin/env npx zx
import { $, fs, path, glob, chalk } from 'zx/core';
const files = await glob('src/**/*.ts');
await $`eslint ${files}`;Run directly:
chmod +x script.mjs && ./script.mjs
# or
npx zx script.mjszx/core — never bare zx. Avoids the heavy CLI wrapper.$`echo ${userInput}` is safe. Never manually quote interpolated variables.$`cmd ${arrayOfArgs}` expands each element as a separate quoted argument..nothrow() to suppress when you expect failures (e.g., grep with no matches).within() for isolation — creates an async scope with its own $.cwd and $.env. Essential for parallel tasks..pipe() — use $`cmd1`.pipe($`cmd2`) instead of shell |.glob() over find, fs over cat/cp/mv, fetch() over curl.cd() is global — use within() or $({cwd: '/path'}) for scoped directory changes..mjs — for top-level await support without bundler config.| Need | File |
|---|---|
$, ProcessPromise, ProcessOutput, configuration, piping, streams | references/core-api.md |
| Ad-hoc scripts, CLI tools, build scripts, deployment, project scaffolding | references/scripting-patterns.md |
| File processing, data pipelines, batch ops, AI scripts, log analysis | references/processing-recipes.md |
references/scripting-patterns.mdreferences/scripting-patterns.mdreferences/processing-recipes.md$, pipes, streams, config -> references/core-api.md// Core — always start here
import { $, fs, path, glob, chalk } from 'zx/core';
// Additional utilities (import individually as needed)
import { spinner, retry, question, echo, sleep, within,
stdin, tmpdir, tmpfile, which, ps, kill,
quote, YAML, argv, fetch } from 'zx/core';Before executing destructive or bulk operations, always verify scope:
// Verify glob matches before bulk processing
const files = await glob('src/**/*.ts');
console.log(`Found ${files.length} files`);
if (files.length === 0) throw new Error('No files matched — check glob pattern');
if (files.length > 500) throw new Error(`Too many files (${files.length}) — narrow the glob`);
// Dry-run flag for destructive operations
const dryRun = argv['dry-run'] ?? false;
for (const file of files) {
if (dryRun) { console.log(`[dry-run] would delete ${file}`); continue; }
await fs.rm(file);
}For parallel operations, use Promise.allSettled() to handle partial failures:
const results = await Promise.allSettled(
files.map(f => within(async () => $`process ${f}`))
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.error(chalk.red(`${failed.length}/${results.length} tasks failed`));
failed.forEach(r => console.error(r.reason));
process.exitCode = 1;
}#!/usr/bin/env npx zx as the first line$.verbose = false before main logic to suppress command echoingmain().catch() pattern — wrap logic in async function main(), then call main().catch(err => { console.error(chalk.red(err.message)); process.exit(1); })chalk.green() for success, chalk.red() for errors, chalk.yellow() for warnings, chalk.dim() for secondary infospinner() for long-running operations in interactive contextsargv (pre-parsed minimist) for CLI argument handlingtmpdir() and tmpfile() — they auto-clean on exitPromise.all() with within() for parallel operations that need isolation