终端里 rm 一下文件就没了,macOS 怎么删文件才进废纸篓

大家好,我是若风。

前两天在终端里清理一批临时文件,手快敲了个 rm,回车的瞬间意识到删错了一个还需要的配置文件,心里一沉。第一反应是去废纸篓找,打开才发现里面根本没有。那一刻才真真切切想起来,终端的 rm 压根不走废纸篓,删了就是删了,想想还有点崩。

这事挺有意思的。同样是「删文件」,Finder 里删和终端里删,是两套完全不同的世界,一个有后悔药,一个没有。我顺着这个坑往下挖,想把 macOS 删除文件的机制彻底搞清楚,结果一路挖到了 osascript 这个终端和 GUI 之间的桥。今天就把整个过程整理出来。

Finder 里删文件,进废纸篓才是默认

先说 GUI 这套,绝大多数人日常用的。

在 Finder 里选中一个文件按 ⌘+Delete,文件就进了废纸篓。右键菜单的「移到废纸篓」,菜单栏里的「移到废纸篓」,或者直接拖到 Dock 的废纸篓图标,都是一回事。这套行为的底层语义是「移动」,文件只是换了个位置,数据还在磁盘上,随时能放回。

那如果就是想直接删,不进废纸篓呢?按住 ⌥(Option)再操作就行。⌘+⌥+Delete 直接永久删除,会弹个确认框,确认后不经过废纸篓,不可恢复。右键和菜单栏也一样,按住 ⌥ 之后,「移到废纸篓」会变成「立即删除」。

这里有个值得记住的规律。⌥ 键在 macOS 里是「换一种操作」的通用修饰键,按住它几乎到处都会变出更多选项。右键文件会多出「立即删除」,点菜单栏会多出一堆隐藏项,截图时按住它还能切换模式。记住这一个键,macOS 的很多隐藏功能就打开了。

清空废纸篓是 ⌘+⇧+Delete,再按住 ⌥ 就是 ⌘+⇧+⌥+Delete,跳过确认直接清空。说实话这几个组合我老是记混,但其实只要记住「⌘+Delete 是基础,每多一个修饰键就多一层狠」就够了。

终端里的 rm,完全是另一个世界

到了终端,画风突变。

rm 文件,直接从文件系统里抹掉,不进废纸篓,不可恢复。Finder 的「移到废纸篓」是个应用层操作,说到底是在移动文件,而 rm 是系统调用层面的 unlink,把目录项删了,数据块标记为可回收。两者的安全级别完全不同。

你想想看,这就带来一个很现实的问题。我在终端里干活,删文件是高频操作,但我又想要废纸篓那层后悔药,怎么办?

社区里常见的解法是装个 trash 命令,brew install trash,之后用 trash 文件代替 rm,行为和 Finder 的 ⌘+Delete 一致,进了废纸篓能找回。这是个好办法,但要多装一个东西。

我就想,macOS 自己明明有废纸篓机制,终端里难道就没有原生的办法调它吗?花了 2 小时翻 man 页和 10 篇资料,最后挖到了 osascript,那一刻有点释然。

osascript,终端和 GUI 之间的那座桥

先看一句话能不能把文件扔进废纸篓。能。

bash 复制代码
osascript -e 'tell application "Finder" to delete (POSIX file "/path/to/file" as alias)'

跑一下,文件从原位置消失,老老实实出现在废纸篓里。零依赖,系统自带。

这条命令背后发生了什么?osascript 执行了一段 AppleScript,这段脚本告诉 Finder,把这个文件删掉。而 Finder 的 delete 命令在它的脚本字典里,语义就是「移到废纸篓」。也就是说,我们是在终端里,借 osascript 的手,按了一下 Finder 的 ⌘+Delete。

表面上看 osascript 只是个跑 AppleScript 的工具,真正关键是,它是终端和 macOS GUI 世界之间的一座桥。终端里 rm、mv 这些命令只能碰文件系统,碰不到 Finder、Safari、系统通知这些应用层的东西。想让 GUI 应用干活,就得有东西能给它发指令,osascript 干的就是这个。

osascript 的真面目,OSA 架构的命令行入口

既然挖到了,就把它拆透。

osascript 这个名字来自 OSA,Open Scripting Architecture,macOS 的一套脚本架构。osascript 是它的命令行入口,默认执行 AppleScript,也支持 JXA(JavaScript for Automation)。它和 osacompile、osadecompile、osalang 是一家人,分别负责执行、编译、反编译、列语言,都在 /usr/bin/ 下,系统自带。

喂脚本给 osascript 有四种方式。单行用 -e,多行就多写几个 -e,复杂脚本写进 .applescript 文件再 osascript 文件名,或者用管道 echo 脚本喂给 osascript 从 stdin 进去。我在终端里跑临时命令习惯用 -e,稍微复杂点就直接写文件,避免引号转义转晕。

