# Automator 手册第五章：用 Shell 脚本强化 Automator

> 本文公开全文。

原文：https://utgd.net/article/20224

在上一章中我们了解了 AppleScript，初步跨入 macOS 自动化中的进阶部分。有了 AppleScript 这座与系统沟通的“桥梁”，Automator 的使用场景变得更加灵活。

- 解压 RAR 格式的文件

- 拼接图片、套壳截图

- 使用正则表达式

- ……

此时，我们就需要转向别的脚本工具。我选用的是 Shell 脚本，它的两大特点吸引了我：

- 拓展性强：拥有大量现成的命令行工具

- 使用简单：可以轻松嵌入 Automator/AppleScript

这一章节中我们都从大量的实例切入，学用 Shell 脚本这一利器。由于内容难度相较前几章有明显提高，建议读者们根据目录选取自己最感兴趣或最需要的部分阅读；同时请先行下载各小节中的 Automator 动作，边使用边阅读，能够更快掌握本章内容。

## 用 Shell 脚本处理文件

Shell 这个名字你可能不常听到，但是“命令行”这个概念多数人都不陌生。即使是小白，平时折腾 macOS 时估计也没少复制别人编写好的命令行来用。

简单来说，命令行就是用文字形式下达给电脑的指令，它能够完成许多图形界面无法做到的操作，这也是我们现在急需的。而单独的脚本，可以粗糙理解为打包、组织过的命令行，容易组织完整的如果、循环等逻辑，可以担任更加复杂的操作，同样是解压文件，它就比较容易实现循环解压

Automator 自带了“运行 Shell 脚本”的模块，和 AppleScript 模块一样可以使用上一步的输入，也能输出结果下一个步骤。但与 AppleScript 模块不同的是，Shell 模块里面没有样板代码，我们需要自己编写。

### Automator 中的 shell

和 AppleScript 类似，我们要通过脚本来获得输入、返回输出。下面是最简单的一个模版，只有一条命令：

```
命令 "$@" 可选参数
```

注意其中的 $@，它代表上一步的结果。比如我把“运行 Shell 脚本”设为 Automator Application 的第一步，那么 $@ 就表示 Automator 接收到的文本或者文件。套用这个模板，解压文件的脚本就是：

```
unzip "$@"
```

也许你已经注意到变量的两边加上了英文直角引号，难道这不会像 AppleScript 一样，把变量名变成纯文本吗？在 shell 里面没有这个担忧，相反，不加反而会出问题。因为 Automator 比较“懒”，偶尔遇到文件名带空格的文件，找东西照旧找到空格为止，后面的路径看也不看，所以不把变量包起来就容易报错。

实际使用中我们经常和带空格文件名的文件打交道——每天产生的截图就通通带空格——所以为了以防万一，可以参考上面的模板，把所有 shell 脚本中的变量两边都套上引号。

### 批处理模板

上面这种最基础的模板只能对付单个文件，如果想要批量处理文件，需要用下面这个带循环处理功能的模板：

```
for 自定义变量名 in "$@"; do
    命令 "$自定义变量名" 可选参数
done
```

注意其中的 "$@" 和 $f，它们两边都带上了引号。此时，shell 模块的“传递输入”要设置成“设为自变量（as arguments）”。用一个清除 macOS 索引缓存文件的 Automator 动作为例，其中的 shell 模块是这样的：

没错，我知道，你一定想问：凭什么要用 $@ 来代表输入的文件？这个规范怎么来的不重要，不过，如果你不喜欢的话，我们还有一个方法可以完全自定义变量名（需要使用英文，下面的中文只是为了便于理解）：

```
while read 自定义变量名; do
    命令 "$自定义变量名" 可选参数
done
```

和上一个简单套引号的模板稍有不同，使用这个模板时需要把“传递输入”设置为“至 stdin（to stdin）”。Stdin 版的缓存清理动作如下：

上面两种模板在使用中没有区别，选自己喜欢的就好。不过，尽量选择一种模板用到底，避免交叉使用造成混乱。

- 动作下载：移除 DS 文件（stdin 版） 🔗

- 动作下载：移除 DS 文件（自变量版） 🔗

### 安装第三方命令行工具

需要编写脚本来处理文件时，我的第一选择就是 shell，因为有大量现成的命令行工具供我选择，比如：

- 用 p7zip 解压文件

- 用 ocrmypdf 识别 PDF 中的文字

- 用 Pandoc 一键转换 Markdown 成 PDF

- ……

管理这些第三方命令行工具最方便的方法就是 HomeBrew，这是一个基于命令行的软件包管理器，你可以把它理解成命令行版本的 App Store。用 HomeBrew 来装第三方命令行工具很方便，完整的流程是：

