@rescript-tauri/plugin-shell

Tauri 2.x shell プラグイン の ReScript バインディングです。子プロセスの起動、stdout / stderr のストリーミング、OS デフォルトアプリでのファイル / URL オープンを提供します。

注釈

本パッケージは main で機能完備済みです。初回 npm 公開は他のパッケージと合わせて予定されています。それまでは、ソースリポジトリ経由かワークスペースリンクで利用してください。

インストール

pnpm add @rescript-tauri/plugin-shell @tauri-apps/plugin-shell

@rescript-tauri/plugin-shell@rescript-tauri/core@tauri-apps/plugin-shell の両方を peerDependencies として宣言しているため、上流側の各バージョンを利用者側で制御できます。

rescript.jsondependencies にパッケージを追加します:

{
  "dependencies": [
    "@rescript-tauri/core",
    "@rescript-tauri/plugin-shell"
  ]
}

Rust 側では、プラグイン crate を追加して builder に登録します:

# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-shell = "2"
// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running app");
}

Capability 設定

Tauri 2.x では、すべての shell 操作に対して capability を付与する必要があります。ls の起動と http(s) URL のオープンに必要な最小構成は以下のとおりです:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "shell:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{ "name": "ls", "cmd": "ls", "args": true }]
    },
    {
      "identifier": "shell:allow-open",
      "allow": [{ "url": "^https?://" }]
    }
  ]
}

起動するプログラムごとに shell:allow-executename と一致するエントリを列挙する必要があります。openPath に渡す URL は shell:allow-open の正規表現パターンのいずれかにマッチしなければなりません。完全な文法は上流の scope and permissions リファレンス を参照してください。

最小サンプル

open RescriptTauriPluginShell

await PluginShell.openPath("https://tauri.app/")

openPathpromise<unit> を返します。設定された shell:allow-open の正規表現にマッチしない URL を渡すと、内部呼び出しが reject されます。

公開 API

シンボル

戻り値

用途

openPath

promise<unit>

OS デフォルトアプリで URL / パスを開く (または ~openWith で指定)

Command.create / Command.sidecar

Command.t<string>

UTF-8 のコマンドを構築する

Command.createRaw / Command.sidecarRaw

Command.t<Uint8Array.t>

バイト列のコマンドを構築する (encoding: "raw")

Command.execute

promise<childProcess<'o>>

完了まで実行し、出力をまとめて収集する

Command.spawn

promise<Child.t>

コマンドを開始してハンドルを返す

Command.onClose / Command.onError

Command.t<'o>

ライフサイクルイベントを購読する

Command.onStdoutData / Command.onStderrData

Command.t<'o>

ストリーミング出力を購読する

Command.removeAllListeners

Command.t<'o>

コマンドレベルのリスナーをすべて解除する

Command.stdout / Command.stderr

