Optique 0.3.0:依赖选项和灵活组合

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

我们发布了 Optique 0.3.0,其中包含多项改进,使构建复杂的 CLI 应用程序变得更加直观。此版本专注于扩展解析器的灵活性并改进帮助系统,这些改进基于社区反馈,特别是来自 Fedify 项目从 Cliffy 迁移到 Optique 的反馈。特别感谢 @z9mb1wwj 在此过程中提供的宝贵见解。

新特性

  • 使用新的 flag() 解析器实现必需的布尔标志,用于依赖选项模式
  • withDefault() 中的灵活类型默认值,支持联合类型以实现条件 CLI 结构
  • 扩展的 or() 容量,现在支持多达 10 个解析器(之前为 5 个)
  • 增强的 merge() 组合器,可与任何生成对象的解析器配合使用,而不仅限于 object()
  • 使用新的 longestMatch() 组合器实现上下文感知帮助
  • @optique/core@optique/run支持版本显示
  • 结构化输出函数,用于一致的终端格式化

使用 flag() 实现必需的布尔标志

新的 flag() 解析器创建必须明确提供的布尔标志。虽然 option() 在缺失时默认为 false,但 flag() 在不存在时会完全失败解析。这种微妙的差异使依赖选项的模式更加清晰。

考虑一个场景,某些选项只有在明确启用模式时才有意义:

import { flag, object, option, withDefault } from "@optique/core/parser";
import { integer } from "@optique/core/valueparser";

// 没有 --advanced 标志,这些选项不可用
const parser = withDefault(
  object({
    advanced: flag("--advanced"),
    maxThreads: option("--threads", integer()),
    cacheSize: option("--cache-size", integer())
  }),
  { advanced: false as const }
);

// 用法:
// myapp                    → { advanced: false }
// myapp --advanced         → 错误:需要 --threads 和 --cache-size
// myapp --advanced --threads 4 --cache-size 100 → 成功

这种模式对于确认标志(--yes-i-am-sure)或从根本上改变 CLI 行为的模式开关特别有用。

withDefault() 中的联合类型

以前,withDefault() 要求默认值与解析器的类型完全匹配。现在它支持不同的类型,创建联合类型,从而实现条件 CLI 结构:

const conditionalParser = withDefault(
  object({
    server: flag("-s", "--server"),
    port: option("-p", "--port", integer()),
    host: option("-h", "--host", string())
  }),
  { server: false as const }
);

// 结果类型现在是一个联合类型:
// | { server: false }
// | { server: true, port: number, host: string }

这一变化使构建 CLI 变得更加容易,不同的标志可以启用不同的选项集,而无需使用复杂的 or() 链。

更灵活的 merge() 组合器

merge() 组合器现在接受任何产生类对象值的解析器。以前仅限于 object() 解析器,现在它可以与 withDefault()map() 和其他转换解析器一起工作:

const transformedConfig = map(
  object({
    host: option("--host", string()),
    port: option("--port", integer())
  }),
  ({ host, port }) => ({ endpoint: `${host}:${port}` })
);

const conditionalFeatures = withDefault(
  object({
    experimental: flag("--experimental"),
    debugLevel: option("--debug-level", integer())
  }),
  { experimental: false as const }
);

// 现在可以合并不同的解析器类型
const appConfig = merge(
  transformedConfig,        // map() 结果
  conditionalFeatures,      // withDefault() 解析器
  object({                  // 传统的 object()
    verbose: option("-v", "--verbose")
  })
);

这一改进源于认识到许多解析器最终都会产生对象,而人为地将 merge() 限制为仅使用 object() 解析器会限制组合模式。

使用 longestMatch() 实现上下文感知帮助

新的 longestMatch() 组合器选择消耗最多输入标记的解析器。这使得复杂的帮助系统成为可能,其中 command --help 显示该特定命令的帮助,而不是全局帮助:

const normalParser = object({
  help: constant(false),
  command: or(
    command("list", listOptions),
    command("add", addOptions)
  )
});

const contextualHelp = object({
  help: constant(true),
  commands: multiple(argument(string())),
  helpFlag: flag("--help")
});

const cli = longestMatch(normalParser, contextualHelp);

// myapp --help           → 显示全局帮助
// myapp list --help      → 显示 'list' 命令的帮助
// myapp add --help       → 显示 'add' 命令的帮助

@optique/core/facade@optique/run 中的 run() 函数现在自动使用这种模式,因此您的 CLI 无需任何额外配置即可获得上下文感知帮助。

版本显示支持

@optique/core/facade@optique/run 现在都通过 --version 标志和 version 命令支持版本显示。详情请参阅 runners 文档

// @optique/run - 简单 API
run(parser, {
  version: "1.0.0",  // 添加 --version 标志
  help: "both"
});

// @optique/core/facade - 详细控制
run(parser, "myapp", args, {
  version: {
    mode: "both",     // --version 标志和 version 命令
    value: "1.0.0",
    onShow: process.exit
  }
});

API 遵循与帮助配置相同的模式,保持一致性和可预测性。

结构化输出函数

@optique/run 中的新输出函数提供了一致的终端格式化,具有自动功能检测。在 messages 文档 中了解更多信息:

import { print, printError, createPrinter } from "@optique/run";
import { message } from "@optique/core/message";

// 带有自动格式化的标准输出
print(message`Processing ${filename}...`);

// 错误输出到 stderr,可选择退出
printError(message`File ${filename} not found`, { exitCode: 1 });

// 针对特定需求的自定义打印器
const debugPrint = createPrinter({
  stream: "stderr",
  colors: true,
  maxWidth: 80
});

debugPrint(message`Debug: ${details}`);

这些函数自动检测终端功能并应用适当的格式化,使您的 CLI 输出在不同环境中保持一致。

破坏性变更

虽然我们尽量保持向后兼容性,但有几个变化需要注意:

  • @optique/run 中的 help 选项不再接受 "none"。要禁用帮助,只需省略该选项。
  • 实现 getDocFragments() 的自定义解析器需要更新其签名,使用 DocState<TState> 而不是直接的状态值。
  • object() 解析器现在使用贪婪解析,尝试在一次传递中消耗所有匹配的字段。这不应影响大多数用例,但可能会在复杂场景中改变解析顺序。

升级到 0.3.0

要升级到 Optique 0.3.0,请更新两个包:

# Deno (JSR)
deno add @optique/core@^0.3.0 @optique/run@^0.3.0

# npm
npm update @optique/core @optique/run

# pnpm
pnpm update @optique/core @optique/run

# Yarn
yarn upgrade @optique/core @optique/run

# Bun
bun update @optique/core @optique/run

如果您只使用核心包:

# Deno (JSR)
deno add @optique/core@^0.3.0

# npm
npm update @optique/core

展望未来

这些改进来自实际使用和社区反馈。我们特别希望听到新的依赖选项模式如何适用于您的用例,以及上下文感知帮助系统是否满足您的需求。

一如既往,您可以在 optique.dev 找到完整文档,并在 GitHub 上提交问题或建议。

3

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0198f5c6-9253-7728-a7d5-5a7868e1f724 on your instance and reply to it.