世界を動かす技術を、日本語で。

CLIバリデーションを書くのをやめよう。最初から正しく解析しよう

2025年9月7日原文(hackers.pub)

概要

  • CLIツールのバリデーションコードの繰り返し問題
  • 「Parse, don’t validate」の考え方をCLIにも適用
  • Optiqueによる型安全なCLIパーサの実現
  • TypeScriptの型推論活用でバリデーション不要化
  • コードの信頼性・可読性・保守性向上

CLIバリデーションコードの苦痛と解決策

  • CLIツール開発で 同じようなバリデーションコード が乱立する現状
  • オプション依存、排他フラグ、環境別要件など 複雑なパターン の繰り返し出現
  • バリデーション自体は難しくないが、 どのプロジェクトでも冗長 になりがち
  • 他のデータ型(JSON等)では Zod等のパーサで直接型安全に処理 している事実
  • CLIだけが「とりあえずパース→バリデーション地獄」の 旧来方式 に留まる現状

「Parse, don't validate」のCLI適用

  • Alexis Kingの「Parse, don’t validate」ブログに 大きな影響 を受けた経験
  • データを「あとから検証」ではなく「最初から正しい型へパース」 する発想
  • 無効データは パース時点で拒否、余計なif文不要
  • CLIでも 型安全なパース を実現すべきという着想

Optiqueによる型安全なCLIパース

  • Optiqueは CLIバリデーションの繰り返し問題 を根本解決するために開発
  • 典型的なバリデーションパターンを パーサの型定義で吸収
    • 依存オプション :serverがfalseならport存在せず、serverがtrueなら必須
    • 排他オプション :or()で「どれか一つだけ」を型で表現
    • 環境依存オプション :or()で環境ごとに異なる型を指定
  • TypeScriptの型推論 で、正しい組み合わせしか許可されない状態を自動生成
  • ランタイムバリデーション不要化、バグ発生源の大幅削減

パーサコンビネータの直感的応用

  • 「パーサコンビネータ」は 難解そうな名前だが実はシンプル
  • パーサは関数、コンビネータはパーサを組み合わせる関数
  • Haskellのoptparse-applicativeに着想を得て CLIパースに応用
  • モナドや圏論不要、ただの関数合成で完結

TypeScript型推論の恩恵

  • Optiqueでは CLIオプション型を手書き不要
  • パーサ定義だけで TypeScriptが型を自動生成
  • actionが"deploy"ならenvironment必須、"rollback"ならversion必須など 型レベルで厳密化
  • 型安全・自動補完・バグ防止 を実現

Optique導入後の変化

  • バリデーションロジックの大部分を削除 可能
  • CLI引数仕様の変更も パーサ定義修正+型エラー修正で完結
  • 複雑なオプション関係も気軽に追加可能
  • オプショングループの再利用性向上 (merge等で柔軟に合成)
  • 「コンパイルが通れば動く」 という強い信頼感

Optiqueを使うべき人・使わなくていい人

  • 10行スクリプト や単一引数なら不要
  • 以下の経験がある人にはおすすめ
    • バリデーションロジックの同期ずれ
    • 本番での オプション組み合わせ爆発
    • --verboseと--jsonの併用バグ の追跡
    • 依存関係チェックの繰り返し実装
  • Optiqueは若いプロジェクト だが、「parse, don’t validate」の思想は堅牢

まとめ・導入のすすめ

  • Optique公式チュートリアル やGitHubで実際に試すことを推奨
  • CLIのバリデーション地獄からの解放 を目指す開発者向け
  • Optiqueが CLI問題の万能薬とは言わない が、同じバリデーションを何度も書く無駄は減らせる
  • 「そのバリデーションコード、本当に必要?」と 自問自答を促すツール

Hackerたちの意見

考えてみて。APIからJSONを受け取ったら、ただ適当にパースしてif文をたくさん書くわけじゃないよね。Zodみたいなもので、欲しい形に直接パースするんだ。無効なデータ?パーサーがそれを拒否する。これで終わり。コードを書くこととZodを使うことは同じじゃない?違いは誰がコードを書いたかってこと。もちろん、Zodが堅牢で、テストされていて、サポートされていて、拡張可能で、ドキュメントもあって、どうやって自分のドメインを表現するか理解できることを願ってるよね。そして、ZodのAPIが変わるときに、あまり時間をかけずに移行できることも願ってる。

そう、どちらもコードを書くことだよ。でも、ほとんどの場合、表現したい制約はZodで表現できるから、Zodを使うことで書くコードが少なくなって、書いたコードもより正確になるんだ。> もちろん、Zodが堅牢で、テストされていて、サポートされていて、拡張可能で、ドキュメントもあって、どうやって自分のドメインを表現するか理解できることを願ってる。そして、ZodのAPIが変わるときに、あまり時間をかけずに移行できることも願ってる。そう、Zod(または他のライブラリ)に依存する価値を見極めるためには判断が必要だね。これは、TypeScriptやNode、V8、V8がコンパイルされたC++コンパイラ、実行しているx86_64チップ、物理法則に対しても同じ原則だよ。

