脚本开发指南
本文档面向已经会写一些 shell / python / applescript 的用户——讲清楚菜单脚本能用哪些解释器、怎么传参、环境怎样、怎么调试。如果你刚开始用,先看 脚本菜单(基础)。
1. 解释器选择
iRightMenu Pro 内置以下 alias,”脚本类型”字段写哪个名字就调用哪个解释器:
| 你写的脚本类型 | 实际调用 |
|---|---|
bash / sh / shell |
/bin/bash |
zsh |
/bin/zsh |
python / python3 |
/usr/bin/python3 |
applescript |
/usr/bin/osascript |
javascript |
/usr/bin/osascript -l JavaScript(JXA) |
ruby |
/usr/bin/ruby |
perl |
/usr/bin/perl |
swift |
/usr/bin/swift |
php |
/usr/bin/php |
任何其他名字(如 lua、node、go、rust…)都通过 /usr/bin/env <name> 在 PATH 里查找——只要解释器装在 PATH 里就能用。
也可以直接填完整路径作为脚本类型,例如:
/opt/homebrew/bin/python3(Homebrew 的 Python)/Users/你/.pyenv/shims/python(pyenv)/opt/homebrew/bin/node
填完整路径最稳定——不依赖 PATH 优先级。
2. Shell 脚本
最常用。脚本类型填 bash / sh / zsh。iRightMenu 自动添加 shebang,你不需要自己写 #!/bin/bash。
参数:
$1、$2、$3… = 选中文件的绝对路径"$@"= 所有选中项(推荐写法)$#= 选中项数量
echo "选中了 $# 个项目"
for f in "$@"; do
if [ -d "$f" ]; then
echo "[文件夹] $f"
else
echo "[文件] $f"
fi
done
踩坑提醒:
- 路径含空格——必须双引号包变量,永远写
"$f"不要$f - 用
set -e让脚本第一个失败立即退出(除非你想容错) - 脚本是非交互式 shell,不读
~/.zshrc里的 alias / function
3. Python 脚本
脚本类型填 python 或 python3,都映射到 /usr/bin/python3(macOS 自带的 Python 3)。
参数:sys.argv[1:] 是选中文件路径列表(argv[0] 是临时脚本路径本身,跳过)。
import sys
for path in sys.argv[1:]:
print(f"处理: {path}")
用 Homebrew / pyenv / conda 的 Python 怎么办?
默认 /usr/bin/python3 没有第三方库(如 requests、numpy)。两种办法:
- 填完整路径作为脚本类型:
/opt/homebrew/bin/python3或/Users/你/.pyenv/shims/python(推荐) - 填
/usr/bin/env python3作为脚本类型:让 PATH 里第一个匹配的 python3 生效——iRightMenu 默认 PATH 已包含 Homebrew 路径
4. AppleScript / JavaScript(osascript)
脚本类型分别填 applescript 或 javascript。底层走 /usr/bin/osascript(JS 加 -l JavaScript)。不会自动加 shebang。
重要差异:osascript 不像 bash 那样自动暴露 $1 $2。要在脚本里拿到选中文件路径,必须用专门的入口:
AppleScript:on run argv
on run argv
repeat with filePath in argv
log "处理: " & filePath
end repeat
end run
控制其他 App 的标准模板:
on run argv
set firstFile to item 1 of argv
tell application "BBEdit"
activate
open POSIX file firstFile
end tell
end run
JavaScript(JXA):function run(argv)
function run(argv) {
for (let path of argv) {
console.log("处理: " + path);
}
}
注意 console.log 输出到 stderr,要在”结果对话框”里显示就用 return value。
5. 选中文件 / 参数
基本规则
- 参数 = 选中文件的绝对路径
- 文件和文件夹都按相同方式传入,脚本不区分类型
- 工作目录 = Finder 当前所在目录(即菜单点击时所在的窗口目录)
路径含空格
macOS 用户目录有大量含空格路径(Application Support / Group Containers / 中文目录名等)。所有变量都要双引号包:
# ❌ 空格会被 shell 拆成多个参数
cp $1 /tmp/
# ✅ 正确
cp "$1" /tmp/
状态栏菜单:没有参数
放在状态栏的脚本菜单无选中文件,$@ 是空的、$# 是 0。可以用这个判断”是从状态栏触发”:
if [ $# -eq 0 ]; then
# 状态栏触发——通常做"全局"动作
open -a "Calculator"
fi
6. 环境变量与 PATH
默认 PATH
脚本拿到的 PATH 已经追加了 Homebrew 等常见路径:
/usr/local/bin
/opt/homebrew/bin
/opt/homebrew/sbin
/usr/local/opt/ruby/bin
/opt/local/bin
$HOME/.local/bin
意味着 Homebrew 装的命令(brew、code、gh、fzf 等)直接可用。
不继承 ~/.zshrc 的自定义
脚本运行的是非交互式 shell,不会读 ~/.zshrc / ~/.bash_profile。所以:
# ~/.zshrc 里写了:
# alias ll='ls -lh'
# export MY_TOOL_HOME="/Users/h4ck/tools"
# 脚本里:
ll # ❌ command not found
echo $MY_TOOL_HOME # ❌ 输出空字符串
解决方法(按推荐度):
- 直接用完整命令(脚本里写
ls -lh替代ll) - 在 Inspector 填”自定义 PATH”,让额外目录里的二进制能找到
- 偏好设置 → 高级 → Shell 环境,写全局
KEY=VALUE行(每行一个),所有脚本继承
调试:看脚本能拿到什么
不知道环境长啥样?跑这段,”反馈方式”设”结果对话框”看输出:
echo "=== PATH ==="
echo "$PATH" | tr ':' '\n'
echo "=== 全部环境变量 ==="
printenv | sort
echo "=== 工作目录 ==="
pwd
echo "=== 参数 ==="
echo "$#: $@"
7. 输出与反馈
“反馈方式”决定脚本运行成功时用户看到什么:
| 反馈方式 | 行为 |
|---|---|
| 无 | 静默执行——成功无反馈 |
| 通知 | 系统通知”执行完成”——不显示 stdout 内容 |
| 结果对话框 | 弹对话框,显示 stdout + stderr 合并的输出(输出过长会截断) |
失败时的强制反馈
脚本退出码非零(exit 1 / 命令失败 / 语法错),无论反馈方式怎么设,都会弹错误对话框(含”查看日志”按钮)。这是兜底机制——避免静默失败。
怎么选反馈方式
- 无:批量操作没必要骚扰用户(如批量改权限)
- 通知:操作成功有意义但不需要细节(如复制 N 个路径成功)
- 结果对话框:用户需要看脚本的具体输出(如
du -sh显示大小、md5看哈希值)
在脚本里访问剪贴板 / 控制其他 App
脚本可以正常用 pbcopy / pbpaste、osascript、open -a 等需要”用户登录会话”的命令,剪贴板写入会立即生效,能被剪贴板管理器(Paste / Maccy 等)正确捕获。
# 复制路径到剪贴板(shell-quoted 格式,可直接粘到 Terminal 用)
printf '%q ' "$@" | pbcopy
osascript -e "display notification \"已复制 $# 个路径\" with title \"iRightMenu\""
8. 文件匹配规则
匹配规则决定”菜单在哪些选中项上显示”。设置错了会出现”右键看不到菜单”的常见 bug。
文件匹配类型
| 类型 | 规则 |
|---|---|
| 无 | 不限扩展名,所有文件都显示 |
| 扩展名 | 列表里的扩展名才显示(如 ["py", "js"]),大小写不敏感 |
| 正则 | 用正则匹配文件名(不含路径),大小写敏感 |
文件夹匹配类型
只支持 无 / 正则(没有”扩展名”——不过 .app 是扩展名 + 文件夹的组合,正则用 \.app$ 即可)。
仅可执行文件
勾上”仅可执行文件”,菜单只在文件具有 +x 权限时显示。注意是权限位检查,不分辨二进制类型——chmod +x somefile.txt 也算可执行。
数量限制
最少文件数/最多文件数都默认 0 = 无限制- 设 1⁄1 = “只在选中单个文件时显示”
- 先按类型过滤,再算数量
文件 vs 文件夹的组合
| 显示在文件 | 显示在文件夹 | 行为 |
|---|---|---|
| ✅ | ❌ | 只在选中”全是文件”时显示 |
| ❌ | ✅ | 只在选中”全是文件夹”时显示 |
| ✅ | ✅ | 文件 / 文件夹混选都显示 |
| ❌ | ❌ | 通用菜单:匹配所有选中项;或在 Finder 空白处右键时显示 |
最后一行有特殊语义——既不限文件也不限文件夹的菜单同时会出现在”空白处右键”的菜单里。
9. 管理员权限脚本
把”执行权限”设为 root,脚本会以管理员身份运行。首次使用时系统弹一次授权对话框,后续不再弹。
限制(重要)
- 不能弹对话框——管理员权限脚本无 GUI 上下文
- 不能控制其他 App(osascript / open -a 都不行)
- 不能写剪贴板(pbcopy 在管理员会话里写到的不是你看到的剪贴板)
- 不读用户登录态——你的 ssh-agent / keychain unlock 状态不可用
- 没有超时机制——脚本卡住会一直卡
适合的场景
- 修改系统配置文件(
/etc/hosts、/Library/...等) - 操作 SIP 之外的系统目录
- 跑需要管理员的诊断命令(
tcpdump、dtrace)
不适合的场景
- 任何 GUI 操作(弹窗 / 控制 App / 写剪贴板)
- 用户级配置(
git config --global/defaults读用户偏好) - 需要”看用户当前界面”的操作
如果脚本既要管理员又要 GUI,拆成两个菜单——管理员菜单做系统操作,普通菜单做 UI 反馈。
10. 调试与日志
每个脚本独立的日志文件
每次脚本执行后,元信息和输出会写到日志文件——iRightMenu Pro → 偏好设置 → 高级 → “打开日志目录”,下面的 Scripts/<菜单标题>.log 就是完整记录。
每段记录含:
- 时间戳
- 脚本类型
- 退出码
- 工作目录
- 选中文件列表
- stdout + stderr 输出
调试三步法
- 先在 Terminal 跑通——把要做的事在 Terminal 里手动执行一次,确认逻辑对
- 粘贴到菜单——把跑通的代码粘贴成菜单脚本,反馈方式设”结果对话框”,再跑一次
- 看日志——如果还是不对,看
Scripts/<菜单标题>.log里的完整输出 + 环境变量 + 退出码
常见症状 → 排查方向
| 症状 | 常见原因 |
|---|---|
| 右键看不到菜单 | 文件匹配规则不对(扩展名 / 数量 / 文件 vs 文件夹) |
command not found |
默认 PATH 不含该命令所在目录——填”自定义 PATH”或用绝对路径 |
| 路径解析错误 | 变量没用双引号包,含空格的路径被拆 |
| 输出空 / 没反应 | “反馈方式”设为”无”——临时改”结果对话框”看输出 |
| AppleScript / JS 拿不到参数 | 没用 on run argv / function run(argv) 入口(见 §4) |
11. 进阶菜谱
下面这些是详细版的进阶例子。简单的菜谱(VSCode 打开 / 总大小 / md5 等)见 基础页。
复制 file:// URL(含 URL 编码)
把路径转成 file:// URL,方便贴到浏览器或 Markdown:
import sys
import urllib.parse
import subprocess
urls = ["file://" + urllib.parse.quote(p) for p in sys.argv[1:]]
text = "\n".join(urls)
subprocess.run(["pbcopy"], input=text, text=True)
print(f"已复制 {len(urls)} 个 file:// URL")
AppleScript:用 BBEdit 打开多个文件
on run argv
tell application "BBEdit"
activate
repeat with p in argv
open POSIX file p
end repeat
end tell
end run
Python:批量加日期前缀
import sys
import os
import datetime
prefix = datetime.date.today().isoformat() + "_"
for path in sys.argv[1:]:
dir_, name = os.path.split(path)
if name.startswith(prefix):
continue # 已加过前缀,跳过
os.rename(path, os.path.join(dir_, prefix + name))
print(f"已重命名 {len(sys.argv) - 1} 个文件")
状态栏快速:在 Finder 当前位置开 Terminal
放状态栏菜单(无文件参数):
tell application "Terminal"
activate
do script "cd " & quoted form of (system attribute "PWD")
end tell
12. 何时该写 Lua 插件
脚本到达以下”临界点”时,该考虑改成 Lua 插件:
- 需要 UI 表单:让用户选证书 / 选选项 / 输入配置——脚本撑不了,Lua 插件有完整表单 API
- 需要持久化状态:跨菜单调用记住设置——脚本只能写文件,插件有 keychain / sqlite API
- 跨菜单复用:同一段逻辑被多个菜单触发——脚本要复制粘贴,插件可一份代码导出多个菜单项
- 想分发给别人:脚本只能复制粘贴文本,插件可以上传到插件商店,他人一键安装