コマンドの標準出力・エラー出力を取得する

execute() が返す stdout・stderr・code で結果を受け取る。日本語版 Windows の Shift_JIS 出力を encoding で読む方法、終了コードでの成否判定、Rust の output() も示す。

外部プロセス 対象: Tauri 2.x 更新日: 読了目安: 約9分 shell-003
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 結果の形と成否の判定
  4. 日本語版 Windows の文字コード
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

execute() はコマンドの終了を待ち、標準出力 stdout、標準エラー出力 stderr、終了コード code を 1 つのオブジェクトで返します。つまずきやすいのは 2 点です。日本語版 Windows のコマンドは UTF-8 以外で出力することが多く、そのままでは読み取り自体が失敗します。また、コマンドが失敗しても execute() は reject されないので、code を見ないと気付けません。出力を 1 行ずつ受け取るなら コマンドの出力をリアルタイムで受け取る、終わらないプロセスは 時間のかかるプロセスを管理・強制終了する を使います。

前提条件

Shell プラグインと shell:allow-execute の scope の書き方は 外部コマンド(ls/dir等)を実行する のとおりです。ここでは ping を 1 回だけ送る形で許可します。Windows と macOS・Linux で回数のオプションが違うので 2 つ書き、宛先は英数字で始まるホスト名か IP アドレスに限ります。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "shell:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [
        {
          "name": "ping-win",
          "cmd": "ping",
          "args": ["-n", "1", { "validator": "[A-Za-z0-9][A-Za-z0-9.-]{0,252}" }]
        },
        {
          "name": "ping-unix",
          "cmd": "ping",
          "args": ["-c", "1", { "validator": "[A-Za-z0-9][A-Za-z0-9.-]{0,252}" }]
        }
      ]
    }
  ]
}

1. フロントエンドから実装する (TypeScript)

結果の形と成否の判定

プロパティ内容
code終了コード。macOS・Linux でシグナルによって終了したときは null
signal終了させたシグナルの番号。Windows では常に null
stdout / stderr出力の全体。既定では UTF-8 として読んだ文字列

execute() が reject されるのは、権限や scope に合わない、プログラムを起動できない、出力を文字列にできない、のどれかのときです。コマンドが 0 以外で終わっても resolve されるので、成否は code で判定します。stderr が空かどうかで決めるのは誤りで、git clone や curl は成功しても進み具合を stderr に書き、Windows の ping や ipconfig は失敗の理由を stdout に書きます。

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

const isWindows = navigator.userAgent.includes('Windows');

// ping を 1 回送り、出力を行の配列で返す。失敗なら理由を付けて throw する
export async function pingOnce(host: string): Promise<string[]> {
  const command = isWindows
    ? Command.create('ping-win', ['-n', '1', host], { encoding: 'shift_jis' }) // 日本語版 Windows 向け
    : Command.create('ping-unix', ['-c', '1', host]);
  const { code, signal, stdout, stderr } = await command.execute();
  const lines = stdout.split(/\r?\n/).filter((line) => line !== ''); // Windows の改行は \r\n
  if (code === 0) return lines;
  const reason = stderr.trim() || lines.join('\n'); // 理由を stdout に書くコマンドもある
  throw new Error(code === null ? `シグナル ${signal} で終了` : `終了コード ${code}: ${reason}`);
}

終了コードの意味はコマンドごとに決まっています。grep や findstr は「見つからない」を 1、diff は「差分あり」を 1 で返し、Windows の robocopy は 8 未満なら成功です。0 以外をすべて失敗とせず、使うコマンドの仕様を確かめます。

日本語版 Windows の文字コード

encoding を指定しないと出力を UTF-8 として読み、UTF-8 として正しくないバイトが 1 つでもあると execute() 全体が reject されます。日本語版 Windows では ping・ipconfig・cmd の組み込みコマンドなど、OS 付属のコマンドの多くが Shift_JIS 系(コードページ 932)で日本語を出すので、この失敗が起こります。Git や Node.js の出力は基本的に UTF-8 なので、そのまま読めます。

encoding には 'shift_jis'・'windows-31j'・'utf-16le' など、ブラウザーの TextDecoder と同じエンコーディング名を指定します。'cp932' は使えません。cmd の組み込みコマンドは cmd /U /C … で UTF-16 の出力にして 'utf-16le' で読むのが確実です(例は shell-001)。Python は環境変数 PYTHONIOENCODING=utf-8 で UTF-8 に揃えられます(渡し方は shell-002)。

