PowerShell スクリプトを実行する

shell プラグインで powershell.exe を起動し、-File で .ps1 に引数を渡す。実行ポリシーで止まる理由と -ExecutionPolicy Bypass、日本語が Shift_JIS で届く問題の直し方も示す。

外部プロセス 対象: Tauri 2.x 更新日: 読了目安: 約9分 shell-013
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 1 行のコマンドの結果を JSON で受け取る
  4. 日本語が読めないとき
  5. 2. バックエンドから実装する (Rust)
  6. 同梱したスクリプトを -File で実行する
  7. 実行ポリシー
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

プリンターやディスクなど Windows 固有の情報は、PowerShell に任せると短く書けます。Tauri からは shell プラグインで powershell.exe(標準で入っている Windows PowerShell 5.1)を起動し、1 行で済む処理は -Command、スクリプトは -File で実行します。つまずきやすいのは実行ポリシー、引数の渡し方、文字コードの 3 点です。macOS / Linux 向けは Bash スクリプトを実行する を参照してください。

前提条件

shell プラグインを追加します。

npm run tauri add shell

フロントエンドから実行する内容は shell:allow-execute に登録します。powershell を "args": true で許可すると任意のコマンドを実行できてしまうので、引数はすべて固定しています(書き方は 外部コマンド(ls/dir等)を実行する)。

{
  "$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": "ps-printers",
          "cmd": "powershell",
          "args": [
            "-NoProfile",
            "-NonInteractive",
            "-Command",
            "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ConvertTo-Json -Compress -InputObject @(Get-Printer | Select-Object Name, DriverName)"
          ]
        }
      ]
    }
  ]
}
オプション意味
-NoProfileユーザーのプロファイルを読まない(PC ごとの設定に左右されない)
-NonInteractive入力待ち(Read-Host や確認)で止まらず、エラーにする
-ExecutionPolicy Bypassこの起動に限り実行ポリシーを無視する
-File <パス> <引数>スクリプトを実行し、後ろの引数を param() に文字列として渡す
-Command <文字列>文字列を PowerShell のコードとして実行する

-File と -Command は最後に置きます。それより後ろはすべてスクリプトやコマンドの一部として扱われます。

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

1 行のコマンドの結果を JSON で受け取る

結果は ConvertTo-Json で JSON にすると、表示用の整形に左右されずに扱えます。要素が 1 つだけだと配列でなくオブジェクトが出るので、-InputObject @(...) で常に配列にしています。実行ポリシーは .ps1 ファイルが対象で、-Command のコマンドには効きません。

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

type Printer = { Name: string; DriverName: string };

export async function listPrinters(): Promise<Printer[]> {
  // 引数はすべて capability 側で固定しているので、ここでは渡さない
  const out = await Command.create('ps-printers').execute();
  if (out.code !== 0) throw new Error(out.stderr.trim() || `exit code ${out.code}`);
  return JSON.parse(out.stdout) as Printer[];
}

日本語が読めないとき

日本語版の Windows では、PowerShell(5.1 も 7 も)がパイプに書き出す文字は Shift_JIS です。shell プラグインは既定で UTF-8 として読むので、日本語が混じると execute() が「invalid utf-8 sequence of 1 bytes from index 0」のようなエラーで失敗します。先頭で [Console]::OutputEncoding を UTF-8 にすれば、以降の出力は UTF-8 になります。コマンドを変えられないなら、Command.create('ps-printers', [], { encoding: 'shift_jis' }) のように JS 側で読み方を合わせます。

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

同梱したスクリプトを -File で実行する

利用者の入力を渡すスクリプトは -File で実行します。-File の後ろの引数は param() に文字列として渡り、$(...) や ; を含んでもコードとしては実行されません。空白や日本語を含む値もそのまま届きます。-Command は残りの引数をつないでコードとして実行するので、入力を混ぜると任意のコマンドを実行されるおそれがあります。

スクリプトは src-tauri/scripts/ に置き、リソースとして同梱します(画像やDBファイルを配布物に同梱する)。

{
  "bundle": {
    "resources": ["scripts/disk-report.ps1"]
  }
}

スクリプトは UTF-8(BOM 付き) で保存します。Windows PowerShell 5.1 は BOM の無いファイルを Shift_JIS として読むため、日本語の文字列が「縺薙s縺ォ縺。縺ッ」のように化けます。

param(
    [Parameter(Mandatory = $true)][string]$Drive
)
$ErrorActionPreference = 'Stop'  # エラーでスクリプトを止め、終了コードを 1 にする
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8  # ここから後の出力を UTF-8 にする

