命令行参数、环境变量、工作目录与退出码

本章解决的问题

工作原理

操作系统创建进程时,会为它提供参数列表、环境变量和当前工作目录。进程结束时,会把退出状态交给父进程。它们共同构成一个稳定的 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 与脚本目录

在长期运行的服务中,随意 chdir() 会产生全局、时序相关的路径 bug。更好的方式是尽早用 path.resolve() 得到绝对路径,并显式传递。

exitCodeexit()

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

常见错误

  1. process.env.PORT 直接当作 number。
  2. Boolean(process.env.FEATURE_ENABLED) 解析布尔配置。
  3. process.exit() 作为所有错误处理的第一选择。
  4. 认为 exitCode = 1 会立刻关闭仍在监听的 HTTP Server。
  5. 假设运行目录永远是项目根目录。
  6. 接收 --output 后不校验授权目录,导致路径越界或覆盖文件。

最佳实践

练习题

  1. 给示例增加 --format json|csv 并校验枚举值。
  2. 实现严格解析 DRY_RUN=true|false,其他值均报错。
  3. 写一个例子验证 exitCode=1 后仍能完成已注册的短定时器。
  4. 对比 Bash 的 $? 与 PowerShell 的 $LASTEXITCODE

验收点