zx 入门实战:用 JavaScript 编写 Shell 脚本的完整上手指南
本文基于 zx 仓库官方文档 docs/getting-started.md 展开,系统讲解如何安装 zx、编写第一个脚本并直接执行、使用免导入的全局函数、理解核心模板命令 $ 及其自动转义机制、掌握 ProcessOutput 结果对象与错误处理,并结合源码印证 ProcessPromise 的底层工作方式。读完后你可以独立编写、运行并调试自己的 zx 脚本。
zx 是什么:为什么用 JavaScript 写脚本
官方文档给出的动机很直接:Bash 很好用,但脚本一旦复杂起来,很多人更倾向于使用一门更便利的编程语言。JavaScript 是个理想选择,只是 Node.js 标准库在直接用之前还有不少"仪式感"。而 zx 包恰好提供了围绕 child_process 的实用封装、参数自动转义,以及一组合理的默认值(引自 docs/getting-started.md 与 package.json 中的描述 "A tool for writing better scripts")。
一段典型的 zx 脚本长这样(官方 Overview 示例,原样保留):
#!/usr/bin/env zx
await $`cat package.json | grep name`
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
const name = 'foo bar'
await $`mkdir /tmp/${name}`
这段代码体现了 zx 的三个核心卖点:
- 顶层
await:像写普通异步 JavaScript 一样按顺序执行命令; ${...}自动转义:foo bar含空格,插入mkdir /tmp/${name}时无需手工加引号也不会被拆成两个参数;- 命令即 Promise:
$`...`返回的ProcessPromise可以直接塞进Promise.all` 并发执行(仓库示例 examples/parallel.mjs、examples/fetch-weather.mjs 也是同样的写法)。
安装与运行环境
基本安装
npm install zx
除了 npm,docs/setup.md 还列出了更多官方安装方式:
npx zx script.js # 不安装 zx 包直接运行脚本
npx zx@8.6.0 script.js # 固定到某个 zx 版本
yarn add zx
pnpm add zx
bun install zx
brew install zx
docker pull ghcr.io/google/zx:8.5.0
Deno / JSR 用户则需要额外权限:
deno install -A npm:zx
# zx requires additional permissions: --allow-read --allow-sys --allow-env --allow-run
当前仓库 package.json 中 version 为 8.9.0,engines 字段声明 node >= 12.17.0,docs/setup.md 进一步说明支持 Node.js >= 12.17.0、Bun >= 1.0.0、Deno 1.x/2.x、GraalVM Node.js,操作系统覆盖 Linux / macOS / Windows,并且需要一个 bash(或 PowerShell)解释器。Windows 上建议使用 WSL 或 Git Bash;如果想切换到 PowerShell,可以用 usePowerShell() 或 usePwsh() 切换(见下文全局函数部分)。
发行渠道
docs/setup.md 将 zx 分成几个发行渠道,按需选择:
| 渠道 | 说明 | 安装方式 |
|---|---|---|
latest |
主线版本,包含最新特性与改进 | npm i zx |
lite |
精简版,适合轻量脚本,详见 docs/lite.md | npm i zx@lite |
dev |
开发快照,含最新改动,可能不稳定 | npm i zx@dev |
legacy |
兼容旧脚本的历史版本,只修 bug 不加新特性 | npm i zx@<version> |
各渠道的详细差异见 docs/versions.md。
包形态
从 package.json 的 exports 与 bin 字段可以看到:zx 是一个混合(hybrid)包,同时提供 ESM(build/index.js)与 CJS(build/index.cjs)入口,并导出 zx、zx/globals、zx/cli、zx/core 四个入口点;bin 字段把 zx 命令映射到 build/cli.js,这就是为什么安装后可以直接用 zx script.mjs 运行脚本。
编写并运行你的第一个脚本
文件扩展名的讲究
按文档要求,把脚本写在 .mjs 扩展名的文件中,才能在顶层直接使用 await。如果你坚持用 .js,需要用 void async function () {...}() 之类的包装函数把脚本包起来。TypeScript 同样受支持。
Shebang 与执行方式
在 zx 脚本开头加上 shebang:
#!/usr/bin/env zx
然后你有两种执行方式。
方式一:加执行权限后直接运行:
chmod +x ./script.mjs
./script.mjs
方式二:通过 zx 的 CLI 运行:
zx ./script.mjs
仓库自带的 examples/hello.mjs 是最小的可运行样例:
#!/usr/bin/env zx
await $({ verbose: true })`echo "Hello!"`
其中 $({ verbose: true })`...` 展示了"按单次调用传入选项"的写法,verbose` 会让命令执行前打印完整命令行。
CLI 常用选项
zx 命令的完整选项定义在 src/cli.ts 的 printUsage() 中,这里整理成速查表(均以 zx [options] <script> 形式使用):
| 选项 | 说明 |
|---|---|
--quiet |
抑制所有输出 |
--verbose |
开启 verbose 模式 |
--shell=<path> |
指定自定义 shell 二进制 |
--prefix=<command> |
为所有命令添加前缀 |
--postfix=<command> |
为所有命令添加后缀 |
--prefer-local, -l |
优先使用本地安装的包与二进制 |
--cwd=<path> |
设置当前工作目录 |
--eval=<js>, -e |
直接执行一段脚本 |
--ext=<.mjs> |
指定脚本扩展名 |
--install, -i |
运行前自动安装依赖 |
--registry=<URL> |
指定 npm registry(默认 https://registry.npmjs.org/) |
--version, -v |
打印当前 zx 版本 |
--help, -h |
打印帮助 |
--repl |
启动交互式 REPL |
--env=<path> |
指定 env 文件路径 |
从源码 src/cli.ts 的 readScript() 还可以确认几个实用行为:zx -e 'await $echo hi' 可求值一段内联脚本;zx - 或空参数时从 stdin 读取脚本;参数是 http(s):// 链接时会先拉取远程脚本;.md 文件会被 transformMarkdown 转换后当作脚本执行(对应文档站中的 Markdown 模式)。
全局函数:开箱即用,无需 import
文档说明:所有函数($、cd、fetch 等)都无需任何 import 即可直接使用。
从源码看,这是 CLI 的功劳:src/cli.ts 在 main() 中执行 await import('zx/globals');而 src/globals.ts 的核心只有一行 Object.assign(globalThis, _),把主入口的全部导出挂到 globalThis 上。该文件随后用 declare global 声明了这些全局量,包括(按源码列表):
- 命令执行与流程:
$、ProcessPromise、ProcessOutput、within、syncProcessCwd、cd、kill、ps; - 配置:
defaults、resolveDefaults、quiet、nothrow、useBash、usePwsh、usePowerShell; - 常用工具:
echo、log、sleep、retry、expBackoff、spinner、question、stdin、fetch(经 src/goods.ts 提供); - 文件与环境:
fs、path、os、glob/globby、tempfile、tempdir(别名tmpfile、tmpdir)、dotenv、YAML、minimist、argv、parseArgv、updateArgv; - 其他:
chalk、which、quote、quotePowerShell、version、VERSION。
如果你不是通过 CLI 运行脚本(例如用 node script.mjs 直接执行),需要显式导入全局以获得同样能力,这也是文档建议的写法(还能让 VS Code 的自动补全正常工作):
import 'zx/globals'
import 一个无导出模块只是为了触发 src/globals.ts 中的副作用赋值,这正是它能"注入全局"的机制。
深入 $ 模板命令
基本用法:同步与异步两种模式
const list = await $`ls -la`
const dir = $.sync`pwd`
- 默认是异步模式:返回
ProcessPromise,await后得到ProcessOutput; $.sync是同步模式:命令执行是阻塞的,立即返回ProcessOutput。
从源码结构看(src/core.ts),$ 是一个 Proxy 包裹的函数:sync$() 把 $.sync 的访问重定向到一个带 sync: true 默认选项的新实例,其余属性读写则落到当前 options 存储上;每次 $`cmd` 调用都会创建 ProcessPromise 并在非 halt 状态下立即 run()。默认选项由 defaults 给出(src/core.ts 中 resolveDefaults):shell: true(默认 bash)、stdio: 'pipe'、nothrow: false、quiet: false、detached: false、preferLocal: false,超时/终止默认信号均为 SIGTERM。此外还支持通过 ZX_ 前缀环境变量覆盖部分默认值(如 ZX_verbose=true),白名单见 src/core.ts 中的 ENV_OPTS。
${...} 自动转义:不需要额外加引号
const name = 'foo & bar'
await $`mkdir ${name}`
模板字符串中 ${...} 插值的内容会被自动转义并加引号,不需要也不应该再手动加引号——手动加引号反而可能造成注入风险。这部分行为的更多规则(数组参数、glob、~ 展开、动态拼接命令)在 docs/quotes.md 中有专门讨论。
转义的具体实现可以追溯到 src/util.ts 的 quote():空字符串渲染为 $'';由 [A-Za-z0-9_/.-+@:=,%] 构成的"安全"字符串原样输出;其余内容用 bash 的 C 风格引号 $'...' 包裹,并对反斜杠、单引号、\n、\t、\r 等控制字符做 C 风格转义。这正是文档所说"zx 偏好 bash 特有的 $'...' 引号方式"(见 docs/quotes.md)的底层来源。quotePowerShell() 则是 PowerShell 场景下的对应实现。
传入参数数组
需要传递一组参数时,${...} 里可以直接放数组,每个元素分别转义后以空格连接:
const flags = [
'--oneline',
'--decorate',
'--color',
]
await $`git log ${flags}`
异步模式下的 thenable 等待
在异步模式下,zx 会在执行命令前等待字面量中的任何 thenable:
const a1 = $`echo foo`
const a2 = new Promise((resolve) => setTimeout(resolve, 20, ['bar', 'baz']))
await $`echo ${a1} ${a2}` // foo bar baz
从 src/core.ts 的 ProcessPromise.run() 可以看到对应机制:若命令字符串在启动时仍不是字符串(即包含未 resolve 的 thenable),会在内部 async run(cb, ctx) 钩子中 await self.cmd 之后才真正派生进程;同步模式则不允许这种异步命令解析(build() 中直接抛出 sync mode does not allow async command resolution)。
ProcessOutput 结果对象与错误处理
结构
文档给出的 ProcessOutput 概貌:
class ProcessOutput {
readonly stdout: string
readonly stderr: string
readonly signal: string
readonly exitCode: number
// ...
toString(): string // Combined stdout & stderr.
valueOf(): string // Returns .toString().trim()
}
对照 src/core.ts 中的实际实现,ProcessOutput 继承自 Error,除上述字段外还额外提供:stdall(stdout 与 stderr 交错合并的完整输出)、duration(执行耗时,单位 ms)、ok(exitCode === 0 且无 error)、以及 json()、text(encoding)、lines(delimiter)、buffer()、blob(type) 等解析方法。toString() 返回 stdall,valueOf() 返回 stdall.trim()。
非零退出码会抛异常
被执行的程序若返回非零退出码,ProcessOutput 会被抛出,可以用 try/catch 捕获:
try {
await $`exit 1`
} catch (p) {
console.log(`Exit code: ${p.exitCode}`)
console.log(`Error: ${p.stderr}`)
}
由于 ProcessOutput extends Error,捕获到的 p 天然携带 message(由 Fail.formatExitMessage 生成的可读退出信息)、cause 等 Error 语义,可以直接打印或向上传播。
输出会被"如实地"捕获
文档强调:进程输出按原样捕获,通常程序结尾会打印一个换行符 \n。当 ProcessOutput 被当作另一个 $ 命令的参数时,zx 会使用其 stdout 并 trim 掉换行:
const date = await $`date`
await $`echo Current date is ${date}.`
结合上文 quote() 的实现理解这一点就顺理成章:${date} 插值走的是转义/取 trim 值的路径,date 命令尾部的 \n 不会把输出拆成多行,echo 最终得到的是 Current date is 2026-09-05 .... 这样干净的一行。
相关进阶阅读
围绕 $ 的更多能力,文档站内有对应页面可以深入:
- docs/process-promise.md:
ProcessPromise的完整 API(管道pipe、nothrow、timeout等); - docs/process-output.md:
ProcessOutput解析方法详解; - docs/quotes.md:转义规则、数组参数、glob 与
~展开的边界情况; - docs/shell.md、docs/cli.md、docs/configuration.md:shell 切换、CLI 与默认值配置;
- docs/typescript.md:TypeScript 支持。
相关测试位于 test/core.test.js、test/global.test.js 与 test/vendor.test.js,可用于验证上述行为在不同运行时下的表现;仓库同时通过 test/smoke/ 下的用例覆盖 Node/Bun/Deno/Windows 等环境。
许可证与免责声明
zx 采用 Apache-2.0 许可(见仓库根目录 LICENSE)。官方文档同时注明:This is not an officially supported Google product(这不是谷歌官方支持的产品)。
小结
按本文步骤走一遍,你应当已经掌握了:用 npm 等渠道安装 zx、以 .mjs + shebang 方式编写脚本并双通道(直接执行 / zx CLI)运行、理解全局函数从何而来、正确使用 $ 模板与自动转义、以及用 ProcessOutput 与 try/catch 处理命令输出和失败。这些正是 docs/getting-started.md 覆盖的核心路径;下一步建议依次阅读 docs/process-promise.md 与 docs/configuration.md,把管道、超时与默认值配置补全,即可写出生产级的 zx 脚本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00