Bash スクリプトを実行する

同梱した .sh を shell プラグインで実行する。シバンと実行権限が要る直接実行と、bash にパスを渡す方法の違い、改行コード、macOS の zsh と PATH の落とし穴も示す。

外部プロセス 対象: Tauri 2.x 更新日: 読了目安: 約9分 shell-014
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. sh と bash、macOS の zsh
  5. 動作確認
  6. よくあるエラーと対処法
  7. OS ごとの違いと注意点
  8. 関連レシピ

バックアップや一括変換など、手元にある Bash スクリプトを macOS / Linux 向けのアプリから呼び出す方法です。実行のしかたは「スクリプト自体を実行する(シバンと実行権限が必要)」と「bash にパスを渡す(どちらも不要)」の 2 通りがあり、失敗したときの出方も違います。ここでは 2 つの使い分けに加え、改行コード、sh と bash の違い、macOS の zsh と PATH にまつわる落とし穴を説明します。Windows 向けは PowerShell スクリプトを実行する を参照してください。

前提条件

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

npm run tauri add shell

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

{
  "bundle": {
    "resources": ["scripts/backup.sh"]
  }
}

フロントエンドから実行する内容は shell:allow-execute に登録します。cmd の先頭には $RESOURCE のようなパス変数を書けるので、同梱したスクリプトそのものを登録できます。これならフロントエンドから実行できるのはこのスクリプトだけです。bash を "args": true で許可すると bash -c で何でも実行できてしまうので避けます(allow の書き方は 外部コマンド(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": "backup",
          "cmd": "$RESOURCE/scripts/backup.sh",
          "args": [{ "validator": "[A-Za-z0-9_-]{1,32}" }]
        }
      ]
    }
  ]
}

スクリプトの例です。1 行目のシバン(#!)で、どのプログラムに実行させるかを指定します。改行コードは LF で保存します。

#!/bin/bash
set -euo pipefail  # 失敗したコマンドがあればそこで止め、0 以外の終了コードで終わる

name="${1:?バックアップ名を指定してください}"
src="$HOME/Documents/MyApp"
dest_dir="$HOME/Backups"
mkdir -p "$dest_dir"

dest="$dest_dir/$name-$(date +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$dest" -C "$(dirname "$src")" "$(basename "$src")"
echo "$dest"  # 最後の行を結果としてアプリへ返す

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

登録した backup をそのまま実行します。スクリプトを直接実行するので、実行権限が必要です。macOS / Linux では chmod +x src-tauri/scripts/backup.sh で付けます。Windows からリポジトリに登録するなら、git update-index --chmod=+x src-tauri/scripts/backup.sh で Git に記録します。配布物でも権限が保たれているかは、ビルドした成果物の中を ls -l で確かめてください。

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

export async function runBackup(name: string): Promise<string> {
  const out = await Command.create('backup', [name]).execute();
  if (out.code !== 0) {
    throw new Error(out.stderr.trim() || `exit code ${out.code}`); // set -e で止まったときは stderr に理由が出る
  }
  const lines = out.stdout.trim().split('\n');
  return lines[lines.length - 1]; // 最後の行 = 保存先
}

引数は配列の 1 要素がそのまま $1 として届き、シェルを通さないので、空白や ; を含んでも別のコマンドとしては解釈されません。スクリプト側では "$1" のように必ず引用符で囲みます。

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

Rust からは bash にスクリプトのパスを渡します。この形なら実行権限もシバンも要らず、配布物で権限が落ちても動きます。capability の範囲には縛られないので、受け取った値はここで確かめます。

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

#[tauri::command]
async fn run_backup(app: tauri::AppHandle, name: String) -> Result<String, String> {
    let valid = !name.is_empty()
        && name.len() <= 32
        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
    if !valid {
        return Err("invalid name".into());
    }
    let script = app
        .path()
        .resolve("scripts/backup.sh", BaseDirectory::Resource)
        .map_err(|e| e.to_string())?;
    let output = app
        .shell()
        .command("/bin/bash") // シバンではなく、ここで指定したプログラムが実行する
        .arg(&script)
        .arg(&name)
        // Finder から起動したアプリの PATH は最小限なので、Homebrew の場所を足す
        .env("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin")
        .output()
        .await
        .map_err(|e| e.to_string())?;
    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        Ok(stdout.trim().lines().last().unwrap_or_default().to_string())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
    }
}

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

