【Deno1.21～】Denoのサブプロセス生成が簡単になった（`Deno.spawn()`）



:::note alert
この記事で紹介しているDeno.spawn()は、その後のリリースで**削除されました**。

現在のDenoにおけるサブプロセス作成方法は以下の記事をご参照ください。

- [`Deno.run`は非推奨になるので代替手段（`Deno.Command`、dax）](https://qiita.com/access3151fq/items/00f40c62d756258b93ac)
- [Deno.run と Deno.spawn と Deno.Command のどれを使えば良いのか](https://qiita.com/kt3k/items/4ac4b6f21895041927aa)
- [Deno 1.31で安定化されたプロセス起動 API Deno.Command を使ってみる](https://developer.mamezou-tech.com/blogs/2023/03/06/deno-new-command-api/)

この記事では、2022年内の8ヶ月間のみに存在した`Deno.spawn()` APIについて解説しています。記録のために内容は残しておきますが、現在では役に立ちません。
:::


<details>
<summary>
記事本文はここをクリック
</summary>

Deno1.21以降、サブプロセスの生成が簡単になります。

```ts:Deno1.21以降
const { code, stdout, stderr } = await Deno.spawn("echo", { args: ["hello"] });

console.log(new TextDecoder().decode(stdout)); //=> stdoutの内容
console.log(new TextDecoder().decode(stderr)); //=> stderrの内容
console.log(code); //=> 0
```


## 従来のAPI

従来は、サブプロセスの生成には`Deno.run()`という関数を用いていました。

```ts:従来の書き方
const p = Deno.run({
  cmd: ["echo", "hello"],
  stdout: "piped",
  stderr: "piped",
});

const status = await p.status();
const stdout = await p.output();
const stderr = await p.stderrOutput();
```

この関数は低レベルなAPIなので、上記のような「コマンドを実行して結果を受け取る」というプログラムでも長めのコードを書く必要がありました。

## 今回導入されたAPI

Deno1.21では、

- `Deno.spawn` - コマンドを実行して結果を受け取る高レベルAPI（非同期）
- `Deno.spawnSync` - `Deno.spawn`の同期版
- `Deno.spawnChild` - ストリーミング処理ができる低レベルAPI

という3つの新しいサブプロセスAPIが入りました。
それぞれ順番に解説していきます。

※これらのAPIはまだunstable扱いなので、実行の際は`--unstable`フラグを付ける必要があります。

### Deno.spawn()

`Deno.spawn`はコマンドを実行して結果を受け取る関数です。

https://doc.deno.land/deno/unstable/~/Deno.spawn

```ts:Deno.spawnの使い方
const { success, code, signal, stdout, stderr } = await Deno.spawn("echo", { args: ["hello"] });

console.log(new TextDecoder().decode(stdout)); //=> stdoutの内容
console.log(new TextDecoder().decode(stderr)); //=> stderrの内容
console.log(success); //=> true
console.log(code); //=> 0
console.log(signal); //=> null
```

この関数は非同期なので、結果を得るために`await`してやる必要があります。
サブプロセスが終了すると、`stdout`（標準出力）、`stderr`（標準エラー出力）、終了ステータスの情報が返されます。
`stdout`と`stderr`は[Uint8Array](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)というバイト配列で返されます。これを文字列に変換するには、`new TextDecoder().decode()`を使います。

### Deno.spawnSync()

`Deno.spawnSync()`は`Deno.spawn()`の同期版です。

https://doc.deno.land/deno/unstable/~/Deno.spawnSync

```ts:Deno.spawnSyncの使い方
const { success, code, signal, stdout, stderr } = Deno.spawnSync("echo", { args: ["hello"] });

console.log(new TextDecoder().decode(stdout)); //=> stdoutの内容
console.log(new TextDecoder().decode(stderr)); //=> stderrの内容
console.log(success); //=> true
console.log(code); //=> 0
console.log(signal); //=> null
```

コマンド処理させるだけの簡単なスクリプトは同期APIである`Deno.spawnSync()`を、
サーバーやUIを表示するプログラムの中で使う場合はスレッドをブロックしない非同期APIである`Deno.spawn()`を使うとよいでしょう。

### Deno.spawnChild

`Deno.spawnChild`は`Deno.spawn`より高度な処理が行えるAPIです。

https://doc.deno.land/deno/unstable/~/Deno.spawnChild

- 標準入出力のストリーミング処理（パイプ）
- コマンド実行中のkill

が可能です。

`Deno.spawnChild`はWebStreamに対応しています。`child.stdout`や`child.stderr`が`ReadaleStream`になっており、ファイルなどに出力をパイプすることができます。

```ts:Deno.spawnChildの使い方
const child = Deno.spawnChild("echo", { args: ["hello"] });
const file = await Deno.open("./output.txt", { write: true });

child.stdout.pipeTo(file.writable); // stdoutをfileへパイプして書き込み
// あるいは
// child.stdout.pipeTo(Deno.stdout.writable); // 自分のstdoutへパイプする

// `await child.status` で、プロセス終了まで待つ
console.log(await child.status); //=> { success: true, code: 0, signal: null }

// ストリーミングが不要な場合は、.output()で最終的な結果を得ることができる。
// const { stdout, stderr, code } = await child2.output();
```

`Deno.spawnChild`はstdinへのストリーミングにも対応しています。stdinへ書き込むときは、オプションに`"piped"`を指定します。

```ts:stdinへ書き込む
const child1 = Deno.spawnChild("echo", {
	args: ["console.log('hello')"],
	stderr: "inherit", // stderrが不要な時は"inherit"か"null"にする
});

const child2 = Deno.spawnChild(Deno.execPath(), {
	stdin: "piped", // stdinに書き込むときは"piped"に設定する
});

// child1の標準出力をchild2にパイプ
await child1.stdout.pipeTo(child2.stdin);

const { stdout, stderr, status } = await child2.output();
console.log(new TextDecoder().decode(stdout)); //=> 標準出力
console.log(new TextDecoder().decode(stderr)); //=> 標準エラー出力
console.log(status); //=> { success: true, code: 0, signal: null }
```

`child1.stdout.pipeTo(child2.stdin)`と書くことで、child1の標準出力をchild2の標準入力にパイプすることができます。`.pipeTo()`の代わりに`.pipeThough(t: TransformStream)`を使うと、間に変換用のTransformStreamを噛ませたうえでパイプできます。

サブプロセスを途中で殺すには、`.kill(シグナル名)`します。

```ts:killする方法
const child = Deno.spawnChild(Deno.execPath());

child.kill("SIGKILL"); // プロセスをkill

const { success, code, signal, stdout, stderr } = await child.output();
console.log(success); //=> false
console.log(code); //=> 1
console.log(signal); //=> null
```

### 引数に渡すオプション

`Deno.spawn`/`Deno.spawnSync()`/`Deno.spawnChild`の引数には、オプションを渡すことができます。このオプションの型定義は以下のようになっています。

```ts
// Deno.SpawnOptions型の型定義
interface SpawnOptions {
  /** コマンドライン引数 */
  args?: string[];

  /** 親プロセスの環境変数を引き継ぐかどうか。デフォルトはfalse（引き継ぐ） */
  clearEnv?: boolean;

  /** 子プロセスのカレントディレクトリ（デフォルトは親プロセスと同じディレクトリ） */
  cwd?: string | URL;

  /** 子プロセスの中で使える環境変数 */
  env?: Record<string, string>;

  /** 標準出力（デフォルトは"piped"） */
  stdout?: "piped" | "inherit" | "null";

  /** 標準エラー出力（デフォルトは"piped"） */
  stderr?: "piped" | "inherit" | "null";

  /** 標準入力（デフォルトは"null"） */
  stdin?: "piped" | "inherit" | "null";

  /** 子プロセスのグループid */
  gid?: number;

  /** 子プロセスのユーザーid */
  uid?: number;

  /** プロセスをkillするために使うAbortSignal */
  signal?: AbortSignal;
}
```

## まとめ

- `Deno.run()`に代わる新しいサブプロセスAPIが導入された
  - `Deno.spawn` - コマンドを実行して結果を受け取る高レベルAPI（非同期）
  - `Deno.spawnSync` - `Deno.spawn`の同期版
  - `Deno.spawnChild` - ストリーミング処理ができる低レベルAPI
- `Deno.spawnChild`はWeb Streamに対応している

特に`Deno.spawnSync`は、makefileやbash scriptをDenoで置き換えることができそうで、便利だと思います。

ただし、記事執筆時点ではまだ`--unstable`フラグ付きなので、APIが変更される可能性がある点に注意してください。

（実際、返り値のステータスの取り方がv1.24から変更されています。）


</details>