Linux 与 macOS 都提供类 Unix 的命令行环境,这种相似性容易让人以为 Linux 命令在 macOS 终端里也会表现一致。它们经常一致,但并非总是如此。

例如,下面这条命令在 Linux 上本来是能正常工作的:

sed -i 's/debug=true/debug=false/' config.ini

我在 Linux 上运行过这条命令很多次,每次都能正常工作。但在 macOS 上,同一条命令却失败了。我检查了语法,检查了文件,又重新运行了一遍,仍然失败。

本文说明为什么 Linux 命令在 macOS 上有时会表现不同:这些差异从何而来、为什么存在,以及如何在它们影响你的 shell 脚本或命令行工作流之前识别出来。

起初一切看起来都很熟悉

失败的那条 sed 命令并不是偶然的例外。大多数 Linux 命令在 macOS 上无需任何改动就能运行。

这种一致性会制造一种预期:既然一条命令能用,下一条也应该能用。

这种预期是合理的。Linux 和 macOS 都提供类 Unix 的命令行环境。类 Unix 操作系统遵循 Unix 引入的许多接口和设计原则。因此,两个系统都暴露了许多相同的命令行工具和熟悉的目录布局。

打开 macOS 终端,你会认出 ls、cd、pwd、cp、mv 和 ssh 这些命令。你也会看到 /usr、/etc、/var 和 /tmp 这些熟悉的目录。

对于日常任务,体验通常是一样的。这就容易让人以为底层的工具行为也相同。

差异正是从这种假设开始的。

同一个命令名并不总是同一个程序

命令名只是告诉 shell 要运行什么,它并不说明这条命令是如何实现的。

Linux 和 macOS 共享许多命令名,因为两者都遵循 POSIX。POSIX 是一个为类 Unix 操作系统定义通用命令行接口和系统行为的标准。它规定了许多命令应该做什么,但并不要求每个实现都支持相同的扩展或命令行选项。

在 Linux 上,sed、grep、find 和 tar 这类命令通常来自 GNU 工具集。GNU 工具集是由 GNU 项目开发的一组命令行工具。

在 macOS 上,许多同名命令来自 BSD 工具集。BSD 工具集提供相同的核心功能,但它们是独立开发的。因此,某些选项、默认值和特性存在差异。

以开头提到的 sed 命令为例。

在 Linux 上:

sed -i 's/debug=true/debug=false/' config.ini

在 macOS 上运行上述命令会得到类似这样的错误:

sed: 1: "config.ini": extra characters at the end of s command

这是因为 BSD 的 sed 把 config.ini 当作备份后缀参数,于是没有剩下任何东西可以当作实际文件,它便试图把 s/debug=true/debug=false/ 当作文件/脚本读取,结果报错。

要让它能在 macOS 上工作,需要用空字符串作为显式的备份后缀参数:

sed -i '' 's/debug=true/debug=false/' config.ini

两条命令都就地编辑文件。区别在于 -i 选项。-i 选项并非 POSIX 定义。GNU 的 sed 和 BSD 的 sed 各自独立添加了它,并各自选择了不同的语法。GNU 的 sed 允许 -i 不带备份扩展名,BSD 的 sed 则要求提供备份扩展名。空字符串('')告诉 BSD 的 sed 不要创建备份。

sed 只是一个例子。find、date、stat、xargs 和 tar 等命令同样包含 GNU 特有的和 BSD 特有的扩展。大多数差异很小,但当你把命令在 Linux 和 macOS 之间复制,或者编写必须在两个系统上运行的 shell 脚本时,它们就变得重要了。

要点:

命令名往往相同,背后的实现却可能不同。这一区别解释了 Linux 与 macOS 之间许多命令差异。

当 shell 脚本不再可移植

在终端里能用的命令,放进 shell 脚本里不一定可移植。

shell 脚本是包含供 shell 执行的命令的文本文件。脚本会组合多个命令,因此更容易依赖 Linux 与 macOS 之间存在差异的行为。

