外部コマンドに渡せる情報は、引数、作業ディレクトリ(カレントディレクトリ)、環境変数の 3 つです。JS では Command.create(name, args, { cwd, env })、Rust ではプラグインの command() に .args()・.current_dir()・.env() を続けます。ここでは、指定したフォルダで git log を実行する例で 3 つをまとめて示します。コマンドを許可する scope の書き方は 外部コマンド(ls/dir等)を実行する、結果の読み方は shell-003 を参照してください。
前提条件
Shell プラグインの導入と権限は shell-001 のとおりです。この例では Git を 2 通りの引数で許可します。--max-count= の後ろは 1〜999 の数字だけ、-- の後ろは改行を含まない任意の文字列を受け付けます。
{
"$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": "git-log",
"cmd": "git",
"args": ["log", "--oneline", { "validator": "--max-count=[1-9][0-9]{0,2}" }]
},
{
"name": "git-log-file",
"cmd": "git",
"args": ["log", "--oneline", "--", { "validator": ".+" }]
}
]
}
]
}
1. フロントエンドから実装する (TypeScript)
import { Command } from '@tauri-apps/plugin-shell';
// repoDir で git log を実行し、1 行ずつの配列で返す
export async function gitLog(repoDir: string, count = 20): Promise<string[]> {
const output = await Command.create('git-log', ['log', '--oneline', `--max-count=${count}`], {
cwd: repoDir, // 絶対パスで渡す
env: { GIT_TERMINAL_PROMPT: '0' }, // 認証を求められても入力待ちで止まらず、すぐ失敗させる
}).execute();
if (output.code !== 0) throw new Error(output.stderr.trim());
return output.stdout.split(/\r?\n/).filter((line) => line !== '');
}
// 1 つのファイルの履歴。-- の後ろなので「-」で始まる名前もオプション扱いされない
export async function fileHistory(repoDir: string, file: string): Promise<string> {
const output = await Command.create('git-log-file', ['log', '--oneline', '--', file], {
cwd: repoDir,
}).execute();
return output.stdout;
}
引数: 配列の 1 要素が 1 つの引数
引数はシェルを通さずにそのままプログラムへ届きます。空白を含むパスも、引用符で囲まずに 1 要素として入れます。自分で " を付けると引用符まで値の一部になります。逆に 'log --oneline' のように 1 つの文字列にまとめると、空白で分割されずに 1 個の引数になります。第 2 引数に文字列を 1 つだけ渡した場合も、1 要素の配列として扱われます。
利用者の入力を引数にするときは、- で始まる値がオプションとして解釈される危険があります。多くのコマンドは -- を「ここから後ろはオプションではない」という区切りとして扱うので、scope の固定値として置いておきます。また scope の配列は長さが決まっているため、「ファイルを何個でも」のような可変個の引数は表せません。args: true に緩めるより、Rust のコマンドにして中で検査する方が安全です。
作業ディレクトリ: cwd は絶対パスで
cwd は、コマンドが相対パスを解決する基準です。Git のようにカレントディレクトリで動作が決まるコマンドには欠かせません。省略するとアプリ自身のカレントディレクトリになりますが、これは tauri dev のときとインストール後にショートカットから起動したときとで違うので当てにしません。@tauri-apps/api/path の documentDir()・appDataDir() と join() で組み立てるか、フォルダを選択させて 得た絶対パスを渡します。存在しないフォルダを指定すると起動できずに reject されます。なお、cwd に置いたプログラムを名前だけで起動することはできません。プログラムは PATH から探されます。
環境変数: アプリの環境に追加される
env に書いた変数は、アプリの環境変数を引き継いだうえで追加・上書きされます。PATH などは残るので、普段どおりコマンドが見つかります。主な用途は、ツールの動作の切り替えです。
| 変数の例 | 効果 |
|---|---|
GIT_TERMINAL_PROMPT=0 | Git が認証の入力を待たずに失敗する |
PYTHONIOENCODING=utf-8 | Python の出力を UTF-8 にする(shell-003) |
NO_COLOR=1 | 対応しているツールで、色付けの制御文字を出さない |
cwd と env は scope で制限されません。 scope が検査するのはプログラムと引数だけなので、JS からはどのフォルダでも、どんな環境変数でも指定できます。node を固定の引数で許可していても NODE_OPTIONS で別のスクリプトを読み込ませられますし、PATH を書き換えると別のプログラムが起動することもあります。利用者の入力を env や cwd にそのまま使わず、固定したいものは Rust 側で決めます。
2. バックエンドから実装する (Rust)
プラグインの command() は、std::process::Command と同じ名前のメソッドで引数・作業ディレクトリ・環境変数を指定します。Rust 側は scope の対象外なので、フォルダの存在や数値の範囲を自分で確かめます。アプリの環境変数を引き継ぎたくないときは .env_clear() で空にしてから必要なものだけ足します(JS の型定義にはこの指定がありません)。
use std::path::PathBuf;
use tauri_plugin_shell::ShellExt;
/// repo_dir で git log を実行し、1 行ずつ返す
#[tauri::command]
async fn git_log(app: tauri::AppHandle, repo_dir: String, count: u32) -> Result<Vec<String>, String> {
let dir = PathBuf::from(&repo_dir);
if !dir.is_absolute() || !dir.is_dir() {
return Err(format!("フォルダがありません: {repo_dir}"));
}
let output = app
.shell()
.command("git")
.args(["log", "--oneline"])
.arg(format!("--max-count={}", count.clamp(1, 100)))
.current_dir(&dir)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await
.map_err(|e| e.to_string())?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
let text = String::from_utf8_lossy(&output.stdout);
// 空行を除く(Rust の output() は行の間に空行が入る。shell-003 を参照)
Ok(text.lines().filter(|l| !l.is_empty()).map(String::from).collect())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![git_log])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
const lines = await invoke<string[]>('git_log', { repoDir: 'C:\\work\\my-app', count: 10 });
動作確認
npm run tauri dev で起動し、Git のリポジトリのフォルダを渡して gitLog() を呼ぶと、['a1b2c3d README を更新', ...] のようなコミットの一覧が返ります。リポジトリではないフォルダを渡すと code が 128 になり、stderr の内容がエラーとして投げられます。
fatal: not a git repository (or any of the parent directories): .git
よくあるエラーと対処法
- 「git: 'log --oneline' is not a git command. See 'git --help'.」: 引数を 1 つの文字列にまとめています。配列の要素に分けます。
- 「ディレクトリ名が無効です。 (os error 267)」(日本語版 Windows)/「No such file or directory (os error 2)」(macOS・Linux):
cwdのフォルダが存在しません。文言は OS の言語によって変わります。 - 「program not allowed on the configured shell scope: git-log」: 引数が validator に合っていません(
countに 0 や 1000 以上を渡した、など)。開発ビルドではターミナルに位置と正規表現が出ます。 - 「invalid args
countfor commandgit_log」で始まるエラー: Rust 版のcountはu32なので、負の数や小数は呼び出しの段階で失敗します。
OS ごとの違いと注意点
- Windows: 環境変数の名前は大文字と小文字を区別しないので、
PathとPATHは同じ変数として上書きされます。 - macOS: Finder や Dock から起動したアプリには、ターミナルの設定ファイル(
.zshrcなど)で足した PATH が引き継がれません。Homebrew で入れたコマンドが、tauri devでは動くのにビルドしたアプリでは見つからない、という形で表れます。scope のcmdをフルパスにするか、envでPATHを補います。 - iOS / Android: コマンドの実行自体ができません(shell-001)。
