按:本文记录了作者在没有过多终端脚本基础的情况下,通过「咨询」ChatGPT 从无到有实现自动修复微信文件权限的过程。如果你对终端环境比较熟悉,文中提到的步骤和方法可能有些基础,解决方案也不一定是最优的。但我们认为这个探索过程是很有意义的,也体现了生成式 AI 与自动化结合的应用场景,值得推荐。如果你对文中提到的 launchd 的原理及其用例有进一步兴趣,可以阅读我们此前的一篇文章

如果你需要本文方案的最简化、一次性实现,可以在终端运行:

find "$HOME/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat" -type f -path '*/*/Message/MessageTemp/*/File/*' -exec chmod 644 {} \;

引言

不知道从什么时候开始,在 macOS 上微信收到的文件都变成了只读,每次打开新文件,都需要另存一份。对于每天都需要用微信收发大量 Word 文档的我,这个改动非常折磨人。当然,最好的办法是不用微信,但这显然不是打工人可以决定的。

看到这行字就难受

好在,经过一些研究,我发现这个问题还是能够简单解决的,这里也将探索的过程分享给大家。需要说明的是,我不是相关方面的专业人士,本文所展示的几种方法未必是最合适的解决方案,特别是相关脚本是否存在风险还请自行判断。

一、自动操作

此前,我曾经在网上学过一个将 Word 文档保存为 PDF 文件的快速操作(Quick Action)指令,用途是批量将 Word 转换为 PDF 文档。因此,这次我首先也想到通过 macOS 系统里自带的自动操作(Automator)来实现这个目标。

修改 Word 文档等文件的权限,至少有三种方法。第一种是打开文档然后复制或另存文件;第二种是对文档右键选择「显示简介」,拉至下方将用户的权限修改为读和写(Read & Write)。

第三种方法看起来更像是适合批量操作的,用到的是 chmod 命令,具体而言,打开终端(Terminal.app),输入

chmod 644 /path/to/file

 即可赋予用户读写的权限。

其中,644 三个数字分别对应用户(所有者)、用户组和其他(所有人)的权限。4 代表读的权限,2 代表写的权限,1 代表执行权限。用二进制写出来的话,就不难想象 6(二进制 110)可以表示读(4,二进制 100)加上写(2,二进制 010)的权限。至于文件路径,把需要的文件拖进终端就可以,注意 chmod、644 与文件路径之间均有空格。

图片来自网络

知道这个命令以后,我开始写快捷操作。一开始,我按照之前 Word 文档转 PDF 文件的经验使用了 AppleScript,告诉(tell)终端(Terminal.app)去运行这行 chmod 命令。问题是,不仅运行时候会有个终端弹出,运行结束后终端也不会关闭,一直保留在 dock 栏上,即使按照网上的说法,使用 exit 命令加上修改 profiles 也不能完全解决。

奇怪于网络上为什么几乎没有人遇到相似问题,我才在检索中发现,根本没有必要使用 AppleScript 打开终端,在自动操作中运行一个 Shell 脚本就可以了。完整的操作过程如下:

  • 打开 macOS 自带的应用程序「自动操作(Automator)」,在弹出的新建窗口中双击选择「快速操作(Quick Action)」。
  • 在左侧的工具中找到,或者直接搜索「Shell」找到「运行 Shell 脚本(Run Shell Script)」并双击或将其拖至右侧窗口。
  • 在右侧窗口设置为收到「文件或文件夹(files or folders)」,位于「访达(Finder.app)」,下方传递输入修改为「作为自变量(as arguments)」,然后在下方自动生成的模板中,将 echo "$f" 这一行替换为 chmod 644 "$f" 就可以了。整体如下图所示:
Shell 中引用变量需要加 $,就写下面那点东西的时间里我可能就忘了 50 次吧
  • 最后,保存并命名(比如:「修改权限」)。此外,也可以在上面的设置中修改图标和颜色。之后,在访达中,对任意文件右键选择「快速操作(Quick Action)」,点击刚才添加的「修改权限」操作,就能够立即将文件权限变更为读和写。

到这一步,问题得到了初步解决,在每次保存微信文件后,我只需要通过右键点击目标文件,再从快捷操作中选择上面创建的内容,就可以让随后打开的文档不再是「只读」。只不过,这样仍然有点麻烦,特别是忘记这一操作径直打开文档时,仍然有不通畅的感觉。

二、监控文件变动:认识 fswatch 工具

