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.