Deno.run と Deno.spawn と Deno.Command のどれを使えば良いのか

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 です。

https://github.com/denoland/deno/pull/1156

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` クラスのオブジェクトを作成します。

```js
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` のデザインに至るまでの歴史的な経緯について紹介しました。