Deno アドベントカレンダー 11日目の記事です。
Deno には現在サブプロセスを実行・制御するための API として Deno.run / Deno.spawn系 / Deno.Command の3系統があり、どれを使って良いか分からないという疑問を聞くことが多いです。
結論から言うと、この3つの API のうち、Deno.Command を使うべきです。本記事ではその理由と、各 API が出来た経緯について解説します。
Deno.Command を使うべき理由
Deno.Command は Deno.run と Deno.spawn系の問題を解決した最終的なデザインの API という位置づけです。近い将来 (Deno v1.30 ぐらい) に安定化される可能性が高く、Deno.Command を使っておけばプログラムがそのまま動き続ける可能性が最も高いと考えられます。
ただし、Deno.Command は今の最新バージョン (Deno 1.28.3) では、安定 API では無いため、実行時に --unstable フラグを付けることが必要になります。ライブラリの中で使っている場合など、--unstable が許容出来ないケースでは Deno.run を使いましょう。Deno.run は近い将来に Deprecated (非推奨) になる可能性が高いですが、長い期間安定 API として提供されていたため、すぐに削除されることは有りません (Deno.run は Deno 2.0 で削除予定とされていますが、2.0 のリリース時期は未定です)。
現状最も使用すべきで無いのが、Deno.spawn系の API (Deno.spawn/Deno.spawnChild/Deno.spawnSync) です。Deno.spawn は、執筆時点ですでに main ブランチで削除されており、次のリリース (Deno 1.29.0) で削除済みの状態でリリースされる予定になっています。
歴史
ここからはこれら3系統の API の歴史について簡単に振り返ってみます。
Deno.run
Deno.run は最初に実装された API です。
Deno 0.2.0 というかなり初期段階に実装されています。Deno.run を実行すると非同期にサブプロセスの実行が始まり、サブプロセスの状態を参照・コントロールするためのオブジェクトとして Process オブジェクトが返されるというデザインになっています。サブプロセスの終了ステータスの取得や、標準出力の取得などはこの Process オブジェクトを介して行うという2段階の API デザインになっています。 この API の特徴は、内部で使っている tokio_process を素直になぞるようなデザインになっている点です。
Deno エコシステの中では、この API は必要十分なデザインとしばらくの間考えられており、現在に至るまで唯一の安定なサブプロセス制御 API として使い続けられました。
Deno.spawn系が生まれた経緯
しばらく Deno.run で満足していた Deno コミュニティでしたが、時間が経つうちに Deno.run の不満点がいくつか見えてきました。
1つめは、Deno.run が2段階の API であることの使い勝手の悪さです。ほとんどのケースで、ユーザーがサブプロセスに対して行いたい操作は、サブプロセスの標準出力を取得するか、もしくはサブプロセスの終了ステータスを取得することです。Deno.run はどちらかというと、サブプロセスに対して細やかな操作をする事にフォーカスしたデザインになっており、ほとんどのケースで Process オブジェクトは詳細すぎる情報を露出しています。Process オブジェクトよりも簡単な、標準出力や終了ステータスだけを提供するような API があった方が使いやすいはずだという意見が出てくるようになりました。
2つめは npm 互換性実装が進む中で同期的な API が必要になったことです。Deno.run は呼び出した時点ですでに非同期にサブプロセス実行が始まってしまっており、これを拡張して同期的にサブプロセスの終了を待つような API を自然にデザインすることが難しいという問題が有りました。
3つめは、標準入出力を pipe した場合の API の挙動が非常に分かりにくい事が発見されたことです。(詳細は割愛)
以上の問題点を解決する API として、Deno.spawn (非同期実行、easy な API) / Deno.spawnSync (同期実行) / Deno.spawnChild (非同期実行、詳細な API) という3つの API が定義・実装されました。この3つの API を使って、npm 互換機能の child_prcess.spawnSync が実装されたり、Deno 内部のテストケースでは Deno.run から Deno.spawn に移行するなどの進捗が有りました。
Deno.Command が生まれた経緯
Deno.spawn / Deno.spawnSync / Deno.spawnChild の3つの API によって、機能面では Deno.run の問題点を克服した API を作成することが出来ましたが、この API はデザインが悪いという意見も根強く残りました。とくに Deno.spawn と Deno.spawnChild という2つの API が名前からは予想がしにくい挙動の違いがある点に対する懸念が強くありました。
一方で、Deno.spawnSync は Node 互換性に必須の機能であり、Node 互換性 (npm: 機能) を安定化するタイミングで、Deno.spawn系を安定化しようという提案がされました。しかし、やはり上のデザイン懸念が議論の中で解決出来ず、もう一度新しい API セットをゼロから再デザインすることになりました。
そこで登場したのが Deno.Command です。この API では、サブプロセスをどの様に呼び出すかに関わらず、まず Command クラスのオブジェクトを作成します。
const command = new Deno.Command("コマンド名", { args: [...] });
この command 変数に対して、.output() を呼ぶと、非同期かつ easy な実行になります。また .outputSync() を呼ぶと、同期的で easy な実行になります。また、.spawn() を呼ぶと、ChildProcess クラスが生成されて、サブプロセスに対して詳細な制御を行う事が出来ます。同期/非同期、easy/詳細な実行をすべてサポートしつつ、API の入り口としては、new Deno.Command() という1種類だけが提供されており、Deno.spawn系にあったデザインの問題が解決されています。
Deno.Command のデザインは Deno.run と Deno.spawn 系の中で出てきたすべての懸念を解決した形のデザインになっており、この API がサブプロセス制御 API の最終形になる可能性が非常に高いです。
まとめ
本記事では、サブプロセス制御には今後は Deno.Command を使うべきである理由と、Deno.Command のデザインに至るまでの歴史的な経緯について紹介しました。