我的想法是,当我将微信中的文件保存到某个文件夹,如果这个文件夹里发生的变化能够被监测到,而发生变动后系统就自动运行一次上面的授权脚本,那么就不需要我手动点击快速操作按钮。

从这一步开始,我想到了 ChatGPT。过去,我拿 ChatGPT(GPT3.5 版本)测试回答工作中遇到的法律问题,得到的结果并不令人满意。但考虑到 ChatGPT 在分析代码方面的能力,我非常期待它在帮助我这样外行人时的效果。结果是,ChatGPT 的表现令我感到惊喜,在很多时候比搜索得到的结果更贴合我的问题,也更详细和耐心。

不过,在部分问题中,ChatGPT 仍然会犯错,需要自行甄别或通过网络去检索更多和更新(尤其是在 ChatGPT 数据集截止日期以后)的解决方法。

(一)fswatch 工具的安装

我询问 ChatGPT 的第一个问题是在 macOS 中是否有监控文件夹内文件发生变动就运行脚本的方法,ChatGPT 告诉我 fswatch 工具可以实现这个目标。

ChatGPT 告诉我 fswatch 需要通过 Homebrew 安装,fswatch 的 GitHub 页面 也给出了其他安装方法。在安装 Homebrew(下载方式见官网链接)后,在终端中输入 brew install fswatch 即可安装 fswatch。

只是,在对话过程中,ChatGPT 偶尔会忘记我在使用 macOS 这一前提,比如给我推荐 inotifywait 工具,在我尝试失败并检索后才发现,这是一个 Linux 上的命令。

欺骗了我的一些感情和一点时间

(二)选择被监控的文件范围

在脚本中,只需要输入 fswatch 文件路径 就可以监控指定文件夹,同时,也可以添加一些更细化的参数以选择监控文件的范围。在这方面问题上,可能由于 fswatch 自身的版本更新,ChatGPT 有时候能够迅速解决了我的问题,但有时候也会犯错导致我走弯路。

比如,我希望只在添加文件时触发事件,ChatGPT 明确告诉我可以使用 --event Create 参数来实现。

但在我想要使用 -i 参数将添加的文件类型限制于 .docx 等文档时,ChatGPT 就明显弄反了 -i(include,包含)和 -e(exclude,排除)两个参数。

这个回答有不止一处错误

考虑到授权陌生文件可能带来的风险,如何只监控特定文件恰恰是我比较关注的事项(当然,都监控然后加一个文件名判断也不是难事)。ChatGPT 在这个问题上没有给我什么帮助。这里需要特别记录一下的是,fswatch 命令中,只写 -i 并不意味着可以只监控符合规则的文件,必须首先写上 -e 并排除一切文件,再写一个 -i 包含需要的文件,才能够实现监控符合条件文件的目标。

上图的另一个错误在于,fswatch 使用的匹配规则并非通配符,而是正则表达式(regex),当然这也可能是由于 ChatGPT 使用过时资料库导致的。在正则表达式中,匹配任意单字符的不是 * 而是 .,任意数量字符则是 .*。在学习和测试后,我将匹配部分写成如下形式:

# 文件类型,可随意修改添加
types=("doc" "docx" "ppt" "pptx" "xls" "xlsx")
# 生成正则表达式
re=""
for i in ${types[*]};do re="$re|$i$";done
re=${re:1}
# 监控满足条件的文件变动
fswatch -e ".*" -EIi $re --event Created $path

其中,re 经过循环后生成的值为 doc$|docx$|ppt$|pptx$|xls$|xlsx$,表示匹配以 doc、docx 等结尾的文件名。顺便一提,Shell 在赋值的时候,= 左右侧不能加空格,这写起来也太难受了。

fswatch 的参数中,-e ".*" 表示排除所有(.*)文件。 -EIi $re 表示包含上述文件类型的文件,E(extended)和 I(insensitive)的含义分别为使用扩展的正则表达式(否则无法使用或逻辑 |),以及不区分大小写。$path 则是目标文件夹,我事先定义了文件路径。

(三)对监控的文件做出修改

监控文件发生变化后,在如何修改变动文件权限的问题上,我一开始又使用了笨方法。我询问 ChatGPT 如何找到最后添加到文件,它在两次提问中分别给了我使用 ls 命令和 find 命令的方法,但我测试后发现这似乎找到的是最后修改文件,而非最后添加文件,这与我的需求不太符合。于是我使用了 find $path -type f -perm 444 来查找整个文件夹内权限为「444」(只读)的文件并批量进行修改。