- 安装 HomeBrew：/bin/bash \-c "$\(curl \-fsSL https\://raw\.githubusercontent\.com/Homebrew/install/HEAD/install\.sh\)"

- 安装命令行工具：brew install 工具名

### 第三方命令行工具的陷阱：环境变量

如果你在看到这一段之前就兴致勃勃地为第三方命令行工具编写了一段脚本，八成会发现运行错误——因为我们还没有为第三方工具设置环境变量（Environment Variables）。

正确的做法，是在命令前加一句 PATH=$PATH\:/usr/local/bin/;。下面这个解压的动作用到了第三方工具 unrar，非得加上环境变量才能运行：

\> 动作下载：UNRAR 解压文件 🔗

环境变量就像一个路标，Automator 全靠环它来“找到”命令行工具所在的位置，这样才能正确执行命令。

### 一个例子：用 ocrmypdf 让扫描件可以被检索

读过扫描版 PDF 的人，都知道那种能看不能搜的感觉是多么难受。@契丹神童 向我推荐过 ocrmypdf 这个命令行工具，能够给扫描件填上一层文字层，使得文本能够被搜索；结合上 Automator，还可以实现批量、自动识别。

先来安装和配置 ocrmypdf。下面是安装 ocrmypdf 及中文语言包的脚本。截至写这篇文章，ocrmypdf 的最新版本是 3\.05\.02，如果下载语言包失败，请到 /usr/local/Cellar/tesseract/ 下检查一下你的版本是多少，并把命令里的旧版本号 3\.05\.02 替换掉。

```
brew install ocymypdf
wget -o /usr/local/Cellar/tesseract/3.05.02/share/tessdata/chi_sim.traineddata https://github.com/tesseract-ocr/tessdata/raw/master/chi_sim.traineddata
```

ocrmypdf 的命令较多，我们只介绍一条：处理中英双语 PDF 文件。如果把 原文件路径 和 新文件路径 填成同一条路径，就可以在 OCR 处理完后覆盖掉原 PDF 文档。

```
ocrmypdf 原文件路径 新文件路径 -l chi_sim+eng --force-ocr
```

接着，我们把这条命令用到 Automator 里面，套用一下之前的批处理模板就做好了。方便理解代码起见，展示的是一个覆盖原文件的版本。

\> 动作下载 🔗

我这个 Application 只是个例子，你可以做成快速操作，直接在 PDF 预览面板里进行识别；或者干脆搞个文件夹操作，把所有放进去的 PDF 都自动光学识别一遍。

总之，以后直接在 Spotlight 就能搜这些 PDF 的内容了。

## 用 shell 来处理文本

上面一节主要介绍 shell 在处理文件方面的应用，其实 shell 处理起文本来也很强悍，但是多数情况下我们不会单独使用 shell 的文本处理功能（不然你指望把它输出到哪里呢），而是把它和 AppleScript 结合起来，在利用 AppleScript 来和系统沟通的同时，填补后者在文本处理方面的不足：

- 提取特定文本

- 正则进行文本替换

- 验证文本是否匹配

- ……

这一节里，我们先来看看 AppleScript 和 shell 是怎么结合的，再通过几个案例来了解灵活组合的思路。

### 将 Shell 嵌入 AppleScript

一般来说，AppleScript 套 shell 的样板是这样的：

```
do shell script "你的脚本"
set output to result
```

把你的脚本填入 你的脚本，第二行的 output 就是 shell 返回的结果。来看一个文本替换的例子：

```
set input to "那些用三个句号当省略号的家伙真是令人无语。。。"
do shell script "echo " & input & "| sed 's/。。。/……/'"
set output to result
```

上面这段脚本的结果就是 那些用三个句号当省略号的家伙真是令人无语……。

重点关注 shell 脚本部分，里面有两个重点：

```
"echo " & 输入 & "| sed 's/。。。/……/'"
```

- 连接符：这段 shell 脚本由 3 部分组成，每部分之间用 & 连接。很多时候我们要往 shell 脚本里面插 AppleScript 变量，就需要借助连接符号（直接把变量写进引号里面，会被 AppleScript 当成文本）。

- 引号，shell 脚本中不能包含英文直角引号 "，不然 AppleScript 会和脚本前后的引号搞混。所幸多数 shell 脚本对于引号的要求比较宽松，一般可以在 shell 脚本里使用英文直角单引号 '。

### 将 AppleScript 嵌入 Shell

AppleScript 也可以反过来嵌入到 shell 中，赋予后者调用系统控件、模拟鼠键操作的能力。当然 shell 也能和系统沟通，但是不如 AppleScript 来得简单，何况我们已经有了 AppleScript 的基础，此时搞一下拿来主义倒是一件很划算的事。

