Zx — 用 JavaScript 编写更优秀的 Shell 脚本
概述
zx 是 Google 推出的工具,让你能用 JavaScript/TypeScript 编写 Shell 脚本。它封装了 child_process,自动转义参数,并提供合理的默认值 —— 让你在脚本中发挥 JavaScript 生态系统的全部威力。
#!/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}` // 无需引号 — 自动转义
Bash 擅长简单任务,但当脚本变得复杂时,一门完整的编程语言会很有帮助。zx 在 child_process 周围添加了便利的封装,自动转义参数,并提供合理的默认值。可以理解为:bash + JavaScript 合二为一。
触发条件
当用户询问关于在 JavaScript 中运行 Shell 命令、将 Bash 脚本转换为 zx、执行远程脚本、Markdown 脚本或 TypeScript Shell 脚本时,也会触发此技能。
快速开始
npm install zx
将脚本编写为 .mjs 文件(支持顶级 await)。添加 #!/usr/bin/env zx shebang 或通过 CLI 运行:
zx ./script.mjs # 直接执行
npx zx ./script.mjs # 通过 npx
node --import zx/globals # 作为 Node.js 加载器
在 zx 脚本中,所有函数($, cd, fetch 等)都是全局可用的,无需导入。如需显式导入(以获取更好的 VS Code 自动补全):
import 'zx/globals'
核心概念
$`command` — 执行 Shell 命令
Tagged template literal 是 zx 的核心。${...} 中的所有内容都会被自动转义并加引号。
// 异步(标准方式)— 返回 ProcessPromise
const output = await $`ls -la`
// 同步变体 — 直接返回 ProcessOutput
const dir = $.sync`pwd`
// 数组会被展平
const flags = ['--oneline', '--decorate', '--color']
await $`git log ${flags}`
// 非零退出码会抛出 ProcessOutput
try {
await $`exit 1`
} catch (p) {
console.log(`Exit: ${p.exitCode}, Error: ${p.stderr}`)
}
使用 $({...}) 进行预设配置
创建具有预设选项的自定义 $ 实例——可链式调用和组合:
const $$ = $({ verbose: false, env: { NODE_ENV: 'production' } })
const pwd = $$.sync`pwd`
// 预设可链式调用
const $1 = $({ nothrow: true })
const $2 = $1({ sync: true }) // 同时应用 nothrow 和 sync
ProcessPromise 和 ProcessOutput
$`cmd` ProcessPromise (继承 Promise)
├── .pipe() 流管道
├── .kill() 终止进程
├── .text() 以字符串形式输出
├── .json() 以解析后的 JSON 形式输出
├── .lines() 按行分割输出
├── .nothrow() 为此命令抑制错误
├── .quiet() 为此命令抑制输出
├── .timeout() 在指定时间后自动终止
├── .stdio() 配置 I/O
├── .exitCode Promise<退出码>
├── .stdout 可读流
├── .stderr 可读流
├── .stdin 可写流
├── .pid / .cmd 进程元数据
└── await → ProcessOutput
├── .stdout string
├── .stderr string
├── .exitCode number
├── .signal string|null
├── .text() / .json() / .lines() / .buffer() / .blob()
└── .ok boolean (当使用 nothrow 时)
决策树
编写 zx 脚本时,请遵循此决策树:
| 目标 | 方法 |
|---|---|
| 运行一个命令 | await $`cmd` |
| 同步运行 | $.sync`cmd` |
| 管道输出 | .pipe($`next`) / .pipe('file.txt') |
| 优雅地处理错误 | $({nothrow: true}) / .nothrow() |
| 设置超时 | $({timeout: '30s'}) / .timeout('30s') |
| 解析 JSON 输出 | (await $`cmd`).json() |
| 实时流式处理 | for await (const line of $`cmd`) |
| 失败时重试 | retry(5, () => $`cmd`) |
| 用户提示 | question('Name: ') |
| 进度指示器 | await spinner('Working...', () => $`cmd`) |
| 更改目录 | cd('/path') 或 within(() => { $.cwd = '/tmp' }) |
| 临时文件/目录 | tmpfile() / tmpdir() |
| 解析 CLI 参数 | argv.flag 或 minimist(process.argv.slice(2)) |
| 加载 .env 文件 | dotenv.config('.env') |
编写高效的 zx 脚本
并行执行
const results = await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
使用 nothrow 处理错误
$.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'))
流管道
// 像 bash 管道一样链式调用命令
const greeting = await $`printf "hello"`
.pipe($`awk '{printf $1", world!"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
// 管道到文件
await $`echo "Hello!"`.pipe('/tmp/output.txt')
// 实时输出到终端
await $`echo 1; sleep 1; echo 2; sleep 1; echo 3`.pipe(process.stdout)
流拆分与合并
// 将一个源拆分成多个消费者
const p = $`some-command`
const [o1, o2] = await Promise.all([
p.pipe`log`,
p.pipe`extract`,
])
// 合并多个源
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()
输出格式化
const p = $`echo '{"foo":"bar"}\nline2'`
await p.json() // { foo: 'bar' }
await p.lines() // ['{"foo":"bar"}', 'line2']
await p.text() // '{"foo":"bar"}\nline2\n'
异步迭代
for await (const line of $`git log --oneline --max-count=5`) {
console.log(line)
}
Shell 配置
zx 默认使用 bash。根据需要切换 Shell:
import { useBash, usePowerShell, usePwsh } from 'zx'
usePowerShell() // PowerShell.exe
usePwsh() // PowerShell v7+
useBash() // 切回 bash
// 手动覆盖
$.shell = '/bin/zsh'
在 Windows 上,考虑使用 WSL 或 Git Bash 来支持 bash,或者通过 usePowerShell() 切换到 PowerShell。
内置辅助函数一览
| 辅助函数 | 用途 | 示例 |
|---|---|---|
cd() | 更改目录 | cd('/tmp') |
fetch() | HTTP 请求,支持 .pipe() | fetch('https://api.example.com') |
question() | 交互式用户输入 | question('Name: ') |
sleep() | 延迟执行 | await sleep(1000) |
echo() | 打印到 stdout | echo`Status: ${p}` |
stdin() | 读取 stdin | JSON.parse(await stdin()) |
within() | 隔离的异步配置上下文 | within(() => { $.cwd = '/tmp' }) |
retry() | 带延迟/退避的重试 | retry(5, () => $curl url) |
spinner() | CLI 进度指示器 | await spinner(() => $long-cmd) |
glob() | 文件 glob 匹配(使用 globby) | glob('**/*.js') |
which() | 查找可执行文件路径 | await which('node') |
ps | 跨平台进程列表 | ps.lookup({ command: 'node' }) |
tmpdir() | 临时目录 | tmpdir('sub') |
tmpfile() | 临时文件 | tmpfile('f.txt', 'content') |
argv | 解析后的 CLI 参数 | argv.verbose |
dotenv | 加载 .env 文件 | dotenv.config('.env') |
暴露的 npm 包: chalk(颜色支持)、fs(fs-extra)、os、path、YAML(yaml)、minimist。
参考资料
- references/api.md — 完整 API 参考:
$选项、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() - references/configuration.md — 所有
$.options:shell、prefix、postfix、quote、verbose、quiet、env、cwd、timeout、nothrow、detached、preferLocal、spawn、kill、log、input、signal、stdio、halt、delimiter、defaults - references/cli.md — CLI 用法:标志、环境变量、Markdown 脚本、远程脚本、stdin 执行、REPL 模式
- references/process.md — ProcessPromise/ProcessOutput 生命周期、管道、终止、中止、输出格式化器、流处理