但当我返回查看 ChatGPT 最初给我的示例时,我意识到我可能把事情想复杂了。

这个例子帮了大忙

虽然暂时不知道 |  的具体作用,但上面这行示例代码的意思显然是,fswatch 发现变动后,通过 while 循环和 echo 命令显示变动的文件,这看起来很符合我的要求。为此,我认真学习了管道 | 的用法……才怪咧,并没有那么多时间。显然,这里我只需要把 echo $file 修改为 chmod 644 $file 就可以了。

read file 的过程中遇到了一个小问题,就是读取的文件名中如果有空格那么就会截断为两个变量。通过检索,将环境变量 IFS 修改为 $'\n'(换行)就能够让 read 变为逐行读取,解决这个问题。

整个脚本的代码展示如下:

# ChatGPT 说,这是一个称为 shebang 的特殊注释,用于指定脚本解释器的路径
# 如果该注释不存在或指定的路径不正确,则操作系统将尝试使用默认的解释器来执行脚本
#!/bin/bash

# 自定义路径,这里以 download 文件夹为例
# 实际上应该也可以直接修改为微信的缓存文件夹(可以通过右键点击微信内接收到的文件查看路径)
path="/Users/用户名/Downloads"

# 生成正则表达式,用于匹配文件
types=("doc" "docx" "ppt" "pptx" "xls" "xlsx")
re=""
for i in ${types[*]};do re="$re|$i$";done
re=${re:1}

# 修改环境变量,让后面的 read 能够逐行读取
IFS=$'\n'

# 监控添加文件的事件,将变动文件传递至下面的 chmod 命令
fswatch -e ".*" -EIi $re --event Created $path |
while read file;do
# 额外增加一个文件存在和不可写的判断,排除打开 Word 文档时产生的临时文件,避免报错
	if [ -f $file ] && ! [ -w $file ];then
		chmod 644 $file
	fi
done

最后,将文件保存为 wxpermfix.sh(名称可以自己选择,保存路径按照惯例,可以放在自己主目录下的 bin 目录,即 ~/bin );并在终端里用 chmod 命令赋予其执行权限:chmod +x ~/bin/wxpermfix.sh

然后,就可以将它拖入终端中回车运行。

三、添加启动项文件:认识 launchd

问题和麻烦从此刻开始不断涌现。

在写完以上脚本后,我打算让脚本自己跑起来,并在开机自动启动,ChatGPT 推荐使用 launchd 工具。

注意修改 Label 和 ProgramArguments 下 <string> 和 </string> 包围的内容即可

可在我模仿上面的文件修改完毕(实际上只需要复制文件夹内随便一个其他文件就可以)后,出现了各种问题,包括但不限于:

  • 以为需要反复重启/登出用户来测试效果——实际上只需要 launchctl unload 卸载 plist 文件后 launchctl load 重新加载文件即可。
  • 加载时告诉我没有足够的权限——修改脚本和 plist 文件的权限,比较奇怪的是,似乎 plist 文件权限修改为 744 是不可以的,必须得 644
  • 放在什么位置——在 ChatGPT 提到的两个目录中,一开始我放在了 /Library/LaunchDaemons,但那是系统级别的守护进程所在位置,放在那里会提示权限不足,实际上应该放在 ~/Library/LaunchAgent,以当前用户身份运行就行了。
  • 通过 launchd 启动不生效,但在终端中启动就可以——原因是 launchd 启动的进程的环境变量 PATH 与交互式终端下不同,无法找到 fswatch,解决方式是找到 fswatch 命令的绝对路径,放在脚本里。
我早就把在脚本、plist 文件中使用的目录都设置为绝对路径,所以一直没有理解 ChatGPT 发出的有关要使用绝对路径的提示,直到这里我才意识到是 fswatch 要使用绝对路径
  • chmod 报错权限不足——一开始我听信了 ChatGPT 的说法,以为需要在脚本中加上 sudo,可那就意味着每次都需要输入密码,这显然是不符合我自动运行脚本需求的。

好在我最后在一个论坛里发现了解决方法,通过赋予 bash 完全磁盘访问权限(full disk access),可以让通过 launchctl 运行的脚本中的 chmod 命令有权限修改其他文件的权限。具体步骤还是可以问到:

至此,我终于有了一个可以自动启动的,不断监控目标文件夹的脚本。让我在微信自己解决这个问题以前,免受只读文件之苦。