EventEmitter.t<{"data": 'o}>

内部の event emitter への低レベルアクセス

Child.pid / Child.kill / Child.write

int / promise<unit> / promise<unit>

子プロセスのアクセサと操作

EventEmitter.*

t<'events>

9 つの汎用メソッド (on / once / off / addListener / removeListener / removeAllListeners / listenerCount / prependListener / prependOnceListener)

関連する record (spawnOptionschildProcess<'o>terminatedPayload) は PluginShell から再エクスポートされています。完全な doc コメントと対応する上流 URL は packages/plugin-shell/src/PluginShell.resi を参照してください。

コマンド実行

ワンショット実行

Command.execute は子プロセスが終了したタイミングで resolve され、収集した出力をまとめて返します。短命なコマンドに最適です:

module Shell = RescriptTauriPluginShell.PluginShell

let listFiles = async () => {
  let cmd = Shell.Command.create("ls", ~args=["-la"])
  let output = await cmd->Shell.Command.execute
  Console.log(output.stdout)
}

childProcess<'o>{code, signal, stdout, stderr} を保持します。codesignal はどちらも Nullable.t<int> で、Unix では相互排他です (シグナルで終了したプロセスは code = null を返します)。

バックグラウンドプロセス

Command.spawn は子プロセスを起動して Child.t ハンドルを即座に返し、終了を待ちません。Child.writeChild.kill と組み合わせると対話的に利用できます:

let runRepl = async () => {
  let cmd = Shell.Command.create("python3", ~args=["-i"])
  let child = await cmd->Shell.Command.spawn
  await child->Shell.Child.write("print('hello')\n")
  // ... later
  await child->Shell.Child.kill
}

Child.pid は OS レベルのプロセス ID を返します。

Raw byte 出力

専用のファクトリ関数を使って encoding: "raw" を渡すと、string の代わりに Uint8Array.t を受け取れます:

let readImage = async () => {
  let cmd = Shell.Command.createRaw("cat", ~args=["icon.png"])
  let output = await cmd->Shell.Command.execute
  Console.log("byte count: " ++ Int.toString(TypedArray.length(output.stdout)))
}

上流の Command.create({encoding: 'raw'}) における TypeScript の条件型による戻り値は、ReScript では 4 つの関数 (Command.create / Command.createRaw / Command.sidecar / Command.sidecarRaw) に分割されており、結果型は常に静的に決定されます。

ストリーミング出力

長時間動作するコマンドからイベントを stream するには onStdoutData / onStderrData / onClose / onError をチェーンします。各メソッドはコマンド自身を返し、上流の Promise<this> パターンに対応しています:

let tail = async () => {
  let cmd =
    Shell.Command.create("tail", ~args=["-f", "/var/log/system.log"])
    ->Shell.Command.onStdoutData(line => Console.log2("stdout:", line))
    ->Shell.Command.onStderrData(line => Console.log2("stderr:", line))
    ->Shell.Command.onClose(payload =>
      switch (payload.code->Nullable.toOption, payload.signal->Nullable.toOption) {
      | (Some(code), _) => Console.log("exited with code " ++ Int.toString(code))
      | (_, Some(sig)) => Console.log("killed by signal " ++ Int.toString(sig))
      | _ => Console.log("closed")
      }
    )
    ->Shell.Command.onError(err => Console.error2("error:", err))

  let _child = await cmd->Shell.Command.spawn
}

より高度な購読パターン (onceprependListener、手動の listenerCount など) には Command.stdout / Command.stderr を直接使ってください。どちらも EventEmitter.t<{"data": 'o}> を返すので、EventEmitter モジュールの 9 つの汎用メソッドで操作できます。

Command.removeAllListeners はコマンドレベルのリスナー (close / error) をすべて解除します。stdout / stderr のリスナーを解除するには、対応するアクセサに対して EventEmitter.removeAllListeners を呼び出してください。

Sidecar バイナリ

Sidecar は、アプリと一緒に同梱されるバイナリで、PATH ではなく tauri.conf.json > bundle > externalBin から解決されます。Command.sidecar (バイト列を扱う場合は sidecarRaw) を使います:

let runHelper = async () => {
  let cmd = Shell.Command.sidecar("my-sidecar", ~args=["--check"])
  let output = await cmd->Shell.Command.execute
  Console.log(output.stdout)
}

sidecar に渡す文字列は、ターゲットトリプルの接尾辞を含まない externalBin エントリと一致させてください。

パス / URL を開く

openPath は引数で渡したパス / URL をシステムのデフォルトアプリケーションで開きます:

await Shell.openPath("https://github.com/tauri-apps/tauri")
await Shell.openPath("/tmp/notes.md")

~openWith を渡すと、特定のオープナー (firefoxchromiumsafarixdg-open など) を強制できます。パスや URL は shell:allow-open の正規表現にマッチする必要があり、マッチしない場合は内部呼び出しが reject されます:

await Shell.openPath("/tmp/notes.md", ~openWith="firefox")

注意点

openPath へのリネーム

上流ではこの関数を open として公開していますが、ReScript ではこの識別子がモジュールアクセス (open MyModule) のために予約されているため、本パッケージでは openPath として再エクスポートしています:

// ❌ syntax error: ReScript treats `open` as a keyword
await Shell.open("https://tauri.app/")

// ✅
await Shell.openPath("https://tauri.app/")

Uint8Array の長さ取得

@rescript/coreUint8Array.tTypedArray.t<int> のエイリアスです。長さの getter は親モジュール側にあります:

let output = await cmd->Shell.Command.execute
Console.log("byte count: " ++ Int.toString(TypedArray.length(output.stdout)))

Scope 設定ミス

対象が src-tauri/capabilities/ で許可されていない場合、コマンドの起動も openPath の呼び出しも失敗します。shell:allow-executename フィールドは、Command.create / Command.sidecar の第 1 引数と完全に一致させる必要があります:

{
  "identifier": "shell:allow-execute",
  "allow": [{ "name": "git", "cmd": "git", "args": true }]
}

上記の場合 Command.create("git", ...) は成功しますが、Command.create("Git", ...)Command.create("/usr/bin/git", ...) は reject されます。

互換性

項目

対応バージョン

上流 @tauri-apps/plugin-shell

^2.3.0 (peer)

Rust tauri-plugin-shell

2.x

@rescript-tauri/core

^0.1.0 (peer)

ReScript

>=12.0.0

@rescript/core

>=1.6.0

対応 OS

Linux / macOS / Windows

関連リンク