Skip to content

注意事项

本文档描述 Clerc 参数解析器的重要行为和注意事项。理解这些行为对于构建可靠的 CLI 应用程序和避免意外问题至关重要。

非贪婪解析

重要

Clerc 解析器是非贪婪的。它只读取第一个标志之前的参数来确定要执行哪个命令。

为什么采用非贪婪解析?

Clerc 采用非贪婪解析有以下原因:

  1. 可预测的行为:标志可以出现在命令之后的任何位置,不会影响命令解析
  2. 兼容性:这与大多数 Unix CLI 工具的行为一致
  3. 灵活性:允许标志在命令之后以任意顺序放置
  4. 简单性:使解析逻辑简单直接,更容易理解

工作原理

解析命令行参数时,解析器遵循以下逻辑:

  1. 从左到右读取参数
  2. 遇到第一个标志(以 --- 开头)时停止读取命令/子命令标记
  3. 第一个标志之后的所有内容都被视为标志及其值,或作为参数

示例

bash
# 命令是 "build",--verbose 是标志
cli build --verbose

# 命令是 "build",--help 是标志,"foo" 是参数(不是子命令)
cli build --help foo

# 没有匹配到命令!因为首先遇到了 --help,所以 "build" 变成了参数
cli --help build

# 命令是 "deploy staging",--force 是标志
cli deploy staging --force

# 命令是 "deploy",--env 是标志,"staging" 是参数(不是子命令)
cli deploy --env staging

对插件的影响

这种非贪婪行为会影响某些插件的工作方式:

Help 插件

--help 标志只有在紧跟 CLI 名称且没有额外参数时,才会显示 CLI 帮助:

bash
# ✅ 显示 CLI 帮助(--help 紧跟在 cli 后面,没有额外参数)
cli --help

# ✅ 显示 "build" 命令的帮助(命令在 --help 之前)
cli build --help

# ❌ 抛出错误!
cli --help build

WARNING

cli --help build 会抛出错误,因为:

  1. 解析器首先遇到 --help,所以没有匹配到命令(尝试匹配根命令)
  2. 没有注册根命令
  3. 因此抛出错误

关键区别:

  • cli --help → Help 插件拦截并显示 CLI 帮助
  • cli --help build → 尝试执行根命令(不存在),抛出错误

如果要显示特定命令的帮助,请始终将命令名称放在 --help 标志之前

bash
# ✅ 正确:显示 "build" 的帮助
cli build --help

或者,使用 help 命令:

bash
# ✅ 始终显示 "build" 的帮助
cli help build

Version 插件

同样,对于版本标志:

bash
# 显示版本(没有匹配到命令)
cli --version

# 匹配到 "build" 命令,但 --version 标志可能被该命令忽略
cli build --version

解析顺序

解析器按以下顺序处理参数:

  1. 命令解析:从第一个标志之前的参数中识别命令
  2. 标志解析:解析所有标志(全局标志和命令特定标志)
  3. 参数收集:剩余的非标志参数成为参数
  4. 双横线处理-- 之后的所有内容按原样收集

双横线(--

双横线 -- 是一个特殊标记,告诉解析器停止解释标志:

bash
# "--foo" 作为参数传递,不会被解析为标志
cli build -- --foo --bar

标志值解析

标志可以通过多种方式接收值:

bash
# 空格分隔
cli build --output dist

# 等号
cli build --output=dist

# 冒号(当值包含 = 时很有用)
cli build --define:KEY=VALUE

对象标志的点表示法

对于 type: Object 的标志,可以使用点表示法设置嵌套值:

bash
# 将 config.port 设置为 "8080"
cli --config.port 8080

# 将 config.server.host 设置为 "localhost"
cli --config.server.host localhost

布尔值处理

对于点表示法标志,特殊值会被自动转换:

输入结果
--config.enabled true{ enabled: true }
--config.enabled false{ enabled: false }
--config.enabled(无值){ enabled: true }
--config.enabled=true{ enabled: true }
--config.enabled=false{ enabled: false }
--config.enabled=(空值){ enabled: true }

转换规则:

  • "true" 或空字符串 → true
  • "false"false
  • 其他值保持为字符串

路径冲突

当一个路径已经被设置为原始值时,后续的嵌套路径会被静默忽略

bash
# --config.port.internal 被忽略,因为 config.port 已经是 "8080"
cli --config.port 8080 --config.port.internal 9090
# 结果:{ config: { port: "8080" } }

为避免这种情况,请确保路径不会冲突(即不要同时设置 a.ba.b.c)。

对象标志的默认值

不推荐

我们不建议对使用点表示法的 Object 标志使用 default 值。

Object 标志遵循全有/全无的默认值行为:

  • 如果没有为该标志提供任何点表示法值,则完整使用 default
  • 如果提供了任何点表示法值,则完全忽略 default(不会合并)
bash
# 示例:env 标志的默认值为 { NODE_ENV: "development", PORT: "3000" }

# 没有提供 --env 标志 → 使用完整的默认值
cli build
# 结果:{ NODE_ENV: "development", PORT: "3000" }

# 提供了任何 --env 标志 → 完全忽略默认值
cli build --env.PORT 8080
# 结果:{ PORT: "8080" }  ← 不包含 NODE_ENV!

为什么不对点表示法使用默认值?

点表示法设计用于用户自定义的运行时配置值(如环境变量、define 宏等),其特点是:

  • 键是预先未知的
  • 用户只指定他们需要的内容
  • 没有"标准的键集合"

这种语义与默认值的不匹配会导致几个问题:

  1. 复杂的合并逻辑:浅合并?深合并?用户可配置的合并函数?每种方法都增加了复杂性
  2. 类型推断复杂性:合并对象类型需要交叉类型和复杂的类型级逻辑
  3. 意外行为:用户可能期望 default 作为缺失键的"后备值",但实现这一点并不简单

推荐做法

不要对点表示法使用默认值,而是在命令处理器中处理默认值:

typescript
cli
  .command("build", "构建项目")
  .flags({
    
env
:
Object
,
}) .on((
context
) => {
const
env
= {
NODE_ENV
: "development",
PORT
: "3000",
...
context
.flags.env, // 用户提供的值覆盖默认值
}; // 使用 env... });

这样可以完全控制合并逻辑,并保持类型推断的简单性。

短标志组合

短标志可以组合使用:

bash
# 等同于:-a -b -c
cli -abc

# -a 和 -b 是布尔标志,-c 接收 "value"
cli -abc value

最佳实践

  1. 将命令放在标志之前:始终写 cli command --flag 而不是 cli --flag command

  2. 使用显式的 help 命令:如有疑问,使用 cli help command 而不是 cli --help command

  3. 引用特殊字符:对包含空格或特殊字符的值使用引号

  4. 使用 -- 传递参数:将参数传递给子进程时,使用 -- 防止解析

bash
# 将 "--watch" 传递给底层工具,而不是传递给 cli
cli build -- --watch

在 MIT 许可证下发布