有些差异会立即产生错误,这类问题通常容易修复。

更棘手的问题出现在脚本运行成功却产生意外输出的时候。脚本看起来正确,结果却是错的。

看这条命令:

find . -type f -printf '%f\n'

在许多 Linux 系统上,它会打印每个匹配文件的名称。

在 macOS 上它会失败,因为 BSD 实现的 find 不支持 -printf 选项。-printf 是 GNU 扩展,不属于 POSIX 标准。

如果你在 macOS(BSD 的 find)上运行上述命令,会得到类似下面的错误:

find: -printf: unknown primary or operator

这是一个常见的可移植性问题。脚本可能在一个操作系统上运行良好,因为它依赖了另一个系统上不存在的扩展。

如果 shell 脚本必须同时运行在 Linux 和 macOS 上,在可行的情况下应按 POSIX 标准来写。POSIX 为类 Unix 操作系统定义了一组通用接口。标准之外的功能通常称为扩展,它们可能在另一个平台上不存在,或者行为不同。Apple 建议在编写跨平台 shell 脚本时避免使用平台特有的扩展。

要点:

可移植的 shell 脚本不只取决于命令名,还取决于是否使用了 POSIX 定义的功能,或在你打算支持的每个操作系统上都可用的功能。

为什么 sudo 并不总能解决问题

在 Linux 上,权限错误通常意味着当前用户没有足够的权限。用 sudo 运行命令通常能解决。

在 macOS 上,这种做法并不总是有效。

即使使用 sudo 运行命令,你仍可能看到类似这样的错误:

Operation not permitted

原因是 macOS 在传统 Unix 文件权限之外增加了安全层。

其中一层是 System Integrity Protection(SIP)。SIP 保护操作系统的关键部分不被修改。它还会限制对受保护文件和目录的更改,即使是以 root 权限运行的进程也不例外。Apple 设计 SIP 是为了降低恶意软件和意外系统更改的影响。

另一层是 Transparency, Consent, and Control(TCC)。TCC 是 Apple 的隐私框架,它控制对敏感用户数据的访问,例如桌面、文稿和下载文件夹。TCC 评估的是请求访问的应用程序,而不只是运行命令的用户账户。

这就是 sudo 有时不够用的原因。终端命令仍可能失败,因为终端本身没有被授予访问受保护数据的权限。在许多情况下,为终端或你的终端应用授予完全磁盘访问权限可以解决与 TCC 相关的访问错误。

并非每个 Operation not permitted 错误都由 SIP 或 TCC 引起。传统文件权限、文件标志或其他安全策略也可能产生同样的错误。不过,对于从 Linux 转向 macOS 的用户来说,SIP 和 TCC 是 sudo 行为不符合预期最常见的原因之一。

要点:

在 Linux 上,sudo 通常足以解决权限问题。在 macOS 上,提升权限只是安全模型的一部分。SIP 和 TCC 仍可能阻止命令访问或修改受保护的资源。

Linux 开发者如何适应 macOS

有经验的开发者很少试图让 macOS 完全像 Linux 那样工作。他们转而构建一个可预测的开发环境。

第一步是理解 PATH 环境变量。PATH 是一个有序的目录列表,shell 在其中查找可执行文件。如果多个可执行文件同名,shell 会运行它找到的第一个。

许多开发者使用 Homebrew 包管理器安装额外的命令行工具。它在不替换操作系统自带工具的情况下安装软件。

一种常见的安装是 GNU Coreutils。GNU Coreutils 是一组标准命令行工具,包括 ls、cp、mv 和 sort。为避免与 macOS 提供的 BSD 工具冲突,Homebrew 安装的许多命令带有 g 前缀,例如 gls 和 gcp。如果你更喜欢标准的命令名,可以配置 PATH,让它在系统目录之前搜索 Homebrew 的 gnubin 目录。