下面还是从一个最简单的案例入手。在用 shell 写完一个文本处理的脚本后，我们可以借用一段 AppleScript 来实现自动粘贴——其实 shell 本身是很难直接实现“粘贴”这个动作的，但是用 AppleScript 就很简单（指挥一下快捷键就行），所以“粘贴”的脚本就是 shell 和 AppleScript 的组合：

- 红色部分：用于触发 AppleScript

- 蓝色部分：真正起作用的 AppleScript 代码

记住 osascript \-e 'AppleScript 脚本' 这个模板，以后我们会经常用到它。

和多数 shell 命令一样，osascript 也有一系列参数，但我们最需要关注的只有 \-e，它允许 osascript 直接运行一条命令，而不需要把命令存储成文件后再来执行（不幸的是，后面那种麻烦的方案是 osascript 的默认使用方式）。

参考：man page osascript（Apple 官方页面已关闭，故援引第三方资料）

知识点：Shell 出现乱码怎么办

用 shell 来处理文本，尤其时处理中文字符时，很可能会出现乱码。

为了避免出现这种情况，可以在 shell 脚本的最开头加上一段环境变量，指定 shell 来处理中文：

```
export LANG="en_US.UTF-8"
```

注：上图乱码的正确中文是“生命万岁”。

### 模拟键盘快捷键

上面展示的例子就是 osascript 最简单的一个应用，我们还可以继续拓展。由于上一张已经介绍过 AppleScript 模拟快捷键的用法，这里只重点列一下编写 shell 脚本时常用的几条：

- 复制：osascript \-e 'tell application "System Events" to key code 8 using command down'，方便获得当前光标所选的文本。AppleScript 没有“所选文本”这个概念，所以要模拟复制动作来获取所选文本。

- 按空格键：osascript \-e 'tell application "System Events" to key code 49'，模拟按下空格键，可以跳过系统弹窗（prompt）。

- 粘贴为纯文本：osascript \-e 'tell application "System Events" to key code 9 using \{command down, option down, control down\}'，粘贴不带样式的文本，在富文本编辑器里处理文字时用得上。

发现了吗？AppleScript 部分都只有一行，没有 end tell，因为我们已经指定过 \-e 参数，所以务必把代码精简在一行里。换个角度看，毕竟 osascript 已经是“套壳”了，所以 AppleScript 部分能简化就尽量简化——应该没有人喜欢俄罗斯套娃一样的代码吧？

### 调用输入框

很多时候我们需要在运行 shell 脚本的中途临时输入一些文本或数据——比如给文件重命名、调整图片宽度——此时 shell 就要借用 AppleScript 来调用输入框了。

来看一个“调整图片分辨率”的 Automator 动作，运行动作后我们会看到一个输入框，它就是 shell 拜托 AppleScript 召唤出来的：

这个动作的内部步骤是这样的：

想必你已经发现，脚本部分两次调用了 AppleScript：

- 红色部分：呼出输入框

- 橙色部分：将用户输入的文本设置为变量

模板中的绿色部分还可以自定义提示语和默认图片宽度。

遇到这种不需要大改动、拿来就能用的代码模板，我们最好准备一个工具把它们收集起来，下次需要的时候直接拷贝。Copied 就是一个理想的工具，里面可以设置一个专门的分组来收集代码模板。

虽然已经给出了图片，但我还是提供代码片段，方便大家直接复制：

```
read -r -d '' applescriptCode1 <<'EOF'
    set imgWidth to text returned of (display dialog "提示语" default answer "默认值")
    return imgWidth
EOF

imgWidth=$(osascript -e "$applescriptCode1")
```

到了这里，第二、三章看得比较认真的读者可能会跳出来质疑：为什么要自己造轮子，Automator 里明明自带了“请求用户输入”的步骤。Automator 也能获得用户输入不错，但是只能传递一个数据给 shell 模块，这个配额在获取文件变量时已经用完，我们也不能在 shell 脚本中间插入一个请求输入的步骤。所以，还是需要在脚本里实现输入。

这里我不想解释每一行代码的作用，暂时我们知道每一块的用途、了解哪些部分可以自定义就足够了。

严格来说，shell 本身其实能够接受用户输入，但是需要自己便携用户输入界面（比如用 Zenity）……光是听着就让人打退堂鼓。还是借助 AppleScript 来调用系统输入框性价比来得高。

### 发送通知

在命令行里执行命令时，不管结果如何，我们都会得到一个结果；而用 Automator 来运行 shell 时，默认情况下系统只会默默无闻地完成任务，除非出了严重的错误才会抛出一个警告。