如后文所述,因为不太想设置自动启动,我自己最后没有用这套方案。但如果有人想要用这套方案,请参考下列步骤,并请特别注意各处权限设置,否则会无法生效:

  • 按照上面所示步骤赋予 bash 完全访问磁盘权限。
  • 进入 ~/Library/LaunchAgents 文件夹,创建一个内容如后附的 wxpermfix.plist 文件(除扩展名外名称可以自定)。打开后参照上面 ChatGPT 给出的示例,删去不需要的部分,修改 Label ProgramArguments 字段中 <string> </string> 包围的内容:前者就是一个单纯用于区分的名称(理论上可以随便填写,但惯例是采用反向 DNS 方式命名);后者则修改为上面  wxpermfix.sh 脚本的路径(注意把 [USERNAME] 部分换成自己的用户名)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"[]>
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.example.wxpermfix</string>
        <key>ProgramArguments</key>
        <array>
            <string>/Users/[USERNAME]/bin/wxpermfix.sh</string>
        </array>
        <key>RunAtLoad</key>
        <true />
    </dict>
</plist>
  • 打开终端(Terminal.app),输入 launchctl load -w ~/Library/LaunchAgents/wxpermfix.plist 后回车,其中 wxpermfix.plist 的路径也可以不用手动输入,从 Finder 将其图标拖入终端窗口即可。
  • 如果这么做没有生效,那么参照上面 ChatGPT 给出的方案,将 wxpermfix.sh 脚本中的 fswatch,替换为运行 which fswatch 命令找到的绝对路径,并重新加载一遍 LaunchAgent:
launchctl unload ~/Library/LaunchAgents/wxpermfix.plist
launchctl load ~/Library/LaunchAgents/wxpermfix.plist

替代方案:不自动启动也不是不可以

在上面询问 ChatGPT 如何赋予 bash 完全磁盘访问权限时,ChatGPT 提到了「存在一定的安全风险」。也许有的读者会介意这里的「安全风险」,因此我也就不推荐所有人采用上面的 launchd 方案。

我回想了一下平时的电脑使用习惯,我平时以待机为主,不经常关机,开机自动启动脚本也不是非要不可,只要启动后可以安静地自动运行就够了。所以,我又开始探索是否可以免去自动启动的要求。

在这里,我又走了一点弯路,我回到了自动操作(Automator),试图通过打包一个应用程序(Application)来实现目标,但运行时显示在上方状态栏的齿轮符号让我打消了这个念头。

于是,我问了 ChatGPT 最后一个问题,它的回答也一如既往的简洁和清楚。

当然,我自己是记不住 nohup 命令后面那几个参数的,所以我又写了一个新的 .sh 文件存放这条启动脚本的命令。等开机后,就将这个文件通过终端运行一次,然后就可以退出终端,让修改权限脚本保持运行。与之对应的是,我也写了一条关掉脚本的指令备用,存在同一个文件夹的另一个 .sh 脚本中。

这两个文件分别为,启动:

#!/bin/bash
# 启动 xxx.sh,替换为正确路径即可
nohup xxx.sh >/dev/null 2>&1 &

关闭:

#!/bin/bash
# 关闭 xxx.sh,替换为正确文件名即可
pkill -f xxx.sh

题外话

在此之前,我从来没有写过 Shell 脚本,否则也不会在一开始干出在自动操作中 tell "Terminal.app" 的事情。虽然学习过程中也遇到了不少问题,但整体上还是比较愉悦和流畅的。一个原因可能在于,对于从零开始的学习者而言,ChatGPT 在每个问题上都提供了详尽有用的解释和非常具体的示例,非常直观和有效。

当然,ChatGPT 好用也不意味着可以对其无条件信任,在解决问题时,查看文档和搜索引擎仍然是有必要的。并且,比起高度抽象地描述问题让它给出完整方案,一步步提问可能对学习和检验都更有帮助。

一点额外的感想是,就像我在 ChatGPT 中提问自身专业知识时,很容易看出它从什么时候开始胡编乱造,相信计算机专业人士在使用 ChatGPT 中也能更高效地甄别信息的真伪,更直接地去利用那些更有价值的东西,扬弃错误和冗余的部分。我想,这某种程度上或许也说明了专业人士并不那么容易因 AI 发展而被替代,反而是比起一般人在利用 AI 解决相关问题时更有优势吧。

以下内容于 2023/04/19 10:26更新
更新 LaunchAgent 示例

应读者要求,我们请作者补充了用于实现开机自启的 LaunchAgent 文件,附于相关小节之后,请按照文中说明参考使用。