首页 / Tauri 2 入门教程 / 执行外部命令

Tauri 2 入门教程

执行外部命令

本教程共 48 篇 · 第 33 篇 · 更新于 2026-08-09 · 约 9 分钟阅读

TauriTauri 2 入门教程plugin-shellsidecar外部命令子进程spawn

本节目标:读完你能用 plugin-shell 在前端 JS 和 Rust 端执行外部程序、读取子进程输出,并了解 sidecar 机制怎么把外部二进制文件打包进应用一起分发。

桌面应用有时候需要调用外部程序——比如调 ffmpeg 转码、调 git 拉代码、或者跑一个自己写的 Python 脚本。Tauri 提供了 tauri-plugin-shell(Shell 插件)来干这件事。它让你能在前端 JS 或 Rust 端启动子进程、传参数、拿输出,甚至能把一个外部可执行文件「捆绑」到应用里,用户安装时自带,不用额外装依赖。

Note

Tauri 2 中,原来 Tauri 1 的 shell.open(用系统默认程序打开 URL 或文件)已经拆到了独立的 Opener 插件tauri-plugin-opener)。本节只讲执行外部命令和 sidecar,不涉及 open

安装与注册 shell 插件

安装过程和前面几个插件一样——

# 1. Rust 端添加依赖
cd src-tauri
cargo add tauri-plugin-shell
// 2. 在 Builder 中注册插件
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("运行 Tauri 应用时出错");
}
# 3. 前端安装 JS 包
npm install @tauri-apps/plugin-shell

装完之后还不能直接用——Shell 插件的权限默认是全部锁住的,你需要在 capability 里显式声明「允许执行哪个命令」。这是安全设计:不能让前端随便执行任意命令。

权限配置:声明能执行哪些命令

Shell 插件的权限配置比其他插件更严格。你不能只写一个 "shell:default" 就完事,而是要具体声明每个允许执行的命令,包括命令名、实际调用的程序、参数格式。这样做的好处是:即使前端代码被注入恶意脚本,攻击者也只能跑你预先声明的那几个命令。

src-tauri/capabilities/default.json 里这样配:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "主窗口权限",
  "windows": ["main"],
  "permissions": [
    {
      "identifier": "shell:allow-execute",
      "allow": [
        {
          "name": "exec-sh",
          "cmd": "sh",
          "args": [
            "-c",
            { "validator": "\\S+" }
          ],
          "sidecar": false
        }
      ]
    }
  ]
}

逐字段解释:

  • identifier: shell:allow-execute 表示允许前端用 Command.create() 创建的命令并调用 .execute() 方法。如果要用 .spawn()(异步、流式),需要改用 shell:allow-spawn
  • name: 你给这个命令起的别名,前端用 Command.create('exec-sh', ...) 来引用它。
  • cmd: 实际要执行的系统程序,这里是 sh(Linux/macOS 的 shell)。
  • args: 参数模板。静态值直接写字符串(如 "-c"),动态值用 { "validator": "正则" } 约束允许的输入。"\\S+" 表示匹配一个或多个非空白字符。
  • sidecar: false 表示这是调用系统已安装的程序,不是 sidecar 捆绑的二进制。
Tip

validator 用正则表达式来限制前端传进来的参数值,是防止命令注入的关键防线。比如只允许 \S+(非空白字符),攻击者就没法塞 ; rm -rf / 这种东西进来。写正则时宁可严一点,别图省事写 .*

前端 JS:Command.create 与 execute

配好权限后,前端就能用了。核心 API 是 Command.create()

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

// 创建命令:名字要和 capability 里的 name 对应
const result = await Command.create("exec-sh", [
  "-c",
  "echo 'Hello World!'",
]).execute();

console.log(result.stdout); // Hello World!
console.log(result.code);   // 0

Command.create 的第一个参数是你在 capability 里声明的命令名(name),第二个参数是实际传入的参数数组。.execute() 会等命令跑完,返回一个对象,里面有 stdoutstderrcode 等字段。

.execute() 是「等完才返回」的模式。如果你要跑一个长时间运行的程序(比如一个本地服务器),需要边跑边读输出,那就得用 .spawn()

const command = Command.create("exec-sh", ["-c", "ping 127.0.0.1"]);

const child = await command.spawn();

// 监听标准输出
command.stdout.on("data", (line) => {
  console.log("输出:", line);
});

// 监听标准错误
command.stderr.on("data", (line) => {
  console.log("错误:", line);
});

// 监听结束
command.on("close", (data) => {
  console.log("退出码:", data.code);
});

.spawn() 立刻返回一个 Child 实例,不会等命令结束。你可以通过事件监听器持续接收输出,也可以通过 child.write() 向子进程的标准输入写数据。注意用 spawn 时,capability 里的 identifier 要改成 shell:allow-spawn

Warning

spawn 的 capability 配置和 execute 不同——identifier 必须是 shell:allow-spawn,否则运行时会报权限错误。如果你两种模式都要用,就同时声明两个 permission 条目,指向同一个命令名但分别用 allow-executeallow-spawn

Rust 端:ShellExt trait

在 Rust 端,Shell 插件通过 ShellExt trait 扩展 AppHandle,给你一个 .shell() 方法来访问命令执行能力:

use tauri_plugin_shell::ShellExt;

// 在某个命令或 setup 里,拿到 app_handle
let shell = app_handle.shell();

