执行外部命令
本教程共 48 篇 · 第 33 篇 · 更新于 2026-08-09 · 约 9 分钟阅读
本节目标:读完你能用 plugin-shell 在前端 JS 和 Rust 端执行外部程序、读取子进程输出,并了解 sidecar 机制怎么把外部二进制文件打包进应用一起分发。
桌面应用有时候需要调用外部程序——比如调 ffmpeg 转码、调 git 拉代码、或者跑一个自己写的 Python 脚本。Tauri 提供了 tauri-plugin-shell(Shell 插件)来干这件事。它让你能在前端 JS 或 Rust 端启动子进程、传参数、拿输出,甚至能把一个外部可执行文件「捆绑」到应用里,用户安装时自带,不用额外装依赖。
NoteTauri 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() 会等命令跑完,返回一个对象,里面有 stdout、stderr、code 等字段。
但 .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-execute和allow-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.json 的 bundle 字段里加 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.json 里 externalBin 数组中的某一项完全匹配。
给 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 工具)。权限配置上,execute 用 shell:allow-execute,spawn 用 shell:allow-spawn,sidecar 加 sidecar: true 标记。