同样的做法适用于其他 GNU 工具,包括 gnu-sed、grep 和 findutils。安装它们会让 GNU 实现可用。只有当你调用带前缀的命令(例如 gsed),或者配置 PATH 让 GNU 版本优先时,shell 才会使用它们。

安装 GNU 工具并不会改变 macOS 的工作方式,它改变的是 shell 运行哪一个实现。

要点:

Homebrew 让 GNU 工具可用,但 PATH 决定 shell 执行哪一个实现。可预测的开发环境始于知道你的命令解析到哪个可执行文件。

结论

Linux 和 macOS 共享许多命令名,因为两者都提供类 Unix 的命令行环境。这种相似性容易让人期待相同的行为。

命令名告诉 shell 要运行什么,它并不说明该命令是如何实现的,或者依赖哪些平台特有的功能。POSIX 为类 Unix 操作系统定义了一组通用接口,但它也允许实现提供自己的扩展。因此,两个同名命令在不同操作系统上可能表现不同。

同样的原则解释了其余差异。有些来自 GNU 和 BSD 工具集,另一些来自 macOS 的安全特性,例如 System Integrity Protection(SIP)和 Transparency, Consent, and Control(TCC)。当 shell 脚本依赖并非处处可用的扩展时,可移植性就会下降。

这些差异都不是随机的。每一个都源于操作系统或实现该命令的工具所做的设计决策。

理解这一点之后,调试会容易得多。与其问“为什么这个命令失败了?”,不如问“这个命令对平台做了哪些假设?”这个问题通常能更快带你找到答案。

要点:

Linux 和 macOS 共享许多命令名,但并不总是共享相同的行为。理解命令背后的实现、标准和操作系统,是解释其行为差异最可靠的方法。

常见问题(FAQ)

问:为什么 Linux 命令不总能在 macOS 上工作?

答:不是。macOS 并非基于 Linux。macOS 构建在 Darwin 之上,这是 Apple 的开源操作系统。Darwin 将 XNU 内核与源自 BSD 及其他开源项目的组件结合在一起。Linux 和 macOS 都是类 Unix 操作系统,但它们使用不同的内核和不同的命令行工具。

问:为什么 sed -i 在 Linux 上能用,在 macOS 上却不行?

答:大多数 Linux 发行版包含 GNU 的 sed,而 macOS 包含 BSD 的 sed。

两者都支持就地编辑,但 -i 选项的语法不同。GNU 的 sed 允许 -i 不带备份扩展名,BSD 的 sed 要求提供备份扩展名。如果你不想要备份文件,传入一个空字符串:

sed -i '' 's/foo/bar/' file.txt

问:为什么 find -printf 在 Linux 上能用,在 macOS 上却不行?

答:-printf 是 GNU 扩展。macOS 附带的 BSD 实现的 find 不支持它,因为它不属于 POSIX 规范。

问:为什么有些 Linux 命令在 macOS 上行为不同?

答:Linux 和 macOS 都提供类 Unix 的命令行环境,因此许多命令名相同。但底层实现往往不同。大多数 Linux 发行版提供 GNU 工具,而 macOS 提供 BSD 工具。尽管两者在核心行为上都遵循 POSIX,但它们支持的扩展、选项和默认值不同。

问:为什么 sudo 不能修复 macOS 上的所有权限错误?

答:sudo 授予提升的权限,但 macOS 还强制执行额外的安全机制。例如,**System Integrity Protection(SIP)**保护操作系统的关键部分,**Transparency, Consent, and Control(TCC)**保护对敏感用户数据的访问。即使命令以 root 权限运行,这些机制也可能拒绝访问。

并非每个 Operation not permitted 错误都由 SIP 或 TCC 引起,但它们是 Linux 用户在 macOS 上遇到权限问题的常见原因。

问:我能让 macOS 像 Linux 一样工作吗?