$d = Get-PSDrive -Name $Drive
[pscustomobject]@{
    Drive  = $Drive
    FreeGB = [math]::Round($d.Free / 1GB, 1)
    UsedGB = [math]::Round($d.Used / 1GB, 1)
} | ConvertTo-Json -Compress

Rust からもプラグインの app.shell().command() で起動します。capability の範囲に縛られないので、受け取った値はここで確かめます。スクリプトが動き出す前のエラー(実行ポリシーや必須パラメーターの不足)は Shift_JIS で出るので、UTF-8 として読めなければ Shift_JIS として読み直します。

use tauri::path::BaseDirectory;
use tauri::Manager;
use tauri_plugin_shell::process::Encoding;
use tauri_plugin_shell::ShellExt;

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

#[tauri::command]
async fn disk_report(app: tauri::AppHandle, drive: String) -> Result<String, String> {
    if drive.len() != 1 || !drive.chars().all(|c| c.is_ascii_alphabetic()) {
        return Err("drive must be a single letter".into());
    }
    let script = app
        .path()
        .resolve("scripts/disk-report.ps1", BaseDirectory::Resource)
        .map_err(|e| e.to_string())?;
    let output = app
        .shell()
        .command("powershell")
        .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"])
        .arg(&script)
        .args(["-Drive", drive.as_str()]) // ここから後ろはスクリプトの param() に渡る
        .output()
        .await
        .map_err(|e| e.to_string())?;
    if output.status.success() {
        Ok(decode_console(&output.stdout))
    } else {
        Err(decode_console(&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![disk_report])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

type DiskReport = { Drive: string; FreeGB: number; UsedGB: number };

const report = JSON.parse(await invoke<string>('disk_report', { drive: 'C' })) as DiskReport;
console.log(`${report.Drive}: 空き ${report.FreeGB} GB / 使用 ${report.UsedGB} GB`);

作業ディレクトリはアプリと同じなので、スクリプトと同じフォルダーのファイルは $PSScriptRoot を基準に開きます(作業ディレクトリの指定は コマンドに引数・作業ディレクトリ・環境変数を渡す)。

実行ポリシー

Windows のクライアント版の既定の実行ポリシーは Restricted で、.ps1 は実行できません。-ExecutionPolicy Bypass はその起動にだけ効き、管理者権限は要らず、PC の設定も変えないので、利用者に Set-ExecutionPolicy を頼む必要はありません。ただし会社の PC などでグループポリシーが実行ポリシーを決めている場合はそちらが優先され、この指定は効きません。Get-ExecutionPolicy -List で MachinePolicy か UserPolicy が Undefined 以外なら、この状態です。

動作確認

npm run tauri dev で起動して listPrinters() を呼ぶと、プリンターが 1 台でも配列で返ります。disk_report を 'C' で呼ぶと次のように出ます。

C: 空き 601.3 GB / 使用 329.1 GB

存在しないドライブ 'Q' を渡すと、invoke() が「Get-PSDrive : ドライブが見つかりません。名前 'Q' のドライブが存在しません。」で始まるエラーで失敗します。

よくあるエラーと対処法

  • 「invalid utf-8 sequence of 1 bytes from index …」: 出力が Shift_JIS です。[Console]::OutputEncoding を UTF-8 にするか、encoding: 'shift_jis' を指定します。
  • 「このシステムではスクリプトの実行が無効になっているため、ファイル … を読み込むことができません。」: 実行ポリシーで止まっています。-ExecutionPolicy Bypass を付けます。この文面も Shift_JIS で出るので、JS から既定の設定で実行すると上の UTF-8 のエラーに化けて見えます。
  • 「program not allowed on the configured shell scope: ps-printers」: allow の name と Command.create() の名前が違うか、shell:allow-spawn 側にだけ登録して execute() を呼んでいます。
  • スクリプトが失敗したのに終了コードが 0: Write-Error はスクリプトを止めません。$ErrorActionPreference = 'Stop' にするか、失敗したら exit 1 で終えます。

OS ごとの違いと注意点

  • Windows: powershell.exe(5.1)は Windows 10 / 11 に標準で入っています。PowerShell 7 の pwsh は別途インストールした PC にしか無いので、配布するアプリでは powershell が確実です。pwsh は BOM の無い UTF-8 のスクリプトも正しく読みますが、出力が Shift_JIS なのは同じです。
  • Windows: shell プラグイン(JS と app.shell())で起動するとコンソールウィンドウは開かないので、-WindowStyle Hidden は要りません。
  • macOS / Linux: powershell は無く、pwsh も標準では入っていないので、Bash スクリプトを実行する の方法を使います。
  • 共通: 時間のかかるスクリプトの進み具合は、spawn() で 1 行ずつ受け取ります(コマンドの出力をリアルタイムで受け取る)。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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