コマンドの標準出力を取得する

Recipe ID: shell-003

コマンドの実行結果(標準出力)を取得する方法を解説します。
await cmd.execute() を使う方法と、イベントリスナーを使ってリアルタイムに取得する方法があります。

前提条件

プラグインのインストールが必要です。

npm run tauri add shell

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
  "permissions": [
    ...,
    "shell:allow-open",
    {
      "identifier": "shell:allow-execute",
      "allow": [
        {
          "name": "echo",
          "cmd": "echo",
          "args": true
        },
        {
          "name": "ping",
          "cmd": "ping",
          "args": true
        }
      ]
    },
    {
      "identifier": "shell:allow-spawn",
      "allow": [
        {
          "name": "ping",
          "cmd": "ping",
          "args": true
        }
      ]
    }
  ]
}

1. フロントエンドから作成する (TypeScript)

1. execute() (非同期・完了待機)

コマンドが終了するまで待機し、全ての結果をまとめて取得します。短時間のコマンドに適しています。

2. on('close', ...) + on('error', ...) (イベント駆動)

cmd.spawn() を使用し、イベントリスナーで出力を受け取ります。長時間実行されるコマンドや、プログレスバーを表示したい場合に適しています。

一括取得 (execute)

import { Command } from '@tauri-apps/plugin-shell';

async function getOutput() {
  const cmd = Command.create('echo', ['output data']);
  const output = await cmd.execute();
  
  // string として取得
  console.log(`STDOUT: ${output.stdout}`);
}

リアルタイム取得 (spawn)

import { Command } from '@tauri-apps/plugin-shell';

async function streamOutput() {
  // ping -c で回数を指定
  const cmd = Command.create('ping', ['127.0.0.1', '-c', '5']);
  
  cmd.on('close', data => {
    console.log(`command finished with code ${data.code} and signal ${data.signal}`);
  });
  
  cmd.on('error', error => console.error(`command error: "${error}"`));
  
  // 標準出力のイベント
  cmd.stdout.on('data', line => {
    console.log(`[STDOUT] ${line}`);
  });
  
  // エラー出力のイベント
  cmd.stderr.on('data', line => {
    console.log(`[STDERR] ${line}`);
  });

  const child = await cmd.spawn();
  console.log('pid:', child.pid);
}

2. バックエンドから作成する (Rust)

Rust で標準出力を取得するには Command::output() を使用します。
リアルタイムにストリーミングしたい場合は、Stdio::piped() を使用して子プロセスを起動し、別スレッドで読み取ります。

一括取得

use std::process::Command;

#[tauri::command]
fn get_output() -> String {
    let output = Command::new("echo")
        .arg("output data")
        .output()
        .expect("failed to execute process");

    String::from_utf8_lossy(&output.stdout).to_string()
}

リアルタイム取得 (イベント通知)

use tauri::{AppHandle, Emitter};
use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader};

#[tauri::command]
fn stream_output(app: AppHandle) {
    let mut child = Command::new("ping")
        .args(["127.0.0.1", "-c", "5"])
        .stdout(Stdio::piped())
        .spawn()
        .expect("failed to spawn");

    let stdout = child.stdout.take().unwrap();
    let reader = BufReader::new(stdout);

    // 別スレッドで読み取り、イベントを発行
    std::thread::spawn(move || {
        for line in reader.lines() {
             if let Ok(l) = line {
                 // フロントエンドへ送信
                 let _ = app.emit("process-stdout", l);
             }
        }
    });
}

コマンドの登録 (lib.rs)

作成したコマンドを invoke_handler に登録します。

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_output, stream_output])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンドでの受信 (TypeScript)

バックエンドから送信されたイベント (process-stdout) をリッスンします。

import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';

// イベントリスナーの登録
const unlisten = await listen<string>('process-stdout', (event) => {
  console.log(`Received: ${event.payload}`);
});

// コマンドの呼び出し
invoke('stream_output');

// 必要なタイミングで解除
// unlisten();