Optique: CLI 파서 컴비네이터

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

이 글에서는 Haskell의 `optparse-applicative`와 TypeScript의 Zod에서 영감을 받아 제작된 새로운 CLI 파서 라이브러리인 Optique를 소개합니다. Optique는 파서 컴비네이터를 활용하여 CLI의 구조를 레고 블록처럼 조립할 수 있게 해줍니다. `option()`, `optional()`, `multiple()`, `or()`, `object()`, `constant()`, `command()`, `argument()` 등의 다양한 파서와 컴비네이터를 통해 복잡한 CLI 구조를 유연하게 정의할 수 있습니다. 특히, `or()`와 `object()` 컴비네이터를 사용하여 상호 배타적인 옵션이나 서브커맨드를 쉽게 구현하는 방법을 예제를 통해 설명합니다. Optique는 단순한 CLI 파서 역할에 집중하고 있어 모든 기능을 제공하지는 않지만, 복잡한 CLI 구조를 표현하는 데 유용하며, 소개 문서와 튜토리얼을 통해 더 자세한 내용을 확인할 수 있습니다.

Read more →
12
2
2

If you have a fediverse account, you can quote this article from your own instance. Search https://hackers.pub/ap/articles/0198c81f-6b6b-71e0-af6a-96c1d65af83f on your instance and quote it. (Note that quoting is not supported in Mastodon.)

最近、OptiqueというTypeScript向けのCLIパーサー「コンビネーター」を作っています。Optiqueは、複雑なCLIを小さなパーツの組み合わせで記述できる様にしてくれます。そして、そのCLIのパース結果を型安全に扱う事が出来ます。(下記のコード参照)アイデアはHaskellのoptparse-applicative から得ましたが、TypeScriptはHaskellとAPIのスタイルがかなり異なる為、APIの面ではZod等を参考にしました。詳しい紹介はHackers' Pubに投稿した記事をご覧ください!

const parser = or(
  object({
    type: constant("network"),
    host: option(
      "-h", "--host",
      string({ metavar: "HOST" }),
    ),
    port: option(
      "-p", "--port",
      integer({ metavar: "PORT", min: 0, max: 65535 }),
    ),
  }),
  object({
    type: constant("local"),
    file: option(
      "-s", "--socket-file",
      string({ metavar: "FILE" }),
    ),
  }),
)

type Result = InferValue<typeof parser>;

// Resultの推論された型
type Result = {
    readonly type: "network";
    readonly host: string;
    readonly port: number;
} | {
    readonly type: "local";
    readonly file: string;
}
0

I've recently been working on Optique, a CLI parser combinator for TypeScript. Optique allows you to describe complex CLIs by combining smaller parts. You can also handle the CLI parsing results in a type-safe manner (see code below). The idea came from Haskell's optparse-applicative, but since TypeScript's API style is so different from Haskell's, I referenced Zod and similar libraries for the API design. For a more detailed introduction, please refer to the article I posted on Hackers' Pub!

const parser = or(
  object({
    type: constant("network"),
    host: option(
      "-h", "--host",
      string({ metavar: "HOST" }),
    ),
    port: option(
      "-p", "--port",
      integer({ metavar: "PORT", min: 0, max: 65535 }),
    ),
  }),
  object({
    type: constant("local"),
    file: option(
      "-s", "--socket-file",
      string({ metavar: "FILE" }),
    ),
  }),
)

type Result = InferValue<typeof parser>;

// The above type is inferred as:
type Result = {
    readonly type: "network";
    readonly host: string;
    readonly port: number;
} | {
    readonly type: "local";
    readonly file: string;
}
2