如果你经常在 macOS 上处理重复性的窗口整理、应用切换、点击操作,大概率听过“ax”这个说法——它不是什么高深框架,也不是某个大厂新出的工具,而是我在终端里压了三个字母的别名,底层就是把osascript这个 macOS 自带命令包了一层。今天想聊的,就是围绕“ax”展开的一套应用调度玩法:早上进办公室一键铺开浏览器、编辑器、终端、通讯工具,下班前一键把没用的窗口收掉,全程不需要鼠标点一下。说白了,这就是“ax调度”在日常工作中的真实形态,它解决的核心问题是:不要再把时间花在重复点开同一个应用、挪同一个窗口这种破事上。
这套东西适合谁参考?如果你会一点点终端操作、每天要和十几个 App 打交道、又觉得手动管理窗口很烦,那下文的内容基本就是给你写的。不需要你有 AppleScript 基础,我会从最底层的原理讲起,再给完整可抄的方案。我尽量把话说得直白一点,能跑就行。
1. ax到底是什么,为什么值得为它写篇文章
1.1 三个字母背后的真实构成
先拆一下。ax不是一个官方命令,它在系统里默认不存在,而是你在 shell 配置文件里自己定义出来的快捷指令。最常见的定义长这样:
alias ax="osascript -e"这行配置写到~/.zshrc(如果你还在用 bash,就是~/.bash_profile),然后source一下,你的终端里就多了一个叫ax的命令。它的工作原理等同于执行osascript -e,也就是把后面跟着的那串参数当作 AppleScript 代码,交给 macOS 的脚本引擎去运行。
我见过有人把ax解释成“AppleScript 执行器”,也有人直接叫它“自动化执行缩写”,其实名字无所谓,关键是它承担的真正任务:用一行终端命令,去控制 macOS 上任何支持脚本化的应用行为。你在界面上能做的“打开浏览器”“把窗口移到左边”“点击某个按钮”,绝大多数都能通过 AppleScript 描述出来,然后让ax替你执行。
为什么单独给它起个这么短的名字?纯粹是实用主义。osascript -e这串字符虽然不算长,但每天要敲几十次,手指肌肉会抗议。我自己的习惯是连 AppleScript 里的转义都尽量简化,最终希望达到的效果是:在终端里输入ax 'tell app "Safari" to quit'就能把 Safari 退出。这个手感一旦习惯了,就再也不想去碰 Dock 上那个右键菜单了。
1.2 为什么用它做应用调度,而不是直接写脚本
你可能会问:AppleScript 本身就可以写成.scpt文件或.applescript文本,双击运行不是更省事?为什么非要走终端?
我的答案很直接:调度需要参数化、组合化和自动化,双击脚本做不到这三件事的丝滑配合,但终端能。
举个例子。同样是“打开浏览器并调整窗口位置”,写死在一个.scpt文件里,每次运行的都是一样的参数,窗口位置写死在代码里,换台电脑或换个屏幕分辨率就得改代码。但如果用ax在终端里执行,你可以结合 shell 脚本的变量、循环、条件判断来动态生成 AppleScript,比如先查询当前屏幕分辨率,再计算窗口位置,然后把这个参数拼进 AppleScript 字符串里执行。
更重要的是,终端命令可以被其他工具调度。你可以把某条ax命令丢进cron定时任务,让它在每个工作日的 09:30 自动执行;也可以把它写进一个名为start_work.sh的脚本里,和cd、git pull、npm run dev等其他命令混在一起,组成一条完整的开发工作流启动链。这种“命令行全局调度”的能力,是双击脚本那种交互方式做不到的。
从安全性角度看,ax其实暴露的是 macOS 官方支持的自动化接口,底层调用的是System Events辅助功能框架。相比那些通过模拟鼠标移动、发送硬件键盘事件的第三方工具,AppleScript 的语义更明确、更可控,出问题时也好排查。它对系统资源的消耗也几乎可以忽略不计,打开活动监视器你都看不到它的存在,因为执行窗口只有那一瞬。
当然,有利就有弊。ax再方便,也绕不开 macOS 的权限模型,首次跑某些 UI 自动化命令时,系统会在“系统设置 → 隐私与安全性 → 辅助功能”里弹窗让你给终端授权。这个后面我会单独讲,不授权的话,ax命令会报各种奇怪的错误,光凭报错信息很难定位。
2. 核心调度能力拆解:窗口、App与UI交互
2.1 应用级控制:打开、激活与退出的几种写法
用ax做应用调度,最基础的一层就是“控制应用本身”。这里要和命令行里直接敲open -a区分开——open -a只能打开应用,没法再做后续控制;而ax打开一个应用后,还能继续让它激活、退出、甚至等它加载完再执行下一步。
先是打开 Safari 并激活到前台:
tell application "Safari" activate end tell这段代码用ax封装后长这样:
ax 'tell application "Safari" to activate'tell后面的名字是应用的“脚本化名称”,一般情况下和显示名称一致,但也不完全是。比如 Google Chrome 在 AppleScript 里就叫 "Google Chrome",而一些国产套壳浏览器可能会注册成别的名字,最好用下面这条命令先看一眼到底注册了什么:
ax 'tell application "System Events" to get name of every application process'跑完之后你会看到一个进程名列表,里面有 "Safari"、"Google Chrome"、"Terminal" 等等。注意,这里的名称可能带应用后缀,也可能不带,匹配的时候需要注意。我自己实测下来,tell application "Terminal"几乎总是有效,但如果你想控制的是 Electron 套壳应用,比如 Visual Studio Code,脚本名其实还是 "Visual Studio Code" 而不是 "Code",虽然 Dock 上显示的名字是后者,这是最容易踩坑的地方。
退出应用也很简单,直接把activate换成quit:
ax 'tell application "Safari" to quit'有些应用会拦截退出指令,弹窗问你是否保存,这种情况下 AppleScript 的quit可能会被忽略掉。遇到这种顽固分子,可以用下面的“暴力退出”:
ax 'tell application "System Events" to set visible of process "Safari" to false'注意这行并没有真正退出 Safari,而是把它的窗口全部隐藏了,视觉上等同于退出,效果类似 Cmd+H。“真正退出”和“隐藏掉”这两个动作,在日常调度里都有用,你要根据场景选:午休锁屏前只是不想看到窗口,那用隐藏就够了;开会前要释放内存,那还是用quit。
2.2 窗口排版:拿到坐标,把屏幕分给不同应用
如果只停留在“打开/退出”这一层,那ax还不值得这么大张旗鼓地讲。它真正厉害的地方在于窗口级控制,也就是“ax调度”里最常被提到的窗口管理能力。
不管是原生应用还是套壳应用,窗口本质上是有一个坐标位置和一套尺寸的。AppleScript 里可以读取和设置这些数值,前提是这个应用允许脚本访问窗口属性。查窗口信息的写法:
tell application "System Events" tell process "Safari" get position of window 1 end tell end tell用ax封装后就是这样:
ax 'tell application "System Events" to tell process "Safari" to get position of window 1'执行结果会返回类似{0, 23}的坐标,对应窗口左上角在屏幕上的 x、y 值。再把get换成set,你就拥有了窗口的绝对控制权:
ax 'tell application "System Events" to tell process "Safari" to set position of window 1 to {0, 23}'窗口尺寸的读写也是同一个套路,属性名是size。我最常用的一组动作,是把当前屏幕从中间切一刀——左边给浏览器、右边给编辑器。如果屏幕物理分辨率是 2560x1440,那左侧窗口尺寸就是{1280, 1400},坐标{0, 23};右侧窗口的坐标往右挪到{1280, 23},尺寸同样是{1280, 1400}。这里面 y 坐标的 23 不是随手写的,它对应的是 macOS 菜单栏的高度,不同系统版本略有差异,不能填 0,否则窗口的标题栏会被系统菜单栏盖住。
上面是硬编码数字的写法,适合你完全清楚自己屏幕参数的情况。如果脚本要给别人用,更好的做法是先用 System Events 查出主屏幕分辨率,再动态计算。不过我平时自己用,图省事就直接写死,换显示器时再改一次,算是重量驱动和效率驱动的选择问题。你要输出一套通用方案的话,建议用分辨率变量动态算。
2.3 界面元素操作:按钮、菜单和输入框的调度
应用调度走到窗口这层,已经能满足八成需求了。剩下两成,就是更细粒度的“界面元素”操作:点击某个按钮、勾选某个菜单项、往输入框里填充文字。这些虽然听起来偏门,但真实场景里非常高频,尤其是那些快捷键失灵、鼠标又点不中的应用。
举个例子,Safari 的“显示阅读器视图”按钮,并没有默认快捷键,想用脚本控制就得先模拟点击。先确认按钮存在,再执行点击:
tell application "System Events" tell process "Safari" click button "阅读器视图" of toolbar 1 of window 1 end tell end tell注意这里按钮名称不一定是你肉眼看到的文字。系统里的实际名称是本地化之后的 accessibility label,所以如果中文系统的按钮叫“阅读器视图”,英文系统就叫 "Reader View"。最简单的办法是把界面上的所有按钮打印出来,自己对着名字找:
ax 'tell application "System Events" to tell process "Safari" to get name of every button of window 1'输出会是一个名字列表,比如 {"关闭", "最小化", "缩放", "阅读器视图", ...},你要点哪个就直接引号里填哪个。这套 API 对几乎所有系统原生应用和很多第三方应用都有效,但 Electron 应用会有例外,因为它的界面本质上是一张画布,内部的按钮对 System Events 来说往往是“不存在”的,这也是我后来换掉一些套壳应用的原因——不是为了情怀,是为了让调度脚本少写一堆 workaround。
菜单栏点击也是同一个思路。比如你想触发“文件 → 导出为 PDF”,菜单路径是固定的,用 AppleScript 描述出来就是:
ax 'tell application "System Events" to tell process "Preview" to click menu item "导出为 PDF…" of menu "文件" of menu bar item "文件" of menu bar 1'这串代码看着绕,但拆开看就是一层层往下钻:菜单栏 → “文件”菜单 → “导出为 PDF…”菜单项。这里有一个经验点:菜单项的省略号、全角省略号要和系统里完全一致,差一个字符就匹配不上,建议还是先用get name of every menu item of menu "文件"把列表打出来,复制粘贴进去最稳妥。
3. 一套可抄的ax调度方案:早晨一键铺开工作台
3.1 场景设计与脚本结构
原理讲再多,不落到实际场景都是空谈。下面我完整拆一个我每天都在用的方案:早晨开工时,一条命令把工作环境铺开。
先描述一下这个场景的痛点。我的日常工作涉及:浏览器要打开常用的 3 个工作标签页,代码编辑器要打开项目目录,终端要启动一个跑在固定端口的本地服务,聊天工具要登录并保持在线。手动操作这套流程,不算加载等待,光点击和切换窗口就需要半分钟以上,而且经常忘开某一个 Tab。有了ax之后,我把这个流程写成一个名为start_work.sh的脚本,放到~/bin目录下,顺便在.zshrc里给它设一个别名sw。
整套脚本的结构只有三大块:准备阶段、启动阶段、布局阶段。准备阶段负责清理上一个工作周期留下的旧窗口;启动阶段用ax逐个唤起需要的前台应用;布局阶段按屏幕区域把窗口摆放整齐。三个阶段串行执行,哪一块失败都不影响后续命令继续跑,这个容错细节后面会细说。
3.2 关键代码段逻辑拆解
完整脚本太长了,我抽几个核心片段来讲。首先是“启动阶段”里最常用的几个动作,比如激活浏览器并打开目标网址:
ax 'tell application "Safari" to activate' ax 'tell application "Safari" to open location "https://your-dashboard.example"'第一行负责把 Safari 从后台拉到前台,第二行在 Safari 当前页签之外新建一个标签页并打开网址。这里有个细节:open location即使 Safari 已经在显示某个页面,也一定会新开页签,你要是想复用已有空白页,那还得再写分支逻辑。我自己的场景是新开页签更省心,因为我每天固定要开那 3 个网址,直接全开再手动切换也不麻烦。
接着是终端启动本地服务。我把npm run dev这个动作拆开处理:先新建一个终端窗口,把它默认执行的命令设为启动开发服务,再让后续窗口保持原来的 shell 位置。
ax 'tell application "Terminal" do script "cd ~/Work/portal && npm run dev" end tell'不要小看这段do script,它比你自己先去终端输入cd、再手动回车跑命令要快得多。它相当于让 Terminal 里跑了一整套命令。跑起来后,Terminal 会向这个 AppleScript 的执行进程返回一个窗口引用,你可以拿到它,拿到窗口标识后就能为布局阶段锁住这个窗口,而不是搞混到底哪个窗口是开发服务的。
最后布局阶段,上述 3 个主要应用(Safari、编辑器、Terminal)都应该在前台跑起来了,我用 System Events 把它们的位置和大小按预设值写进去:
ax 'tell application "System Events" tell process "Safari" set position of front window to {0, 23} set size of front window to {1280, 1400} end tell tell process "Code" set position of front window to {1280, 23} set size of front window to {1280, 1400} end tell tell process "Terminal" set position of front window to {2560, 23} set size of front window to {1100, 1400} end tell end tell'这段代码假设主屏幕是 3840 宽的外接屏,所以 Safari 占左半边、编辑器占右半边、终端放在最右边延伸出来的一截屏里。如果你的屏幕没这么宽,不要照抄数字,把size的宽度按实际分辨率减半,position的 x 坐标按分界点调整。坐标和尺寸的单位是逻辑像素,不是物理像素,macOS 在 Retina 屏上会自动换算,所以不要被“这数字怎么会这么大”吓住,按系统实际报告的数值来就好。
3.3 从脚本到“一条指令”的落地配置
脚本写完之后,让它变成“一条 ax 指令”才是点睛之笔。在.zshrc里加入:
alias sw="$HOME/bin/start_work.sh"然后终端里输入sw回车,整套工作台就自动铺开。你可能觉得这只是省了半分钟,但人脑的“切换成本”比时间成本更值钱:不用记着要先开什么后开什么,不担心中途被别的事打断而漏掉某一步,这种确定性让每天早晨进入工作状态的心理摩擦小了很多。
把同样思路扩展到下班场景,就是另一条qu命令:隐藏所有聊天工具窗口,把剩余窗口平铺,清空废纸篓,甚至顺手执行一次brew update。这类命令本身并不复杂,但价值在于“稳定重复”,每天固定执行,慢慢就养成了肌肉记忆。
还有一个小技巧:想调试某一条命令时,别直接在完整脚本里改,先单独把那条ax命令复制到终端跑一遍,确认语法和效果都正确,再回脚本里更新。AppleScript 的报错信息对新手很不友好,一旦某个节点出错,整个脚本就可能中断,分步调试能帮你快速定位是哪一行的问题。
4. 排查实录与避坑指南
4.1 高频问题速查表
ax用久了,遇到的问题也就那几类。我整理了一个速查表,后面真撞见了直接对着查,不用翻文档。
| 症状 | 大概率原因 | 解决办法 |
|---|---|---|
命令报了execution error: Not authorized to send Apple events | 终端缺少 Apple Events 授权 | 系统设置 → 隐私与安全性 → 自动化,给“终端”打勾 |
提示Application isn’t running,但应用明明开着 | 应用名写错或用了显示名而不是脚本名 | 用get name of every application process查询真实脚本名 |
| 窗口位置设置无效,命令没报错但窗口不动 | 应用没有启用“辅助功能”权限,System Events 无法控制 UI | 系统设置 → 隐私与安全性 → 辅助功能,勾选终端 |
界面元素点不到,报Can’t get button | 按钮名称和实际 accessibility label 不一致 | 先用get name of every button列出所有名称再精确匹配 |
菜单项点击报menu item not found | 菜单结构层级写错,或菜单名翻译差异 | 逐层用get name of every menu和get name of every menu item查层级 |
| 脚本偶尔成功偶尔失败,没有固定规律 | 上一行命令的操作还没完成就执行了下一行 | 在关键动作之间插入delay 0.5,给 UI 反应时间 |
这几个问题覆盖了我日常维护 ax 脚本时遇到的九成麻烦,剩下的一成基本是系统版本升级带来的行为变化,这类问题没有万能解法,只能靠更新代码适配。
4.2 我踩过的几个隐蔽坑
这里分享几个不在官方文档里写成注意事项、但实战里坑过我的细节。
第一个坑:应用听不懂中文菜单名。中文系统的 Safari 按钮名叫“阅读器视图”,但你用 AppleScript 点击时,有些子系统接受的是英文 accessibility 名称。所以当我从同事那里拿到一份全英文的脚本,他环境里的/toolbar按钮名到我的中文环境就失效了。解决方式是先get name of every button把当前系统下的真实名称打印出来再改代码。没有万能的跨语言脚本,每台机器、每个系统版本都可能有差异。
第二个坑:Electron 应用的窗口控制极其不稳定。像 Slack、VS Code 这类基于 Electron 的应用,绝大部分窗口属性能被 System Events 读到,但一旦你在好几个显示器之间切换,或者应用自己做了 DPI 适配,坐标和尺寸的设定就可能完全失灵。脚本不会报错,就是窗口原地不动。后来我的方案是把 Electron 应用排除在“窗口布局”之外,只保留“激活/退出”级别的调度,反而稳定了。
第三个坑:front window并不总是你肉眼看到最前面的窗口。某些后台应用在 Dock 里还挂着几个隐藏窗口,System Events 认为front window是指“最靠前的可见窗口”,但如果你先执行了activate,又马上查front window,可能拿到的是正在动画切换的窗口,位置还没稳定下来,结果位置设置被发到了一个正在收起的窗口上。应对方法是在activate和后续设置之间加delay 0.3,别小看这 300 毫秒,它能少刷很多有亿点点莫名其妙的 bug。
4.3 稳定性优化心得
最后聊聊稳定性。我最早用ax写自动化脚本时,失败率大概有 20%,原因基本都是时序:前一个应用还没完全启动,activate已经执行完了;窗口动画还没结束,set position就追上去了。后来我总结出一条稳妥的规则:宁可多用delay换取确定性,也不要追求极致的执行速度。
具体操作上,我会在三个关键位置插入延迟:应用启动后等待 0.3 秒,窗口激活动画后等待 0.5 秒,复杂菜单操作后等待 0.5 秒。虽然整体耗时增加了大概两秒,但成功率从 80% 提到了接近 100%。如果你有 10 个窗口需要布局,启动阶段可以并行触发,布局阶段必须串行执行,因为后一个窗口的坐标计算依赖前一个窗口的位置,并行会导致front window指向混乱。
另一个提升稳定性的办法是善用try和end try。AppleScript 中程序执行遇到错误会直接中断,但外层脚本经常希望“某个动作失败也继续往下走”。比如我想关闭所有残留窗口,但某个应用恰好没有窗口,close window 1就会报错,导致后面的代码全部停摆。正确的写法是:
try tell application "Safari" to close window 1 end try套上这层之后,报错会被吞掉,脚本会继续往下执行,这实际上是默认的容错保护。不过要注意,吞掉报错也意味着问题不会暴露,所以你调试阶段还是要把try去掉,跑出错误后再决定要硬性处理还是忽略,等验证完再包回去。
还有一点是关于日志的。调度脚本写到后期会越来越多,我给每条ax调度都加了一行 echo 日志,时间精确到小时分秒,比如:
echo "[$(date +%H:%M:%S)] activating Safari"一旦某个环节出问题,翻日志就能精确知道最后成功执行到哪一行。光凭 AppleScript 的报错去猜位置,那是自虐。
5. 一点个人体会
用ax调度的这一两年,最大的体会是:工具本身并不复杂,macOS 早就有这套自动化接口,只是平时大家都被图形界面惯坏了,懒得去碰。真正让它发挥威力的,是你愿不愿意花一个下午把自己最常做的那套流程固化下来,写成脚本,然后享受每天多出来的那几分钟。
这套方案的边界我摸得很清楚:它适合个体工作流,不适合需要和团队共享的复杂自动化平台;适合语义明确的点击和布局,不适合拖拽、手势这类底层交互;适合自己定制,拿来即用的通用工具反而不如直接说需求来得自然。
如果你也想试试,建议别一上来就写复杂的全流程,先从一条最不起眼的命令开始:每天下班前用ax隐藏当前所有窗口,让桌面恢复干净。坚持一周,你自然会开始琢磨怎么把更多重复步骤交给它。