命令行参数、环境变量、工作目录与退出码
本章解决的问题
- CLI 如何接收参数和环境变量?
process.cwd()与脚本所在目录有什么区别?- 为什么通常使用
process.exitCode,而不是立即调用process.exit()?
工作原理
操作系统创建进程时,会为它提供参数列表、环境变量和当前工作目录。进程结束时,会把退出状态交给父进程。它们共同构成一个稳定的 CLI 边界:
父进程 ── argv/env/cwd ──> Node.js CLI
父进程 <── exit status ─── Node.js CLI
环境变量是当前进程获得的一份环境映射。子进程通常继承父进程启动时传给它的值;修改 process.env 不会反向修改父进程的环境。
高频 API
process.argv;
process.execPath;
process.execArgv;
process.env;
process.cwd();
process.chdir("some-directory");
process.exitCode = 1;
process.exit(1);
argv 与 Node 启动参数
执行:
node --trace-warnings dist/analyze.js --input sample.csv
典型含义:
process.execPath Node.js 可执行文件路径
process.execArgv ["--trace-warnings"],传给 Node.js 的参数
process.argv[0] Node.js 可执行文件路径
process.argv[1] 被执行脚本路径
process.argv[2...] ["--input", "sample.csv"],应用参数
通过 ts-node 启动时,argv[1] 等细节可能受启动器影响。业务代码应解析用户参数,而不是依赖路径字符串的固定形状。生产 CLI 可使用成熟参数解析库;本章为学习原理使用简单解析。
具体代码:安全的小型转换 CLI
import * as path from "node:path";
interface CliOptions {
inputPath: string;
outputPath: string;
}
function readOption(name: string): string | undefined {
const index: number = process.argv.indexOf(name);
if (index === -1) {
return undefined;
}
return process.argv[index + 1];
}
function parseOptions(): CliOptions | undefined {
const input: string | undefined = readOption("--input");
const output: string | undefined = readOption("--output");
if (!input || !output) {
process.stderr.write(
"用法:ts-node convert.ts --input <文件> --output <文件>\n",
);
process.exitCode = 2;
return undefined;
}
return {
inputPath: path.resolve(process.cwd(), input),
outputPath: path.resolve(process.cwd(), output),
};
}
async function main(): Promise<void> {
const options: CliOptions | undefined = parseOptions();
if (!options) {
return;
}
process.stdout.write(`${JSON.stringify(options)}\n`);
}
main().catch((error: unknown) => {
const message: string =
error instanceof Error ? error.message : "未知错误";
process.stderr.write(`转换失败:${message}\n`);
process.exitCode = 1;
});
约定的退出码可以是:
0 成功
1 未分类执行失败
2 CLI 参数错误
退出码范围和解释最终由操作系统、Shell 与应用协议共同决定;保持为小的非负整数并写入文档最容易跨平台。
环境变量:先校验再转换
process.env 的业务值应视为 string | undefined。字符串 "false" 在 JavaScript 中仍是真值,不能用 Boolean(process.env.DEBUG) 转换。
function readPort(): number {
const rawPort: string = process.env.PORT ?? "8080";
const port: number = Number(rawPort);
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error(`PORT 无效:${rawPort}`);
}
return port;
}
const debugEnabled: boolean = process.env.DEBUG === "true";
不要打印完整 process.env,也不要把 PATH、数据库密码、访问令牌无条件传给不可信第三方程序。
cwd 与脚本目录
process.cwd():调用者启动进程时的工作目录,相对路径默认以它为基准。__dirname:CommonJS 模块当前文件所在目录。process.chdir(dir):改变整个进程的 cwd,会影响此后所有使用相对路径的代码。
在长期运行的服务中,随意 chdir() 会产生全局、时序相关的路径 bug。更好的方式是尽早用 path.resolve() 得到绝对路径,并显式传递。
exitCode 与 exit()
process.exitCode = 1;
设置退出码后,Node.js 仍会继续处理 Event Loop 中已有工作;待没有待处理工作时,以该退出码自然结束。它适合普通 CLI 错误收尾。
process.exit(1);
process.exit() 会尽快同步终止进程,即使还有未完成的异步操作或尚未刷新的 stdout/stderr 写入。它只适合确实需要立即终止、且已接受数据可能未收尾的情形。
此外,只设置 exitCode 不会强制退出:如果还有 Server、定时器或 Socket 保持 Event Loop 活跃,必须先关闭这些资源。
正常时间线
解析 argv → 校验 env → 将相对路径解析为绝对路径
→ 完成业务 → 写 stdout → Event Loop 清空 → exit status 0
异常时间线
推荐:
参数无效 → 写错误到 stderr → exitCode=2 → return
→ 标准流有机会收尾 → Event Loop 清空 → exit status 2
容易丢输出的做法:
写入大量 stdout → 紧接 process.exit(1)
→ 管道写入尚未完成 → 输出可能截断
Bash 与 PowerShell
Bash:
node dist/convert.js --input a.csv --output b.json
echo $?
PORT=8080 node dist/server.js
PowerShell:
node dist/convert.js --input a.csv --output b.json
$LASTEXITCODE
$env:PORT = "8080"
node dist/server.js
PowerShell 的 $? 表达“上一操作是否成功”的布尔状态,外部程序的数值退出码应查看 $LASTEXITCODE。跨平台脚本不要混用 Bash 的 $VAR 与 PowerShell 的 $env:VAR。
常见错误
- 把
process.env.PORT直接当作 number。 - 用
Boolean(process.env.FEATURE_ENABLED)解析布尔配置。 - 用
process.exit()作为所有错误处理的第一选择。 - 认为
exitCode = 1会立刻关闭仍在监听的 HTTP Server。 - 假设运行目录永远是项目根目录。
- 接收
--output后不校验授权目录,导致路径越界或覆盖文件。
最佳实践
- 入口处集中解析并校验 argv/env,转换为有明确类型的配置对象。
- 机器可读结果写 stdout,诊断写 stderr,退出码表达最终状态。
- 普通失败优先设置
process.exitCode并return;服务需要显式关闭资源。 - 对外部可见的退出码建立文档,不要让每个模块随意发明含义。
- 日志脱敏;绝对不要输出完整环境变量。
练习题
- 给示例增加
--format json|csv并校验枚举值。 - 实现严格解析
DRY_RUN=true|false,其他值均报错。 - 写一个例子验证
exitCode=1后仍能完成已注册的短定时器。 - 对比 Bash 的
$?与 PowerShell 的$LASTEXITCODE。
验收点
- [ ] 能区分
argv、execArgv和env。 - [ ] 能解释
cwd为什么不等于源码目录。 - [ ] 普通错误优先使用
exitCode,不会随意强制退出。 - [ ] 知道设置
exitCode不能替代资源关闭。