答:你可以让命令行环境更熟悉,但无法让 macOS 完全像 Linux 那样工作。许多开发者用 Homebrew 安装 GNU 工具,并配置 shell 使用这些工具。这能减少兼容性问题,但 macOS 仍然使用自己的内核、安全模型和默认系统工具。

问:我应该用 GNU 工具替换 BSD 工具吗?

答:通常不需要。Homebrew 会把 GNU 工具与 macOS 自带的 BSD 工具一起安装。对许多工具来说,GNU 版本使用 g 前缀,例如 gsed,因此不会与系统版本冲突。如果你希望 shell 优先使用 GNU 实现,可以配置 PATH。

问:如何检查我的 shell 正在运行哪个命令?

答:使用:

command -v sed

command -v 由 POSIX 定义,会显示 shell 将运行哪个可执行文件。你也可以使用:

which sed

许多系统提供 which,但对 shell 脚本来说 command -v 是更可移植的选择。

问:如何编写同时适用于 Linux 和 macOS 的 shell 脚本?

答:在可行的情况下使用 POSIX 功能。避免 GNU 特有或 BSD 特有的扩展,除非你的脚本针对特定的操作系统。如果脚本必须同时支持 Linux 和 macOS,请在两个平台上测试,而不是假设行为相同。

问:什么是 POSIX?

答:POSIX(Portable Operating System Interface)是一个为类 Unix 操作系统定义通用接口和行为的标准。它规定了命令和系统调用应如何表现,但允许实现添加自己的扩展。

问:如果 Linux 和 macOS 都遵循 POSIX,为什么仍有差异?

答:POSIX 定义的是通用基线,而非所有功能。许多常用的选项是 GNU 或 BSD 扩展,不属于标准。因此不同实现可以在保持 POSIX 兼容的同时提供额外功能。

问:GNU 工具和 BSD 工具有什么区别?

答:GNU 工具是由 GNU 项目开发的命令行工具,常用于 Linux。BSD 工具源自 Berkeley Software Distribution(BSD)项目,随 macOS 一起提供。两者提供类似的命令,但某些选项、默认值和行为不同。

问:什么让 shell 脚本可移植?

答:可移植的 shell 脚本尽可能避免 GNU 特有或 BSD 特有的扩展。它依赖 POSIX 定义的功能,并在预期支持的每个操作系统上测试。

问:什么是 System Integrity Protection(SIP)?

答:System Integrity Protection(SIP)是 macOS 的一项安全特性,保护关键系统文件和目录不被修改,即使是以 root 权限运行的进程也不例外。

问:什么是 Transparency, Consent, and Control(TCC)?

答:Transparency, Consent, and Control(TCC)是 Apple 的隐私框架。它控制应用程序对敏感用户数据的访问,例如桌面、文稿和下载文件夹中的文件,并要求在授予访问前获得用户同意。

问:Unix 权限和 TCC 有什么区别?

答:Unix 权限决定用户是否有权访问文件。TCC 决定应用程序是否被允许访问受保护的用户数据。在 macOS 上,两项检查可能都需要通过。

问:什么是 Homebrew?

答:Homebrew 是 macOS 的包管理器。它安装和管理软件,而不替换操作系统提供的工具。

问:为什么 Homebrew 安装的 GNU 命令通常以 g 开头?

答:Homebrew 安装的许多 GNU 工具带有 g 前缀,例如 gsed 和 gcp,以避免与 macOS 已包含的 BSD 版本冲突。

问:PATH 环境变量是什么?

答:PATH 是一个有序的目录列表,shell 在其中查找可执行程序。当多个可执行文件同名时,shell 运行它在 PATH 中找到的第一个。

问:为什么安装 GNU 工具不会自动改变命令行为?

答:安装 GNU 工具只是让它们可用。shell 仍会使用在 PATH 中排在最前面的可执行文件。除非更新 PATH 或显式调用 GNU 命令(例如 gsed),否则仍会运行 BSD 版本。