zx 配置指南:深入解析 `$.shell`、`$.kill`、`$.defaults` 等全部配置项
zx 的几乎所有行为都通过全局 $ 对象上的属性进行配置:从指定 shell 与 spawn 实现,到控制命令前后缀、输出详略、超时信号与日志格式。本文基于官方文档 configuration,结合 src/core.ts、src/log.ts、src/util.ts 与 src/cli.ts 的源码实现,逐项说明每个配置项的默认值、取值范围、生效时机,以及对应的 CLI 参数与 ZX_ 环境变量写法,帮助你在脚本中精确掌控 zx 的进程执行细节。
配置的作用模型:$ 是一个动态选项存储
理解所有配置项的前提,是理解 $ 的底层机制。从源码看,$ 由 src/core.ts 中的 sync$ 用 Proxy 包裹:
$.xxx = value实际写入的是AsyncLocalStorage中的当前选项存储(getStore()),而非普通对象属性;- 每次执行
$`cmd`时,getSnapshot会基于当时的 store 生成一份快照(src/core.ts),因此配置对命令的生效时点是命令被创建的那一刻; - 局部覆盖可通过
$({ timeout: 5 })这类形式完成——此时选项只在within()建立的异步上下文中生效(src/core.ts),不会污染全局$。
这意味着两种配置写法是等价的:
// 全局配置
$.verbose = true
await $`long-running-cmd`
// 仅对单条命令生效
await $({ verbose: true })`long-running-cmd`
$.shell 与 $.spawn:决定命令由谁来执行
$.shell
指定执行命令所用的 shell,默认为系统 PATH 中查找到的 bash(文档描述为 which bash)。
$.shell = '/usr/bin/bash'
对应的 CLI 参数是 --shell:
zx --shell=/bin/bash script.mjs
源码中 $.shell 的取值是 string | true(src/core.ts):为字符串时直接作为 spawn 的 shell 路径,为 true 时交由 Node.js 使用默认 shell(src/core.ts)。如果 $.shell 为假值,命令构建阶段会直接抛出 No shell is available 错误(src/core.ts)。
此外,zx 提供了三个切换 shell 的辅助函数,它们会联动修改 $.shell、$.prefix、$.postfix 和 $.quote(src/core.ts):
useBash() // 切换 bash:prefix 设为 'set -euo pipefail;',postfix 清空,quote 用 bash 版
usePwsh() // 切换 pwsh(PowerShell 7+)
usePowerShell() // 切换 powershell.exe
以 setShell 的源码可以看到,切换到 PowerShell 时会启用 ; exit $LastExitCode 后缀并改用 quotePowerShell 转义函数——这正是后文 $.postfix 与 $.quote 两个配置项的典型应用场景。
$.spawn / $.spawnSync
指定底层的 spawn 实现,默认是 Node.js 原生的 child_process.spawn 与 child_process.spawnSync(src/core.ts)。如果你的运行环境(如 Deno、Bun)或测试框架需要替换进程创建逻辑(例如统一 mock),可以分别设置:
import { spawn } from 'node:child_process'
$.spawn = spawn
$.spawnSync = spawnSync
这两个实现最终透传给 src/core.ts 中 exec() 的 spawn / spawnSync 参数,sync: true 的同步命令走 spawnSync,异步命令走 spawn。
$.prefix 与 $.postfix:为每条命令加前后缀
$.prefix 指定会拼接在所有命令前面的 shell 片段,默认值是 set -euo pipefail;(src/core.ts 中 useBash() 在模块加载时设置)。它保证子命令出现非零退出、未定义变量或管道失败时脚本立即中断:
$.prefix = 'set -euo pipefail;'
CLI 对应 --prefix:
zx --prefix='set -e;' script.mjs
$.postfix 则作用于命令结尾,文档给出的典型场景是兼容 PowerShell(bash 下为空字符串):
$.postfix = '; exit $LastExitCode'
从源码看,两者的拼接发生在 ProcessPromise.fullCmd 中:prefix + cmd + postfix(src/core.ts),随后 fullCmd 被传入 exec 实际执行(src/core.ts)。一条带前后缀的完整命令形如:
set -euo pipefail; git status ; exit $LastExitCode
--prefix 与 --postfix 也可以同时通过 CLI 传入(src/cli.ts):
zx --prefix='echo foo;' --postfix='; echo bar' script.mjs
$.preferLocal:优先使用本地 node_modules 中的二进制
$.preferLocal 控制是否优先使用 node_modules/.bin 下的本地可执行文件,而不是全局安装的版本。取值有三种形态:
$.preferLocal = true // 在 $.cwd 与 process.cwd() 下查找
$.preferLocal = '/some/to/bin' // 在指定目录查找
$.preferLocal = ['/path/to/bin', '/another/path/bin'] // 多个目录
$.preferLocal = true
await $`c8 npm test` // 优先用本地 node_modules/.bin/c8
实现上,当 preferLocal 为 true 时,查找目录是 [$.cwd, process.cwd()];为字符串或数组时则展开为该目录列表(src/core.ts)。随后 preferLocalBin 会把每个目录的 node_modules/.bin 子目录和目录本身前置到 PATH(Windows 下对应 Path 环境变量),原 PATH 值追加在末尾。这就是为什么文档示例中 c8 npm test 能命中本地覆盖率工具。CLI 对应 --prefer-local, -l,且支持传入外部目录:zx -l=/external/node_modules/or/nm-root script.mjs。
$.quote:命令替换时的特殊字符转义
$.quote 指定一个转义函数,用于模板字符串中变量插值(命令替换)时对特殊字符进行安全转义,防止注入。默认实现 quote 的转义策略是:
- 空字符串返回
$''; - 仅由
[\w/.\-+@:=,%]组成的参数原样输出; - 否则包裹成 bash 的 ANSI-C 引用
$'...',并转义反斜杠、单引号以及\f\n\r\t\v\0等特殊字符。
切换到 PowerShell 后,usePwsh() 会将 $.quote 替换为 quotePowerShell(单引号包裹、内部单引号翻倍)。$.quote 是命令构建的必备项——build() 阶段若发现其为空会抛出 No quote function is defined 错误(src/core.ts),因此不建议随意置空,只应在需要自定义转义规则时替换实现。
$.verbose 与 $.quiet:输出详略控制
$.verbose(默认 false)开启后,zx 会打印每条执行的命令及其输出。CLI 参数 --verbose 等价于 $.verbose = true(src/cli.ts)。
$.quiet(默认 false)则抑制所有输出,--quiet 对应 $.quiet = true。两者不是简单的互斥,从 isVerbose() 的实现看:
isVerbose(): boolean {
return this._snapshot.verbose && !this.isQuiet()
}
即 quiet = true 会强制压制 verbose 输出。另外注意日志中 stderr 的打印条件是 !isQuiet()(src/core.ts):即使不 verbose,stderr 也会透出,只有 quiet 会把它也关掉。
$.env 与 $.cwd:进程环境变量与工作目录
$.env
指定子进程的环境变量映射,默认是 process.env。修改后对所有后续命令生效:
$.env = { ...process.env, NODE_ENV: 'test' }
注意:$.env 会被 $.preferLocal 在运行时改写(重新计算 PATH),所以 preferLocal 场景下不要在之后依赖旧引用。
$.cwd
指定所有通过 $ 创建进程的工作目录。这里有一个容易混淆的点:cd() 函数只改变 process.cwd() 并同步内部的 $[CWD] 符号属性(src/core.ts);进程实际使用的目录取自 $.cwd,未显式设置时回退到 $[CWD](即 process.cwd(),与原生 spawn 行为一致,src/core.ts):
cd('/tmp') // 只影响 process.cwd()
$.cwd = '/tmp' // 显式指定所有 $ 进程的 cwd
run() 阶段还会校验 cwd 是否存在,不存在时命令直接以 The working directory '...' does not exist. 失败,不会 spawn 进程(src/core.ts)。CLI 对应 --cwd。
$.log:自定义日志函数与格式化器
$.log 指定一个日志函数,默认实现位于 src/log.ts。zx 把命令生命周期拆成多种 LogEntry(cmd、stdout、stderr、end、cd、fetch、retry、custom、kill,见 src/log.ts),全部经由 $.log 输出。典型用法是给命令打印加一个脱敏过滤器:
import {LogEntry, log} from 'zx/core'
$.log = (entry: LogEntry) => {
switch (entry.kind) {
case 'cmd':
// 例如对 cmd 打印应用自定义脱敏函数
process.stderr.write(masker(entry.cmd))
break
default:
log(entry)
}
}
默认日志的定位类似 debugger,因此使用 process.stderr 输出。要改变输出流,覆盖 $.log.output:
$.log.output = process.stdout
要自定义各类条目的打印格式,定义 $.log.formatters(src/log.ts 中优先取用户 formatter,找不到才回退内置):
$.log.formatters = {
cmd: (entry: LogEntry) => `CMD: ${entry.cmd}`,
fetch: (entry: LogEntry) => `FETCH: ${entry.url}`,
}
另一个实用细节:默认 log 函数在 entry.verbose 为假时直接返回(src/log.ts),所以非 verbose 模式下只有 stderr 与 end 等不带 verbose 标记的条目才会输出——这解释了 verbose/quiet 对可见输出的精确影响。
$.timeout 与 $.timeoutSignal:命令级超时
$.timeout 指定命令执行超时时间,配套的 $.timeoutSignal 指定超时时发送的信号,默认 SIGTERM(src/core.ts):
$.timeout = '1s'
$.timeoutSignal = 'SIGKILL'
await $`sleep 999`
超时值的类型是 Duration,可以是数字毫秒,也可以是 '500ms'、'1s'、'2m' 这类带单位字符串,由 parseDuration 解析(ms/s/m 三种单位,非法值抛错)。触发机制在 ProcessPromise.timeout():命令 start 事件时注册 setTimeout,到期后调用 kill($.timeoutSignal),命令结束后自动清理定时器。超时属于"命令失败",ProcessOutput 的 signal 字段会记录实际发送的信号。
$.delimiter:输出切分分隔符
$.delimiter 指定把命令输出切分为行的分隔符,默认 /\r?\n/(换行符或回车+换行,src/core.ts)。处理含特殊字符的文件名时,常用 null 字符作为分隔:
$.delimiter = /\0/
await $`find ./ -type f -print0 -maxdepth 1`
该分隔符影响 ProcessOutput.lines() 与对 ProcessOutput 的 for...of 遍历(src/core.ts)以及 ProcessPromise 的异步迭代(src/core.ts)。优先级是:单条命令通过 o.lines(/\0/) 传入的参数 > $.delimiter > 默认 /\r?\n/。
$.kill 与 $.killSignal:进程终止策略
$.kill 指定一个 kill 函数,默认实现是"半优雅终止":基于 ps.tree() 递归收集进程树后代,逐一 process.kill(pid, signal),再尝试 -pid(进程组)与单 pid 兜底(src/core.ts);在 Windows 上会优先 taskkill /pid <pid> /t /f。$.killSignal 默认 SIGTERM,用于 ProcessPromise.kill() 未指定信号时的兜底(src/core.ts)。
默认的进程树扫描在某些平台上可能不够彻底,可以替换为更精细的实现,例如 tree-kill:
import treekill from 'tree-kill'
$.kill = (pid, signal = 'SIGTERM') => {
return new Promise((resolve, reject) => {
treekill(pid, signal, (err) => {
if (err) reject(err)
else resolve()
})
})
}
注意区分两个信号:$.killSignal 管 kill() 调用,$.timeoutSignal 管超时终止,两者可独立设置。
$.defaults:默认配置全景与 ZX_ 环境变量
$.defaults 持有所有默认配置值,当 $ 上的对应选项未指定时回退使用。文档给出的完整默认值清单如下(defaults 的对应实现):
$.defaults = {
cwd: process.cwd(),
env: process.env,
verbose: false,
quiet: false,
sync: false,
shell: true,
prefix: 'set -euo pipefail;', // for bash
postfix: '; exit $LastExitCode', // for powershell
nothrow: false,
stdio: 'pipe', // equivalent to ['pipe', 'pipe', 'pipe']
detached: false,
preferLocal: false,
spawn: childProcess.spawn,
spawnSync: childProcess.spawnSync,
log: $.log,
kill: $.kill,
killSignal: 'SIGTERM',
timeoutSignal: 'SIGTERM',
delimiter: /\r?\n/,
}
这里有两点源码层面的补充:
prefix: 'set -euo pipefail;'与postfix: '; exit $LastExitCode'分属 bash 与 PowerShell 两套约定,实际由模块加载时的useBash()决定启用哪一种(src/core.ts)——默认是 bash 前缀,postfix 为空。- 默认值并非一成不变:
resolveDefaults会用ZX_前缀的环境变量覆盖其中白名单内的选项(src/core.ts)。当前支持的环境变量键为(src/core.ts):
ZX_CWD ZX_PREFER_LOCAL ZX_DETACHED
ZX_VERBOSE ZX_QUIET ZX_TIMEOUT
ZX_TIMEOUT_SIGNAL ZX_KILL_SIGNAL ZX_PREFIX
ZX_POSTFIX ZX_SHELL
例如 ZX_VERBOSE=true ZX_SHELL='/bin/bash' zx script.mjs 等价于 zx --verbose --shell=/bin/bash script.mjs;布尔值通过 parseBool 解析('true' → true,'false' → false,其他非空字符串视为 true)。ZX_ 前缀的选项名会经 toCamelCase 转换(ZX_TIMEOUT_SIGNAL → timeoutSignal),在 CI 里配置尤为方便:
steps:
- name: Run script
run: zx script.mjs
env:
ZX_VERBOSE: true
ZX_SHELL: '/bin/bash'
CLI 侧同样在参数解析阶段调用 resolveDefaults 合并这些环境变量(src/cli.ts),因此优先级可以理解为:命令行显式参数 > 脚本内 $.xxx 赋值 > ZX_ 环境变量 > 内置默认值。
CLI 参数速查
与配置项直接相关的 CLI 参数(完整列表见 printUsage 与 cli.md):
| 参数 | 等价配置 | 说明 |
|---|---|---|
--shell=<path> |
$.shell |
自定义 shell 二进制 |
--prefix=<command> / --postfix=<command> |
$.prefix / $.postfix |
给每条命令加前后缀 |
--verbose / --quiet |
$.verbose = true / $.quiet = true |
输出详略 |
--prefer-local, -l |
$.preferLocal |
可用 --prefer-local=<dir> 指定外部目录 |
--cwd=<path> |
$.cwd |
设置当前目录 |
--env=<path> |
加载 dotenv 文件后重新 resolveDefaults |
环境变量文件,配合 --cwd 解析相对路径 |
其中 --env 的行为值得注意:src/cli.ts 先按 $.cwd ?? process.cwd() 解析 env 文件路径,用 dotenv 载入后再执行一次 resolveDefaults(),因此 env 文件中写入的 ZX_ 变量也会参与默认值解析。
小结
zx 的配置体系围绕"全局 $ + 按命令选项快照"两级展开:$.shell/$.spawn/$.kill 决定进程如何创建与终止,$.prefix/$.postfix/$.quote 决定命令如何拼装,$.verbose/$.quiet/$.log/$.delimiter 决定输出如何呈现,$.timeout/$.env/$.cwd/$.preferLocal 决定执行上下文,而 $.defaults 与 ZX_ 环境变量提供了不改脚本的最后一层覆盖能力。理解 src/core.ts 中快照(getSnapshot)与 fullCmd 的拼接顺序后,上述每一项配置在何时生效、与谁相互影响,都可以从源码直接验证。
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