どちらで出てくるか分からないときは encoding: 'raw' でバイト列のまま受け取り、UTF-8 で読めなければ Shift_JIS として読みます。型定義上は Uint8Array ですが、実際には数値の配列で届くので、new Uint8Array() に詰め直してから TextDecoder に渡します。

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

function decode(bytes: Uint8Array): string {
  try {
    return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
  } catch {
    return new TextDecoder('shift_jis').decode(bytes); // UTF-8 として不正なら Shift_JIS で読む
  }
}

export async function executeAndDecode(name: string, args: string[]) {
  const out = await Command.create(name, args, { encoding: 'raw' }).execute();
  return {
    code: out.code,
    stdout: decode(new Uint8Array(out.stdout)), // 数値の配列を Uint8Array にする
    stderr: decode(new Uint8Array(out.stderr)),
  };
}

2. バックエンドから実装する (Rust)

プラグインの output() も終了を待って結果を返します。stdout と stderr はバイト列で、status.code() が終了コード、status.success() が「0 で終わったか」です。文字コードの変換にはプラグインが公開している Encoding を使えるので、クレートの追加は要りません。起動できなかったときだけ Err になり、0 以外の終了コードでも Ok が返る点は JS と同じです。

use serde::Serialize;
use tauri_plugin_shell::{process::Encoding, ShellExt};

#[derive(Serialize)]
struct CmdOutput {
    code: Option<i32>,
    stdout: String,
    stderr: String,
}

/// UTF-8 として読めなければ Shift_JIS として読む
fn decode_bytes(bytes: &[u8]) -> String {
    match std::str::from_utf8(bytes) {
        Ok(text) => text.to_string(),
        Err(_) => {
            let sjis = Encoding::for_label(b"shift_jis").expect("known label");
            sjis.decode(bytes).0.into_owned()
        }
    }
}

#[tauri::command]
async fn ping_localhost(app: tauri::AppHandle) -> Result<CmdOutput, String> {
    let count_flag = if cfg!(windows) { "-n" } else { "-c" };
    let output = app
        .shell()
        .command("ping")
        .args([count_flag, "1", "127.0.0.1"])
        .output()
        .await
        .map_err(|e| e.to_string())?; // 起動できなかったときだけ Err
    Ok(CmdOutput {
        code: output.status.code(),
        stdout: decode_bytes(&output.stdout),
        stderr: decode_bytes(&output.stderr),
    })
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![ping_localhost])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

type CmdOutput = { code: number | null; stdout: string; stderr: string };

const result = await invoke<CmdOutput>('ping_localhost');
console.log(result.code, result.stdout);

プラグインの output() は出力を行ごとに集めて改行を付け直すため、元の改行と合わせて行の間に空行が入ります。行単位で使うなら lines() の後で空行を除きます。

動作確認

日本語版 Windows で npm run tauri dev を起動し、pingOnce('127.0.0.1') を呼ぶと次の行が返ります。

127.0.0.1 に ping を送信しています 32 バイトのデータ:
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128
127.0.0.1 の ping 統計:
    パケット数: 送信 = 1、受信 = 1、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 0ms、最大 = 0ms、平均 = 0ms

encoding: 'shift_jis' を外すと「invalid utf-8 sequence of 1 bytes from index 12」で reject されます(12 は最初の日本語の位置)。存在しないホスト名を渡すと終了コード 1 になり、理由は stderr ではなく stdout 側に入ります。

よくあるエラーと対処法

  • 「invalid utf-8 sequence of 1 bytes from index …」: 出力が UTF-8 ではありません。encoding を指定するか、'raw' で受け取って自分で変換します。
  • 「unknown encoding cp932」: 対応していないエンコーディング名です。shift_jis か windows-31j を使います。
  • 失敗したのに catch に来ない: 0 以外の終了コードでは reject されません。code を調べて自分でエラーにします。
  • 文字化けする: 指定したエンコーディングと実際の出力が違います。/U を付けた cmd の出力を shift_jis で読んでいないかなどを確かめます。

OS ごとの違いと注意点

  • Windows: 改行は \r\n です。shift_jis の決め打ちは日本語版向けで、ほかの言語の Windows ではコードページが違います(英語版は 437 など)。エラーメッセージの言語も環境によって変わるので、文言で分岐せず終了コードで判定します。
  • macOS / Linux: 出力はほぼ UTF-8 です。外部から強制終了された場合などは code が null になり、signal にシグナルの番号が入ります。
  • 共通: execute() は出力をすべてメモリにためてから返します。大量のログを出すコマンドや終わらないコマンドには向かないので、shell-009 の spawn() を使います。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する