うん、「パースしろ、バリデートするな」ってアドバイスは、これがあるから空虚に感じる。誰かがそのバリデーションをやってるんだから。アドバイスは「人気のライブラリを再実装しないようにしよう」って言った方がいいかも。

重要なポイントは、著者が明言していないけど、(a) パースが最初に全部行われて、バリデーションとロジックが一緒になっていないこと、(b) パースによってアプリケーションの不変条件をエンコードした新しい構造が作られるから、アプリケーションの他の部分は何もチェックする必要がなくなるってことだと思う。Zodを使うか手動でやるかは関係なくて、大事なのはデータを変換する前処理ステップがあって、単にバリデーションするだけじゃないってことだね。

これは繰り返し出てくるアイデアだね。「パースして、検証しない」。以前の記事: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va... (2019年、Haskellを使用) https://www.lelanthran.com/chap13/content.html (2025年4月、Cを使用)

著者は最初にアレクシス・キングにクレジットを与えていて、その投稿にリンクしてる。

// これはパーサーだ >> const port = option("--port", integer()); これがパーサーだって、どういうこと?型がない言語で型を強制する方法なだけじゃないの?コマンドラインのテキストを受け取って、構文や値を検証するための状態遷移機械みたいなものを期待してたんだけど。

重い作業は optioninteger の定義で行われるんだ。これらは引数を受け取って、何らかの Stream -> Result> 関数を出力する。ちょっとごちゃごちゃに聞こえるかもしれないけど、著者が言ってるようにパーサーコンビネーターはそんなに複雑じゃなくて、慣れるのも早いし、自分でライブラリを作りたいならかなりシンプルだよ。裏で動いてるコードはあまり多くないし、特別な魔法もない。あのパース手法の利点は、かなり宣言的だってこと。これが著者の核心的なポイントみたい。パーサーコンビネーターのコードは、好きなコンビネータライブラリを使って、パース結果として欲しいオブジェクトを書き出すだけで、すべてが自動的に動いて、もし言語にその機能があれば素晴らしい型チェックもある。欠点は、1. どんなパース手法でもそうだけど、実際にパースしたいもののニュアンスを考慮しないといけない(例えば、ホワイトスペースの扱いに関する条件付きルールとか)。ブログ記事からの印象だけど、このプロジェクトは Stream タイプを argv リストとして扱うことでその辺を回避してるように見える。だから「次のブロブを文字列としてパースする」みたいなことができるんだ。2. 手作りのパーサーよりも確実に遅いし(メモリも多く使う)、他の「自動生成された」パースコードよりもその点で劣ることが多い。CLI引数の場合、特に argv を基にしたストリームタイプを選んでるなら、その欠点はほとんど存在しないと思う。ただ、cp みたいなコマンドの argv パースではパフォーマンスが悪くなるかもしれない(もしかしたらそうじゃないかも、-- のような区切り文字からのパース失敗が多い git cp とか?)。オプションと巨大なファイルリストがあるから、引数の指定をあまり気をつけないと指数バックトラッキングの問題が出ることもあるし、手作りのパーサーではそれが明らかになるところが、パーサーコンビネーターでは見えなくなっちゃうかも。

投稿やリンクされたチュートリアルには、無効なオプションを渡したときのユーザー体験についての情報が全然ないね。例を実行してみたけど、NodeやTypeScriptのことを忘れすぎてて、うまくいかなかった。(@optiqueの参照が解決できない。)--foo、--target bar、または--port 3.14を渡したらどうなるの?

私も似たような疑問があるんだけど、「出力形式」や「または」ステートメントは、ユーザーが間違えたことを知らせるのではなく、決定論的に一つの勝者を選ぶように見える。良いパーサーは素晴らしいけど、有用なフィードバックを提供する必要があるよね。

有効なフラグとパラメータの組み合わせごとに関数を書くのが好きなんだ。処理されないものはデフォルトで拒否される。パターンマッチングやガードがあるErlangみたいな言語だと、これがすごく楽だよ。

このアドバイス、いいね。そうそう、いつも違法な状態を表現できないようにしようとしてるけど、ちょっと行き過ぎることもあるかも。ここでの問題は、これをやるときにどうやって良いエラーメッセージを作るかなんだよね。ユーザーが複数の問題を含む入力を渡してきたとき、パーサーが途中でクラッシュしちゃったら、何が間違っているのかリストをどうやって作るの?

Hacker Newsで議論の続きを見る