const saved = await invoke<string>('run_backup', { name: 'daily' });
console.log('保存先:', saved);

環境変数や作業ディレクトリを渡す方法は コマンドに引数・作業ディレクトリ・環境変数を渡す を参照してください。

sh と bash、macOS の zsh

  • sh script.sh で実行すると、シバンは無視されて /bin/sh が読みます。Ubuntu などの /bin/sh は bash ではないので、[[ ]] や配列のような bash の書き方はエラーになります。bash 用のスクリプトは bash に渡します。
  • macOS の /bin/bash は古いバージョン 3.2 のままです。連想配列(declare -A)や mapfile は使えません。#!/usr/bin/env bash は PATH から bash を探すので、ターミナルから tauri dev したときは Homebrew の新しい bash(入っていれば)、Finder から起動した配布版では /bin/bash になり、開発中だけ動くことがあります。
  • macOS の標準のシェルは zsh ですが、shell プラグインは利用者のシェルを通さずにプログラムを起動します。~/.zshrc などは読まれないので、そこで定義したエイリアスや関数は使えません。PATH も、ターミナルから tauri dev したときはターミナルのものを引き継ぎますが、Finder から起動すると ~/.zprofile などで足した場所は入りません。zsh 用に書いたスクリプトなら、シバンを #!/bin/zsh にして /bin/zsh に渡します。

動作確認

macOS か Linux で chmod +x src-tauri/scripts/backup.sh を実行してから npm run tauri dev で起動し、runBackup('daily') を呼びます。~/Documents/MyApp があれば ~/Backups/ に圧縮ファイルができ、保存先が返ります。

保存先: /Users/you/Backups/daily-20260912-101500.tar.gz

runBackup('my backup') のように空白を含む名前を渡すと、validator に合わないので実行前に止まります。PATH の問題は tauri dev では再現しないので、npm run tauri build した .app を Finder から起動して確かめます。

よくあるエラーと対処法

  • 「Permission denied (os error 13)」という趣旨のエラー: スクリプトに実行権限がありません。chmod +x で付けるか、2 章のように bash に渡します。chmod の前に一度 tauri dev していた場合は、src-tauri/target/debug/scripts/ にコピー済みのファイルにも付けます。
  • 「Exec format error (os error 8)」という趣旨のエラー: 1 行目がシバンになっていません。#!/bin/bash を先頭に書きます(前に空行や BOM があっても失敗します)。
  • 「No such file or directory」という趣旨のエラー、または $'\r': command not found: 改行コードが CRLF です。シバンの行末に \r が付き、bash が見つからなくなります。LF で保存し直し、Git なら .gitattributes に *.sh text eol=lf を書いて CRLF への変換を止めます。
  • 配布版だけ command not found(終了コード 127)になる: PATH に Homebrew などの場所が入っていません。スクリプトで絶対パスを使うか、上の例のように PATH を渡します。
  • 「program not allowed on the configured shell scope: backup」: 引数が validator に合っていないか、name が違います。

OS ごとの違いと注意点

  • macOS: Homebrew の場所は Apple シリコンなら /opt/homebrew/bin、Intel なら /usr/local/bin です。.app の中のスクリプトを書き換えると署名が壊れるので、設定値は引数や環境変数で渡します。
  • Linux: bash は /bin/bash にあるのが一般的ですが、/bin/sh は Debian 系では dash です。
  • Windows: bash は標準では入っていません。bash という名前で WSL が起動することもあり、その場合は Linux 側で動くので Windows のパスはそのまま使えません。Windows では PowerShell 版を用意して OS ごとに呼び分けます(PowerShell スクリプトを実行する)。
  • 共通: 長く動くスクリプトは spawn() で起動して kill() で止められますが、kill は強制終了なので、スクリプトの trap による後始末は動きません(時間のかかるプロセスを管理・強制終了する)。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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