app.shell - Shell 命令 API
执行 Shell 命令。支持协程内自动异步执行。
权限:L2 敏感 — 需声明,安装时显示权限警告(
"shell")
方法
app.shell.execute(command, options?)
执行 Shell 命令(同步)。
参数:
command(string) - 要执行的命令options(table, 可选) - 选项env(table) - 自定义环境变量。PATH的值会追加到默认 PATH 之后,其余变量直接覆盖
返回值: table
code(number) - 退出码(0 表示成功)stdout(string) - 标准输出stderr(string) - 标准错误。跟 stdout 是分开的两个管道,命令写到 stderr 的内容只会出现在这里
-- 简单命令
local result = app.shell.execute("ls -la")
if result.code == 0 then
app.log.info(result.stdout)
else
app.log.error(result.stdout)
end
-- 执行脚本
local result = app.shell.execute("python3 /path/to/script.py")
-- 获取命令输出
local result = app.shell.execute("which git")
local gitPath = result.stdout:gsub("\n", "")
协程内自动异步:在协程内调用时,会自动 yield 并在后台执行,对调用者透明。
-- 在协程内调用
app.thread.create(function()
-- 自动异步执行,不会阻塞主线程
local result = app.shell.execute("sleep 5 && echo done")
app.log.info(result.stdout)
end)
app.shell.start(command, options?)
启动命令并立刻返回一个任务句柄,不阻塞。跟 executeAsync 的区别是:它能拿到结果,
也能中途取消。
参数:
command(string) - 要执行的命令options(table, 可选) - 跟execute()同一套,支持env
返回值: table - 任务句柄,带三个方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
task:isDone() |
boolean |
命令是否已结束 |
task:result() |
table 或 nil |
结果,只能取一次(见下) |
task:cancel() |
boolean |
终止命令 |
task:result() 返回的表:
code(number) - 退出码(0 表示成功;被cancel()取消时为-1)stdout(string) - 标准输出stderr(string) - 标准错误,跟stdout分开,语义与execute()一致
local task = app.shell.start("find / -name '*.log' 2>/dev/null")
-- 轮询
local timer
timer = app.timer.start(0.5, function()
if not task:isDone() then return end
local r = task:result()
if r.code == 0 then
app.log.info(r.stdout)
else
app.log.error("失败: " .. r.stderr)
end
timer:stop()
end)
result()取完即清。 第一次调用会把结果交出来并从内部表里删掉, 第二次调用返回nil。要用多次就先存到变量里:
local r = task:result() -- 存下来
if r then
useIt(r.stdout)
useAgain(r.code) -- 用变量,不要再调 task:result()
end
取消一个还没结束的任务:
if not task:isDone() then
task:cancel()
-- 此后 task:result() 返回 { code = -1, stdout = "", stderr = "Cancelled" }
end
什么时候用哪个:
| 需求 | 用 |
|---|---|
| 要结果,且在协程里(可以阻塞) | execute()——协程内会自动 yield 到后台,对调用者透明 |
| 要结果,但不能阻塞;或者需要中途取消 | start() |
| 发了就不管 | start() 之后不碰那个句柄即可 |
注意事项
- 命令通过 /bin/bash -c 执行:可以使用 bash 特性
- 超时时间:默认 60 秒超时
- 路径处理:文件路径可能包含空格,需要正确引用
- 返回值:始终检查
result.code判断命令是否成功 - 编码:输出为 UTF-8 编码
示例
调用外部工具
function MyPlugin:handleConvert(context)
-- 检查工具是否安装
local result = app.shell.execute("which convert")
if result.code ~= 0 then
app.dialog.alert({title = "错误", message = "请先安装 ImageMagick"})
return
end
local files = context.selectedFiles
local p = app.progress.create({title = "转换中", indeterminate = false})
for i, file in ipairs(files) do
p:update(i * 100 / #files)
local output = app.path.withExtension(file, "png")
-- 使用引号处理路径中的空格
local cmd = string.format('convert "%s" "%s"', file, output)
local result = app.shell.execute(cmd)
if result.code ~= 0 then
app.log.error("转换失败: " .. result.stdout)
end
end
p:close()
app.notification.show("完成", "转换了 " .. #files .. " 个文件")
end
调用 Git 命令
function MyPlugin:handleGitStatus(context)
local dir = context.currentDirectory
-- 切换到目录执行命令
local result = app.shell.execute(string.format('cd "%s" && git status --short', dir))
if result.code ~= 0 then
app.dialog.alert({title = "错误", message = "不是 Git 仓库"})
return
end
if result.stdout == "" then
app.dialog.alert({title = "Git 状态", message = "工作目录干净"})
else
app.dialog.alert({title = "Git 状态", message = result.stdout})
end
end
使用 FFmpeg 提取音频
function MyPlugin:handleExtractAudio(context)
-- 检查 ffmpeg
local result = app.shell.execute("which ffmpeg")
if result.code ~= 0 then
app.dialog.alert({title = "错误", message = "请先安装 ffmpeg"})
return
end
for _, video in ipairs(context.selectedFiles) do
local audio = app.path.withExtension(video, "mp3")
local cmd = string.format(
'ffmpeg -i "%s" -vn -acodec libmp3lame -q:a 2 "%s"',
video, audio
)
local result = app.shell.execute(cmd)
if result.code == 0 then
app.log.info("提取成功: " .. audio)
else
app.log.error("提取失败: " .. result.stdout)
end
end
end
后台运行长任务
function MyPlugin:handleBackgroundTask(context)
local script = context.selectedFiles[1]
if not script then return end
-- 后台执行脚本
app.shell.start(string.format('"%s" > /tmp/output.log 2>&1', script))
app.notification.show("已启动", "脚本正在后台运行")
end
获取系统信息
function MyPlugin:getSystemInfo()
local info = {}
-- 获取 macOS 版本
local result = app.shell.execute("sw_vers -productVersion")
if result.code == 0 then
info.osVersion = result.stdout:gsub("\n", "")
end
-- 获取 CPU 信息
result = app.shell.execute("sysctl -n machdep.cpu.brand_string")
if result.code == 0 then
info.cpu = result.stdout:gsub("\n", "")
end
-- 获取内存大小
result = app.shell.execute("sysctl -n hw.memsize")
if result.code == 0 then
local bytes = tonumber(result.stdout)
if bytes then
info.memory = string.format("%.1f GB", bytes / 1024 / 1024 / 1024)
end
end
return info
end
处理命令输出
function MyPlugin:parseOutput(command)
local result = app.shell.execute(command)
if result.code ~= 0 then
return nil, result.stdout
end
-- 按行分割输出
local lines = {}
for line in result.stdout:gmatch("[^\n]+") do
table.insert(lines, line)
end
return lines, nil
end
function MyPlugin:handleListProcesses(context)
local lines, err = self:parseOutput("ps aux | head -20")
if err then
app.log.error(err)
return
end
app.dialog.alert({
title = "进程列表",
message = table.concat(lines, "\n")
})
end
工具方法
app.shell.quote(str)
对字符串进行 POSIX 单引号转义,用于安全拼接 shell 命令参数。
参数:
str(string) — 需要转义的字符串
返回值: string — 转义后的字符串(用单引号包裹)
-- 安全处理包含空格或特殊字符的路径
local cmd = "ls -la " .. app.shell.quote("/path/to/my file.txt")
app.shell.execute(cmd)
-- 处理包含单引号的路径
app.shell.quote("it's a file") -- "'it'\\''s a file'"