首页
/ zx 配置指南:深入解析 `$.shell`、`$.kill`、`$.defaults` 等全部配置项

zx 配置指南:深入解析 `$.shell`、`$.kill`、`$.defaults` 等全部配置项

2026-09-05 09:43:22作者:冯爽妲Honey

zx 的几乎所有行为都通过全局 $ 对象上的属性进行配置:从指定 shell 与 spawn 实现,到控制命令前后缀、输出详略、超时信号与日志格式。本文基于官方文档 configuration,结合 src/core.tssrc/log.tssrc/util.tssrc/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 | truesrc/core.ts):为字符串时直接作为 spawn 的 shell 路径,为 true 时交由 Node.js 使用默认 shell(src/core.ts)。如果 $.shell 为假值,命令构建阶段会直接抛出 No shell is available 错误(src/core.ts)。

此外,zx 提供了三个切换 shell 的辅助函数,它们会联动修改 $.shell$.prefix$.postfix$.quotesrc/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.spawnchild_process.spawnSyncsrc/core.ts)。如果你的运行环境(如 Deno、Bun)或测试框架需要替换进程创建逻辑(例如统一 mock),可以分别设置:

import { spawn } from 'node:child_process'
$.spawn = spawn
$.spawnSync = spawnSync

这两个实现最终透传给 src/core.tsexec()spawn / spawnSync 参数,sync: true 的同步命令走 spawnSync,异步命令走 spawn

$.prefix$.postfix:为每条命令加前后缀

$.prefix 指定会拼接在所有命令前面的 shell 片段,默认值是 set -euo pipefail;src/core.tsuseBash() 在模块加载时设置)。它保证子命令出现非零退出、未定义变量或管道失败时脚本立即中断:

$.prefix = 'set -euo pipefail;'

CLI 对应 --prefix

zx --prefix='set -e;' script.mjs

$.postfix 则作用于命令结尾,文档给出的典型场景是兼容 PowerShell(bash 下为空字符串):

$.postfix = '; exit $LastExitCode'

从源码看,两者的拼接发生在 ProcessPromise.fullCmd 中:prefix + cmd + postfixsrc/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

实现上,当 preferLocaltrue 时,查找目录是 [$.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 = truesrc/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 把命令生命周期拆成多种 LogEntrycmdstdoutstderrendcdfetchretrycustomkill,见 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.formatterssrc/log.ts 中优先取用户 formatter,找不到才回退内置):

$.log.formatters = {
  cmd: (entry: LogEntry) => `CMD: ${entry.cmd}`,
  fetch: (entry: LogEntry) => `FETCH: ${entry.url}`,
}

另一个实用细节:默认 log 函数在 entry.verbose 为假时直接返回(src/log.ts),所以非 verbose 模式下只有 stderrend 等不带 verbose 标记的条目才会输出——这解释了 verbose/quiet 对可见输出的精确影响。

$.timeout$.timeoutSignal:命令级超时

$.timeout 指定命令执行超时时间,配套的 $.timeoutSignal 指定超时时发送的信号,默认 SIGTERMsrc/core.ts):

$.timeout = '1s'
$.timeoutSignal = 'SIGKILL'

await $`sleep 999`

超时值的类型是 Duration,可以是数字毫秒,也可以是 '500ms''1s''2m' 这类带单位字符串,由 parseDuration 解析(ms/s/m 三种单位,非法值抛错)。触发机制在 ProcessPromise.timeout():命令 start 事件时注册 setTimeout,到期后调用 kill($.timeoutSignal),命令结束后自动清理定时器。超时属于"命令失败",ProcessOutputsignal 字段会记录实际发送的信号。

$.delimiter:输出切分分隔符

$.delimiter 指定把命令输出切分为行的分隔符,默认 /\r?\n/(换行符或回车+换行,src/core.ts)。处理含特殊字符的文件名时,常用 null 字符作为分隔:

$.delimiter = /\0/

await $`find ./ -type f -print0 -maxdepth 1`

该分隔符影响 ProcessOutput.lines() 与对 ProcessOutputfor...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()
    })
  })
}

注意区分两个信号:$.killSignalkill() 调用,$.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/,
}

这里有两点源码层面的补充:

  1. prefix: 'set -euo pipefail;'postfix: '; exit $LastExitCode' 分属 bash 与 PowerShell 两套约定,实际由模块加载时的 useBash() 决定启用哪一种(src/core.ts)——默认是 bash 前缀,postfix 为空。
  2. 默认值并非一成不变: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_SIGNALtimeoutSignal),在 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 参数(完整列表见 printUsagecli.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 决定执行上下文,而 $.defaultsZX_ 环境变量提供了不改脚本的最后一层覆盖能力。理解 src/core.ts 中快照(getSnapshot)与 fullCmd 的拼接顺序后,上述每一项配置在何时生效、与谁相互影响,都可以从源码直接验证。

登录后查看全文
热门项目推荐
相关项目推荐