AppleScript 的灵魂就一个字,tell。tell application "Finder" to 加某件事,就是告诉 Finder 去做这件事。多条命令发给同一个应用就用 tell 块包起来。能被 tell 的,是那些实现了 Apple Event 接口的「可脚本化应用」,Finder、Safari、Mail、Music、Terminal 都是,但不是所有应用都支持。

这里有个理解权限的关键。AppleScript 的命令分两类。一类是 Standard Additions,系统自带的标准命令集,像 display notification、display dialog、current date、get volume settings,这些不针对具体应用,不需要 tell,也不弹权限框。另一类是应用专属命令,像 tell application "Finder" to 某事,第一次跑会弹「终端想要控制 Finder」,同意了才行,之后在系统设置里找到「隐私与安全」的「自动化」那一栏管理。所以你写 display notification 发通知,怎么跑都不弹框,但一 tell 某个应用,权限就来了。

osascript 的命令行参数里,有几个常被忽略但很实用。man osascript 写得清楚,我挑几个说。

-l 用来切语言,-l JavaScript 就是跑 JXA,不喜欢 AppleScript 那套英语语法可以用 JS 写,底层都是 Apple Event,能力等价。

-i 是交互模式,敲一句执行一句,相当于一个 AppleScript 的 REPL。调试脚本的时候特别有用,比每次改完文件重跑快得多。

-s 控制输出样式,后面跟一个修饰符字符串。h 是人类可读,这是默认的,s 是可重新编译的源码形式。这俩的区别举个例子你就懂了,列表 {"foo", "bar"} 在 h 模式下显示成 foo, bar,在 s 模式下连花括号和引号都保留,能直接拷回去重新编译。做自动化测试时还有 e 和 o,控制错误输出到 stderr 还是 stdout,方便把脚本错误和其它诊断信息分开。

最让我惊喜的是传参。osascript 可以把命令行参数传进脚本,写法是 on run argv,argv 就是一个字符串列表。脚本里写 on run argv,然后 return "hello, " & item 1 of argv,再 osascript a.scpt world,输出就是 hello, world.。这意味着 osascript 不只是个跑固定脚本的工具,它能接收外部输入,可以包成真正的命令行小工具。

还有个反向的桥很容易被忽略,叫 do shell script。AppleScript 里写 do shell script "ls -la",就能在脚本里调 shell 命令,拿到 stdout。说真的,这条链是双向的,终端能用 osascript 指挥 GUI 应用,AppleScript 也能用 do shell script 回调终端,两个世界互相打通。

光说不练假把式,几个能直接跑的例子

讲了这么多参数,不如直接看几个能跑的。下面这些我都在本机验证过,复制到终端就能用。

先来个最实用的,脚本跑完发个通知。CI 构建完、长任务结束,弹一条系统通知比盯着终端强多了。

bash 复制代码
osascript -e 'display notification "构建完成" with title "CI" sound name "Glass"'

这条不 tell 任何应用,属于 Standard Additions,零权限,怎么跑都不弹框。同一个家族的还有读音量、拿当前日期,也都不需要权限。

bash 复制代码
osascript -e 'return "当前音量 " & (output volume of (get volume settings)) & "%"'
osascript -e 'return current date'

再来看传参。把 on run argv 写进文件,传个参数进去,它就成了一个能接收输入的小工具。

bash 复制代码
# 存成 greet.applescript
cat > greet.applescript <<'EOF'
on run argv
  return "hello, " & item 1 of argv & "."
end run
EOF

# 跑起来,传个参数
osascript greet.applescript world
# => hello, world.

do shell script 这个反向桥也演示一下。AppleScript 里调 shell,拿到 stdout,两个世界就这么互相来回。

bash 复制代码
osascript -e 'do shell script "whoami"'
# => ruofeng

输出样式那个 -s s,光说不如对比。同一个列表,默认 h 模式是 foo, bar,加了 -s s 就是 {"foo", "bar"},连引号和花括号都保留,能直接拷回去重新编译。写自动化测试要解析输出时,这个区别很关键。

bash 复制代码
osascript -e 'return {"foo","bar"}'        # => foo, bar
osascript -s s -e 'return {"foo","bar"}'   # => {"foo", "bar"}

最后来个会触发权限的。让 Safari 打开一个网页,第一次跑系统会弹「终端想要控制 Safari」,同意了才行。

bash 复制代码
osascript -e 'tell application "Safari" to open location "https://obsidian.md"'

同样的套路,tell application "System Events" 能列出当前所有运行中的应用,tell application "Music" to quit 能让音乐 App 退出。一旦理解了 tell 这一个字,剩下的就是查每个应用的脚本字典,看它能听懂哪些命令。

路径处理是最大的坑,人人都会栽

用 osascript 调 Finder 处理文件,几乎人人会栽在路径上。

AppleScript 有两套路径。一套是 POSIX 路径,就是 Unix 风格的 /Users/ruofeng/x.txt,终端里用的。另一套是 HFS 路径,老式 Mac 风格,Macintosh HD:Users:ruofeng:x.txt,用冒号分隔,是 AppleScript 的原生格式。两套混用必报错。

