バックアップや一括変換など、手元にある 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による後始末は動きません(時間のかかるプロセスを管理・強制終了する)。