不管运行结果如何，我们都希望 Automator 能给个明确的提示：如果运行成功，需要有个动作结束的提示音；如果运行错误，也请 Automator 告诉我们问题出在哪里，而不是抛出一个仿佛在嘲讽用户的警告弹窗。

如果你只想知道最后的结果，那么和上一章中一样，末尾直接接上通知模块就行了。

对于通知内容、提示音有进一步客制化需求的话，我们就需要借助 AppleScript，调用系统通知来显示运行结果。

一条完整的通知模板可以很长：

```
osascript -e 'display notification "通知内容" with title "通知标题" sound name "提示音文件名"'
```

上面这个通知模板里的内容都是预设的，如果你想显示 shell 的运行结果，也有办法：

```
osascript -e "display notification \"$(shell 命令)\""
```

注：上图的脚本是 osascript \-e "display notification \\"$\(echo 运行成功\)\\""。

在实际使用的时候，不一定要照抄上面的模板，最好是看自己需要来选取标题、内容和声音。

针对远程工作、通过 GitHub 来协作的工作方式，我写了下面的脚本来快速 Pull 文件（简单理解为“从远程同步文件到本地”），它的通知形式就只有文本。

```
cd /Users/apple/Power-Plus
git fetch
osascript -e "display notification \"$(git pull)\""
```

osascript 这一行负责发出通知，内容是 git pull 命令的结果。如果成功“同步”，就能看到一切安好的提示：

示例通知里面只调用了一条命令的结果，你当然可以组合任意多条命令，让通知达到你满意的样式为止——甚至可以根据运行情况发出对应的提示（这个脚本内容超纲了，这里只是给大家看个大概意思）。

我承认，脚本中大量的反斜杠 \\ 很不优雅，但它们有着不可替代的转义作用，能够帮助 Automator 区分开 AppleScript 脚本内部、两端的不同引号 "。

另外，编写一些简单的脚本时，也可以只发出运行成功的提示音而不要整条通知（横幅多了其实很碍眼）。此时不劳烦 osascript，只需要下面这条简短的代码：

```
命令 && afplay "/System/Library/Sounds/Submarine.aiff"
```

&& 是一组连接符号，表示前一条命令运行成功才会继续发出提示音。连接符后的 /System/Library/Sounds/Submarine\.aiff 是系统自带提示音文件的路径，不喜欢 Submarine 这个声音的话，在 /System/Library/Sounds/ 文件夹里还有十几个选择，不过要尽量避免使用 Funk 和 Glass 两种声音，以免和系统默认提示音搞混。

知识点：shell 中的多命令执行符

包括上文提到的 && 在内，shell 还支持其他的多命令执行符（连接符），能够指定多条命令之间的先后关系：

- 命令1 ; 命令2：先运行前一个命令，再运行后一个命令。

- 命令1 && 命令2：前一个命令运行整齐，才运行后一个命令。常用与命令运行正确后发送通知提醒。

- 命令1 \|\| 命令2：前一个命令运行错误，才会运行后一个命令。可以把后一个命令作为补救措施。

本章“批处理模板”小节所介绍的“移除 DS 文件”动作就用到了 \|\| 连接符，当 Automator 没找到可清理的缓存文件时直接就啥也不干（说明压缩包里本来就干净，用不着清理），避免报错影响心情。

如果仔细看的话，不难发现，这一章里我们不断和“报错”作斗争。在 A Philosophy of Software Design（软件设计的哲学）一书中，作者着重提到过“减少报错”的原则，软件能自己解决的就不要丢给用户（不妨回忆一下某些操作系统中让人摸不着头脑的报错窗口）。

这种对于软件设计的追求，我们也应该借鉴。编写 Automator 动作本来就是为了让工作更方便，要是花了大力气反倒制造了一堆警告、弹窗，那也太让人失望了吧。

## 小结

这一章里面，我们系统性地讲解了 shell 和 Automator 的组合，也涉及了 shell 和 AppleScript 的相互嵌套。“运行 Shell 脚本”这个看似简单的模块，背后的具体用法已经被我们挖得非常深了。但不管写多少，本章都无法覆盖所有的场景。在此希望读者们掌握 shell 脚本的编写思路与现成模板，在使用中灵活组合。

AppleScript 和 Shell 几乎是整个 Automator 里面最难的内容，它们为我们打开了自定义 Automator 的新大门。第一遍阅读时遇到难以理解的地方可以先跳过，以后用到时再返回来。如果你读完觉得神清气爽、思路通畅，那么也提前恭喜你：你已经是 macOS 自动化的高玩了。

---

[前往标准阅读页](https://utgd.net/article/20224?audience=web)