从终端调 Finder,必须把 POSIX 路径转成 Finder 认识的对象,写法是 POSIX file "/路径" as alias。前面那个删文件进废纸篓的命令里就有这一段,少了它 Finder 会说「不理解」。这个套路记住就行,别问怎么知道的,历史包袱。

OSA 家族和 JXA,能用的不止 AppleScript

既然说到这,顺手把家族里的其它成员也提一下。

osacompile 把纯文本脚本编译成 .scpt 二进制,加载更快,也能藏住源码不被直接看到。osadecompile 反过来,把 .scpt 还原成源码。osalang 列出系统装了哪些 OSA 语言,我这台跑出来是 AppleScript、JavaScript 和 Generic Scripting System。

JXA 值得单独说一句。用 -l JavaScript 切过去之后,写的就是标准 JavaScript,Application("Finder") 这样拿到应用对象,能调它的属性和方法。对程序员来说语法亲切得多,还能用上数组的 map 和 filter。但坦白讲,JXA 的社区文档比 AppleScript 少一大截,出了问题搜不到答案,调试器也不如系统自带的「脚本编辑器」顺手。我自己的感受是,简单任务用 JXA 写着舒服,复杂自动化还是老老实实 AppleScript,资料多。

坑和经验,我替你踩过了

写 osascript 这段时间,踩的坑不少,挑几个最疼的说。

引号地狱排第一。bash 里包 AppleScript,双引号要转义成 ",单引号里又不能有单引号,嵌套两层就开始怀疑人生。我的经验是,超过两行的脚本就别硬塞 -e 了,写进 .applescript 文件再 osascript 文件名,干净得多。

权限是第二疼的。第一次 tell 某个应用,系统弹「终端想要控制 X」,你没点同意或者点了拒绝,脚本就静默失败,报个看不懂的错误编号,根本看不出是权限问题。脚本跑不通,第一件事去系统设置里找「隐私与安全」的「自动化」查,比盯着代码看有用。

慢是第三个。每条 tell 都是一次跨进程的 Apple Event 往返,要序列化、要跨进程通信、要等回复。你在循环里 tell Finder 删 100 个文件,能等出喝杯咖啡的时间。正确做法是把操作合并到一个 tell 块里,或者干脆用 shell 的批量操作,别让 Apple Event 一次一次跑。

还有几个小的。Finder 的很多命令要 alias 类型,直接给字符串路径会报「不理解」,记得套 POSIX file 某路径 as alias。display dialog 是阻塞的,会卡住等你点按钮,脚本化和 CI 场景千万别用。不是所有应用都支持脚本化,不支持的你 tell 它,要么报错要么静默无效,别指望它能响应。

总结一下

道理讲了一堆,最后给个能直接用的。把下面这段丢进 ~/.zshrc,终端里就有了 trash 命令,行为和 Finder 的 ⌘+Delete 一致,进了废纸篓能找回,零依赖。

bash 复制代码
trash() {
  for f in "$@"; do
    osascript -e "tell application \"Finder\" to delete (POSIX file \"$(realpath "$f")\" as alias)" >/dev/null
  done
}

realpath 把相对路径转成绝对路径,避免 Finder 找不到文件。多个文件循环处理,每个都走废纸篓。

回到开头那个问题,macOS 怎么删文件才进废纸篓。Finder 里是 ⌘+Delete,终端里原生没有,但 osascript 调 Finder 能做到,包成 trash 函数就和 GUI 一样顺手。更值钱的是,osascript 这座桥能做的事远不止删文件,发通知、控 Safari、调音量、操作任何可脚本化应用,都是同一个套路。

希望这篇文章能救你一次误删,也能帮你省下我花掉的那两个小时。坑我已经替你踩过了。

相关推荐
小妖6661 小时前
React input 输入卡顿问题
前端·javascript·react.js
李明卫杭州2 小时前
Vue2 portal-target 组件与 Vue3 的针对性优化
前端·javascript·vue.js
你怎么知道我是队长3 小时前
JavaScript的函数介绍
开发语言·前端·javascript
weedsfly3 小时前
观察者模式 vs 发布-订阅模式:从概念到实战,一次讲清楚
前端·javascript·面试
咪饭只吃一小碗4 小时前
告别微调成本!用 RAG 本地向量库解决大模型私有领域数据接入
javascript·后端·面试
miss4 小时前
海康威视 IoT 平台前端实战:视频流、海量设备与实时数据可视化
前端·javascript·物联网
mONESY4 小时前
MCP 协议实战博客:跨进程、跨语言可复用 AI 工具标准化方案
javascript
糖墨夕5 小时前
第一章:为什么你要学 AI Agent?—— 从"切图仔"到全栈的进化之路
前端·javascript·vue.js
自律懒人5 小时前
ChatGPT Work 上线当天实测:GPT-5.6 Sol 把 Codex 塞进桌面应用,跨应用自动做报表和 PPT,一个指令干 3 小时的活
javascript·开源