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()を使います。