let output = tauri::async_runtime::block_on(async move {
    shell
        .command("echo")
        .args(["Hello from Rust!"])
        .output()
        .await
        .unwrap()
});

if output.status.success() {
    println!(
        "Result: {:?}",
        String::from_utf8(output.stdout)
    );
} else {
    println!("Exit with code: {}", output.status.code().unwrap());
}

Rust 端的 .command() 直接传程序名,不需要像前端那样在 capability 里预先声明。因为 Rust 端代码是你自己写的,不存在「前端被注入」的风险。不过 .output() 是同步等完的,长时间运行的程序建议用 .spawn() 拿到 Child,再异步读事件。

Sidecar:把外部二进制打包进应用

有时候你依赖的外部程序并不是用户机器上预装的——比如一个用 Python 写的 CLI 工具,你不能指望每个用户都装了 Python。Tauri 的 sidecar(边车) 机制解决这个问题:把外部可执行文件打包到应用安装包里,随应用一起分发。

配置 externalBin

src-tauri/tauri.conf.jsonbundle 字段里加 externalBin

{
  "bundle": {
    "externalBin": [
      "binaries/my-sidecar"
    ]
  }
}

路径是相对于 tauri.conf.json 所在目录(即 src-tauri/)的。上面这行表示二进制文件放在 src-tauri/binaries/my-sidecar

但这里有个关键细节:Tauri 要求每个支持的平台都有一个带目标三元组(target triple)后缀的文件。比如 binaries/my-sidecar 这个配置,实际需要这些文件:

  • src-tauri/binaries/my-sidecar-x86_64-pc-windows-msvc.exe(Windows)
  • src-tauri/binaries/my-sidecar-x86_64-unknown-linux-gnu(Linux)
  • src-tauri/binaries/my-sidecar-aarch64-apple-darwin(macOS ARM)

查看当前平台的目标三元组:

rustc --print host-tuple
# 输出例如:x86_64-pc-windows-msvc
Note

--print host-tuple 需要 Rust 1.84.0 及以上。旧版本可以用 rustc -Vv 然后找 host: 那一行。

Sidecar 的权限配置

Sidecar 的权限和普通命令类似,但多了一个 sidecar: true 标记:

{
  "identifier": "shell:allow-execute",
  "allow": [
    {
      "name": "binaries/my-sidecar",
      "sidecar": true
    }
  ]
}

name 要和 externalBin 里写的路径完全一致。用 spawn 的话把 identifier 改成 shell:allow-spawn

前端调用 sidecar

前端用 Command.sidecar() 而不是 Command.create()

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

const command = Command.sidecar("binaries/my-sidecar");
const output = await command.execute();
console.log(output.stdout);

Command.sidecar 的参数必须和 tauri.conf.jsonexternalBin 数组中的某一项完全匹配。

给 sidecar 传参数

Sidecar 也可以传参数,而且参数同样需要在 capability 里用 validator 声明。先配权限:

{
  "identifier": "shell:allow-execute",
  "allow": [
    {
      "name": "binaries/my-sidecar",
      "sidecar": true,
      "args": [
        "arg1",
        "-a",
        { "validator": "\\S+" }
      ]
    }
  ]
}

这里 args 定义了参数的固定顺序:前两个是静态值("arg1""-a"),第三个是动态值(用正则 \S+ 约束)。前端调用时必须按这个顺序传所有参数:

const command = Command.sidecar("binaries/my-sidecar", [
  "arg1",
  "-a",
  "any-string-matches-validator",
]);
const output = await command.execute();
Tip

如果你不想限制参数,可以在 args 里写 "args": true,表示允许任意参数。但这会降低安全性——只有在 sidecar 本身能安全处理任意输入时才这样做。

Rust 端调用 sidecar

Rust 端调用 sidecar 用 .shell().sidecar()

use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
use tauri::Emitter;

let sidecar_command = app.shell().sidecar("my-sidecar").unwrap();
let (mut rx, mut child) = sidecar_command
    .spawn()
    .expect("Failed to spawn sidecar");

tauri::async_runtime::spawn(async move {
    while let Some(event) = rx.recv().await {
        if let CommandEvent::Stdout(line_bytes) = event {
            let line = String::from_utf8_lossy(&line_bytes);
            println!("sidecar 输出: {}", line);
            // 向 sidecar 的标准输入写数据
            child.write("message from Rust\n".as_bytes()).unwrap();
        }
    }
});

注意 .sidecar() 的参数是文件名(不含路径前缀和平台后缀),比如 externalBin 写的是 "binaries/my-sidecar",这里就传 "my-sidecar"spawn() 返回一个接收器 rx 和一个 Child,你可以异步读取 stdout 事件,也可以通过 child.write() 写 stdin。

小结

tauri-plugin-shell 让 Tauri 应用能执行外部程序:前端用 Command.create() 调用系统命令,用 .execute() 等完拿结果或 .spawn() 流式读输出;Rust 端用 ShellExt trait 的 .shell().command()。所有前端调用都必须在 capability 里预先声明,用 validator 正则约束参数,防止命令注入。Sidecar 机制通过 externalBin 配置把外部二进制文件打包进应用,配合 Command.sidecar() 调用,适合分发用户不需要单独安装的依赖程序(如 Python CLI 工具)。权限配置上,executeshell:allow-executespawnshell:allow-spawn,sidecar 加 sidecar: true 标记。