POSIX入门科普:Shell脚本跨平台通用原理与避坑指南 POSIX入门科普为什么你的Shell脚本能在Linux和macOS通用很多初学 Shell 脚本的人会有一个很自然的困惑我在 Linux 上写好的脚本换到 macOS 上居然也能跑我在 macOS 终端里敲的命令到了 Linux 服务器上也基本通用这两个系统的内核、包管理器、默认 Shell 都不一样凭什么能做到这一点答案不是“巧合”也不是“苹果抄了 Linux”而是因为它们共同遵守了一套看不见的契约POSIX 标准。这篇文章不打算堆砌晦涩的规范条文而是从一个开发者的实际问题出发讲清楚 POSIX 到底是什么、它规定了哪些东西、为什么你的 Shell 脚本能在 Linux 和 macOS 之间迁移以及更重要的——哪些地方你以为兼容、实际上一跑就炸。读完这篇文章你会得到三层收获彻底理解 POSIX、Unix、Linux、macOS 之间的关系不再被这些词绕晕。知道 Shell 脚本跨平台时哪些命令、语法、行为是安全的哪些是雷区。拿到一份可以直接用在日常开发中的可移植脚本写法和排错清单。1. 这篇文章真正要解决的问题先说一个我经常在开发群里看到的场景新同事在 Mac 上写了个部署脚本本地跑得好好的push 到 Linux 服务器上执行直接报Syntax error或者command not found。然后一群人开始围着终端排查最后发现原因千奇百怪。这类问题的本质是很多人默认“Linux 和 macOS 都是 Unix 系所以脚本一定通用”。这个判断大方向没错但它忽略了一个关键事实它们不是同一个操作系统只是共享了一部分接口约定。共享的部分就是 POSIX 标准覆盖的范围而超出 POSIX 范围的部分比如 GNU 工具链的扩展参数、macOS 特有的命令行为恰恰是脚本“水土不服”的高发区。这篇文章适合下面这些读者刚接触 Shell 编程想搞清楚跨平台原理的新手。日常在 macOS 上开发、部署目标却是 Linux 服务器的后端工程师。需要维护多套 CI/CD 脚本希望减少平台分支的 DevOps 工程师。准备 Linux 面试需要把 POSIX 这个概念讲清楚的人。我们的目标不是背诵标准条文而是建立一个判断框架当你写一行 Shell 命令时你能大致判断出“这个语法放在 Linux 和 macOS 上是否都安全”。2. POSIX 是什么一套“接口契约”不是一个操作系统很多人第一次听到 POSIX会误以为它是一个操作系统或者是一个开源项目。其实都不是。2.1 POSIX 的字面含义POSIX 全称是 Portable Operating System Interface发音通常读作“pah-zicks”。这个名字本身就已经把它的目的说清楚了可移植的操作系统接口。简单理解POSIX 不是操作系统而是一份标准文档规定了操作系统应该向应用程序提供哪些接口。只要两个操作系统都实现了 POSIX 规定的那部分接口那么针对这套接口写的程序就可以在两边编译运行不需要改代码。打个比方POSIX 就像电器行业的插座标准。你买了一个国标插头的充电器在中国的酒店能充电在另一个品牌的酒店也能充电。不是因为这两家酒店用了同一个电网公司而是因为它们都遵守了同一个插座接口标准。POSIX 就是操作系统世界的“插座标准”。2.2 POSIX 与 Unix 的关系这里要先纠正一个刻板印象Unix 不是 Linux 的旧称Linux 也不是 Unix 的某个发行版。Unix 是 1969 年在贝尔实验室诞生的一个商业操作系统家族后来分化出 BSD、System V 等多个分支。Linux 则是 1991 年由 Linus Torvalds 从零写出的内核它没有使用 Unix 的商业代码但它的设计目标就是兼容 Unix 的行为接口。macOS 的情况更特殊。macOS 的内核叫 XNU底层有一部分来自 BSD Unix所以它也是一个 Unix 系统并且在 2020 年左右通过了 POSIX 认证。Linux 虽然没有正式拿到 POSIX 认证但它在接口层面高度兼容 POSIX 标准。所以结论是Linux 和 macOS 是两个实现完全不同的操作系统但它们都努力对应用程序暴露一套相似的接口这套接口就是 POSIX 标准定义出来的。这也是你的 Shell 脚本能在两边通用的根本原因。2.3 POSIX 标准到底规定了什么POSIX 标准覆盖的范围非常广但对我们日常写脚本的人来说最重要的是下面几块范畴包含内容你平时接触到的东西系统调用接口open、read、write、fork、exec 等用 C 语言写文件操作、进程创建命令行解释器Shell 的语法和行为bash、sh、zsh 的基础语法常用命令文件操作、文本处理、进程管理ls、cp、grep、sed、awk环境变量标准环境变量名和含义PATH、HOME、LANG文件系统布局目录结构的基本约定/tmp、/etc、/usr/bin权限模型文件权限、用户和组chmod 755、chown换到实际开发场景就是下面这些事你在 Linux 上执行ls -lmacOS 上也能执行你调用fork()创建子进程两边都有这个系统调用你写#!/bin/sh的脚本两边的系统都能解释执行。这些都是 POSIX 的功劳。3. 为什么偏偏是 Shell 脚本受益最大如果说 POSIX 是接口契约那 Shell 脚本就是这份契约最直接的受益者。原因很简单Shell 脚本不经过编译直接调用系统命令和解释器语法所以它对接口兼容性的依赖是最敏感的。3.1 编译型程序为什么不容易踩坑写一个 C 程序在 Linux 上编译生成二进制拿到 macOS 上跑不了因为二进制格式都不一样Linux 是 ELFmacOS 是 Mach-O。但如果你带着源代码在两边的系统上分别用编译器重新编译源代码大多能编译通过因为系统调用接口是一致的。这就是 POSIX 对编译型语言的作用它不是让程序二进制通用而是让源代码可以跨平台重编译。3.2 Shell 脚本的跨平台方式Shell 脚本不编译它本身就是源码。脚本里的每一条命令在运行的瞬间由 Shell 去系统里查找对应的可执行程序并调用。所以只要你用的命令、语法、选项在两边都存在且行为一致脚本就能直接运行。但这里有一个关键点Shell 脚本跨平台本质上是“脚本解析器 外部命令库”的组合能跨平台。在 Linux 上/bin/sh通常是指向dash或者bash的符号链接在 macOS 上/bin/sh是bash的旧版本或者在新系统里变成了zsh的兼容模式。而ls、grep、sed这些命令在 Linux 上大多来自 GNU 工具包在 macOS 上则来自 BSD 工具包。这就是后面所有坑的根源。3.3 一个关键认知GNU 工具和 BSD 工具的分歧写脚本时最隐蔽的坑不是语法而是命令参数的行为差异。Linux 的sed、awk、grep、find等命令绝大多数来自 GNU 项目GNU 项目在实现 POSIX 功能之外还加了很多扩展参数。macOS 的同类命令来自 BSDBSD 工具对 POSIX 的实现更“保守”扩展参数少且个别参数的行为和 GNU 不一致。举一个最常见的例子# Linux 上可以用 -i 直接修改文件且可以带后缀参数 sed -i s/foo/bar/g test.txt # macOS 上 -i 要求必须带一个备份后缀参数 sed -i s/foo/bar/g test.txt如果直接在两个系统上跑同一个sed -i命令Linux 能正常运行macOS 就会报错或者行为不符合预期。这不是你的语法写错了而是两边的sed实现不同。这个例子告诉我们一个判断准则写跨平台脚本时优先使用 POSIX 标准里明确规定的参数少用 GNU 扩展。如果你不确定某个参数是不是 POSIX 规定的一个快速判断方法是在 macOS 上跑一下试试因为 macOS 的工具集更贴近 POSIX 基线。4. Shell 脚本跨平台真正的通用层在哪里现在我们把“跨平台”这个抽象概念拆开看看到底是哪些东西在跨平台。4.1 通用层一Shell 语法核心if、for、while、case、函数定义、变量赋值、重定向、管道这些语法是 POSIX Shell 规范规定的内容无论在 Linux 还是 macOS 上都一样。比如下面这个for循环在两个平台上跑起来几乎没有差别#!/bin/sh for file in *.log; do echo 处理文件: $file done这里要注意一个小细节脚本的 shebang 写的是#!/bin/sh而不是#!/bin/bash。这是跨平台脚本的第一个最佳实践后面会详细讲。4.2 通用层二POSIX 规定的系统命令POSIX 标准规定了一批“系统必备命令”这些命令在所有遵循 POSIX 的系统上都必须存在。例如ls cd cp mv rm mkdir grep sed awk cut sort uniq wc head tail理论上这些命令的基本行为在 Linux 和 macOS 上是一致的。但在实际使用中你必须刻意避免使用它们的 GNU 扩展参数只使用 POSIX 规定的基础参数。4.3 通用层三标准环境变量PATH、HOME、USER、SHELL、LANG这些变量在两个系统上都会存在语义也基本一致。但这里有一个典型的误区很多新手以为$HOME在哪个系统都指向自己的家目录于是直接写cd $HOME/Downloads这个脚本在 macOS 上大概率没问题在 Linux 服务器上也不一定报错但它隐含了一个假设~/Downloads存在。事实上macOS 的用户目录结构和 Linux 服务器差别很大更稳妥的写法是先去判断目录是否存在再决定是否切换。4.4 真正的坑平台差异层下面这些内容不在 POSIX 标准的统一范围内恰恰是跨平台脚本出问题的地方差异点Linux 常见行为macOS 常见行为包管理器apt/yum/dnfbrew用户目录结构服务器常见/home/用户名/Users/用户名默认 Shellbash很多发行版zsh新版默认sed -i参数后缀可选后缀必填find的-exec行为GNU 实现BSD 实现工具链路径/usr/bin、/bin/usr/bin、但部分工具在/usr/local/bin系统日志/var/log下集中统一通过log show理解了这张表你就能明白一个事实跨平台 Shell 脚本能通用的部分是那层“POSIX 接口契约”不能通用的部分是契约之外的系统特有行为。而一个成熟脚本的编写过程本质上就是在不断地识别自己写的命令到底落在了哪一层。5. 从零写一个可跨平台的 Shell 脚本理论讲了不少现在我们把思路落地。下面用一个“日志文件归档”的小项目演示怎么写出一个同时在 Linux 和 macOS 上运行的脚本。5.1 明确需求与平台约束这个脚本要做的事情很简单检查日志目录是否存在。把 7 天前的.log文件打包压缩。删除打包前的原始文件。输出一条执行结果。这个需求涉及文件判断、日期计算、find 查找、tar 压缩、rm 删除。每一处都有跨平台的分歧点。我们用一个约束来回避大部分问题统一写成 POSIX 语法使用#!/bin/sh不使用 bash 特有语法不用 GNU 扩展参数。5.2 最终脚本archive_logs.sh#!/bin/sh # 文件路径archive_logs.sh # 用途归档指定目录下 7 天前的 .log 文件 # 用法./archive_logs.sh /path/to/log/dir LOG_DIR${1:-/var/log/myapp} ARCHIVE_DIR${LOG_DIR}/archive RETENTION_DAYS7 if [ ! -d $LOG_DIR ]; then echo 错误日志目录不存在$LOG_DIR 2 exit 1 fi if [ ! -d $ARCHIVE_DIR ]; then mkdir -p $ARCHIVE_DIR || exit 1 fi # 关键点mtime 7 表示 7 天前被修改过的文件 # 注意这里刻意不用 GNU find 的 -delete 扩展参数 find $LOG_DIR -maxdepth 1 -type f -name *.log -mtime $RETENTION_DAYS -print \ | while read -r file; do basename_file$(basename $file) tar -czf ${ARCHIVE_DIR}/${basename_file}.tar.gz -C $LOG_DIR $basename_file if [ $? -eq 0 ]; then rm $file echo 归档完成$file else echo 警告压缩失败保留原文件$file 2 fi done echo 脚本执行结束归档目录$ARCHIVE_DIR5.3 代码逻辑逐步讲解先看脚本开头LOG_DIR${1:-/var/log/myapp}这里使用了 Shell 的参数展开语法含义是“如果传入第一个参数就使用它否则使用默认值/var/log/myapp”。这是 POSIX 语法两个系统都支持。再看文件判断的判断语句if [ ! -d $LOG_DIR ]; then[实际上是test命令的另一种写法-d判断目录是否存在。这里一定要记住[和后面的参数之间、后面的参数和]之间都必须有空格这是一个新手最常见的语法错误。然后是 find 命令find $LOG_DIR -maxdepth 1 -type f -name *.log -mtime $RETENTION_DAYS -print这里有一个值得注意的地方-maxdepth 1不是 POSIX 标准参数在 macOS 的 BSD find 里也是支持的所以可以放心用。但为了更稳妥也可以改成 POSIX 标准写法用-prune实现不过可读性会差一些。这篇文章的主旨是“能用就先用”所以保留-maxdepth。接着用管道把 find 的结果喂给while read| while read -r file; do这里用-r参数是为了防止文件名里的反斜杠被错误解释。用while read而不是for file in $(find ...)是为了正确处理文件名中带空格的情况这也是一个非常经典的 Shell 编程建议。压缩命令tar -czf ${ARCHIVE_DIR}/${basename_file}.tar.gz -C $LOG_DIR $basename_file注意我先用basename $file拿到纯文件名再用-C $LOG_DIR切换到日志目录然后只打包文件名而不是打包完整路径。这个做法的好处是解压后得到的是文件名而不是一整串路径在 macOS 和 Linux 上的行为也一致。最后删文件rm $file删除前我们检查了上一条 tar 的退出码。这是生产脚本必备的防御式写法。5.4 如何运行和验证先给脚本加执行权限chmod x archive_logs.sh然后构造一个测试目录mkdir -p /tmp/test_logs touch -t 202401010000 /tmp/test_logs/old.log touch /tmp/test_logs/recent.logtouch -t可以指定文件的修改时间这里把old.log的修改时间设置到过去确保它会被归档而recent.log保持当前时间应该被跳过。执行脚本./archive_logs.sh /tmp/test_logs预期的输出是归档完成/tmp/test_logs/old.log 脚本执行结束归档目录/tmp/test_logs/archive然后检查归档目录ls -l /tmp/test_logs/archive/ tar -tzf /tmp/test_logs/archive/old.log.tar.gz如果tar -tzf打印出old.log说明打包内容正确而且原始文件已经被删除。6. 更完整的兼容性示例常用命令的跨平台写法上面那个脚本展示了从“需求”到“跨平台实现”的完整思路。下面再提供几组高频场景每一组都给出“危险写法”和“兼容写法”的对比。6.1 查看脚本的字符编码这个问题在热搜词里出现了确实很实用。很多脚本在 Linux 上正常到了 macOS 上报“非法字节序列”就是因为文件编码不是 UTF-8。在 Linux 上可以用file命令查看编码file -i archive_logs.sh输出类似archive_logs.sh: text/x-shellscript; charsetutf-8在 macOS 上file命令也支持但-i的输出格式不同file -I archive_logs.sh输出archive_logs.sh: text/x-shellscript; charsetutf-8注意参数不同Linux 用-imacOS 用-I大写 i。这也算一个经典的跨平台差异。如果你的脚本文件在编辑时被存成了 GBK 或者带 BOM 的 UTF-8Shell 解析时就可能出现诡异的语法错误。解决方法是统一用 UTF-8 无 BOM 编码保存VS Code 右下角可以直接切换编码。6.2 文件删除命令rm这个看似毫无争议的命令也有跨平台注意点。危险写法rm -rf /tmp/foo/在 Linux 上rm -rf早就习惯了macOS 上也能跑。但-r和-f都是 POSIX 参数所以这层是安全的。真正的问题是rm -rf /这种不受保护的高危路径在 macOS 上会提示“Operation not permitted”在 Linux 上则以 root 跑的时候可能把系统删掉。所以更推荐的写法是加一层保护TARGET_DIR/tmp/foo if [ $TARGET_DIR / ]; then echo 拒绝删除根目录 2 exit 1 fi rm -rf $TARGET_DIR不要小看这几行判断它在 CI 脚本里能救你一命。6.3 查看进程ps 命令Linux 上常见的写法ps auxmacOS 上也支持ps aux输出格式相似。但如果你用 Linux 特有的参数比如ps -ef加上管道去匹配特定列在 macOS 上可能列的位置不同。更稳的跨平台写法是直接用pgreppgrep -f nginxpgrep在 Linux 和 macOS 上都存在且语义一致。这也是一个“命令选得好跨平台没烦恼”的例子。6.4 获取脚本所在目录这是一个非常隐蔽的坑。很多脚本里需要找到它自己所在目录以便读取同目录配置文件。常见写法SCRIPT_DIR$(dirname $0)这个写法在简单场景下能用但当你用绝对路径调用脚本、或者通过 PATH 调用、或者符号链接调用时$0的结果就不一定是你想要的。更可靠的 POSIX 写法是SCRIPT_DIR$(CDPATH cd -- $(dirname -- $0) pwd)这段命令先把目录切换到脚本所在目录然后输出当前路径而且用CDPATH避免cd被环境变量干扰。在 Linux 和 macOS 上都能稳定得到绝对路径。6.5 算术运算Bash 和 zsh 都支持$(( ))语法这是 POSIX 规定的所以可以用count$((count 1))但不要用let count或者count后者在 zsh 和 bash 里行为可能不同POSIX 规范里也没有定义。6.6 字符串比较POSIX 标准建议用而不是来比较字符串if [ $name root ]; then echo 是 root 用户 fi在 bash 和 zsh 里也能用但它不是 POSIX 的[命令的标准写法。不信的话把 shebang 改成#!/bin/sh在某些默认sh为 dash 的 Linux 发行版上就会报错。7. 常见问题与排查思路下面把跨平台 Shell 脚本最常见的问题集中列一张表遇到问题时可以直接对照排查。问题现象可能原因排查方式解决方案syntax error near unexpected token脚本文件是 CRLF 换行或者编码不是 UTF-8用file命令检查行尾和编码用dos2unix转换或编辑器里改成 LFcommand not found用到了 GNU 扩展工具比如realpath或timeout在两边的which 命令名检查是否存在改用 POSIX 命令或加入平台判断sed: 1: test.txt: undefined label在 macOS 上执行了 GNU 风格的sed -i查看 sed 版本sed --versionLinux 支持macOS 不支持使用sed -i 或改用 Perl 调用find: -maxdepth: unknown primary某些旧的 Unix 或裁剪过的环境不支持-maxdepthman find查看当前系统 find 支持的参数改用-prune实现深度控制脚本在 Linux 正常运行macOS 报错用到了 bash 特有语法比如数组、[[ ]]检查 shebang 是否为#!/bin/bash需要 bash 就明写#!/usr/bin/env bash需要跨平台就用 POSIX 语法变量内容包含空格循环被拆开for file in $(find ...)的错误写法给变量加引号改用while read见前文 5.2 节最后一段中文文件名乱码或文件名被截断环境变量LANG设置不一致echo $LANG查看当前语言环境在脚本开头统一设置export LANGC.UTF-8或LC_ALLen_US.UTF-8这里再展开几条比较有代表性的排错思路。7.1 排错第一步确认当前用的解释器是谁不要凭感觉判断。先执行ls -l /bin/sh echo $SHELL sh --version在 Ubuntu 上/bin/sh通常指向dash在 CentOS 上指向bash在 macOS 上可能是旧版bash或兼容模式。不同的sh对语法的支持程度不同。如果你的脚本语法依赖 bash 扩展建议直接把 shebang 改成#!/usr/bin/env bash并且在两台机器上都装 bash。7.2 排错第二步用sh -x跟踪执行这是所有 Shell 脚本调试里最实用的一招sh -x archive_logs.sh /tmp/test_logs-x会打印每一条被执行的命令和它的展开结果。你一眼就能看到哪一行报错、变量被展开成了什么、管道里传输了什么。这个方法比猜“哪一步出错”快得多。7.3 排错第三步逐段隔离如果一个脚本本身很长建议先删减到最小复现。比如只保留 find 那一行先跑通再加管道再加压缩。逐段隔离能让问题范围快速缩小。8. 最佳实践与工程建议概念和排错都聊完了最后整理一套可以直接进入团队规范的最佳实践。8.1 明确定义运行环境shebang 的选型跨平台脚本的第一个选择就是写#!/bin/sh还是#!/bin/bash。如果你的脚本只用了 POSIX 语法用#!/bin/sh移植性最好。如果你的脚本用了数组、[[ ]]、${var,,}这类 bash 扩展特性就不要假装自己是 POSIX 脚本明写#!/usr/bin/env bash。永远不要不写 shebang然后指望靠手动执行bash script.sh来运行。一旦被别人用./script.sh执行环境不同就会出问题。8.2 变量引用一律加双引号这是一个永远值得强调的规则# 坏习惯变量展开后可能被拆成多个词 rm -rf $TARGET_DIR # 好习惯即使变量包含空格也没问题 rm -rf $TARGET_DIR特别是在find管道到while read的场景里不加引号几乎是生产事故的隐藏导火索。8.3 固定使用同一套工具集或者平台分支当项目必须支持 Linux 和 macOS 时有两种路线路线一只用 POSIX 接口。所有代码都限定在 POSIX 语法和 POSIX 参数里。缺点是很多好用的扩展参数用不了代码会显得啰嗦。路线二在脚本开头做平台检测然后设置变量来保存“平台相关的命令参数”。比如#!/bin/sh case $(uname -s) in Darwin) SED_INPLACEsed -i ;; Linux) SED_INPLACEsed -i ;; *) echo 不支持的系统类型 2 exit 1 ;; esac # 使用变量来执行 sed $SED_INPLACE s/foo/bar/g test.txt这种模式的优点是可读性好且把差异集中在一个地方后续维护时只需要改这一处。8.4 善用uname做平台分支uname -s是最可靠的分支判断依据。在 macOS 上输出Darwin在大多数 Linux 发行版上输出Linux。注意不要用uname来判断具体发行版比如 Ubuntu 还是 CentOS那是另一个维度的兼容性问题。8.5 日志与错误输出规范生产环境的脚本一定要把错误输出和正常输出分开echo 正常信息 echo 错误信息 22是“重定向到标准错误”的写法。这样做的好处是在 CI 流水线里错误可以根据stderr单独抓取不会和普通日志混在一起。8.6 不要依赖 PATH 之外的路径某些软件安装在不同位置会导致找不到命令。如果一个命令的路径不固定建议在脚本开头检测并定义if command -v docker /dev/null 21; then DOCKER_BIN$(command -v docker) else echo Docker 未安装 2 exit 1 ficommand -v是 POSIX 规定的命令查找方式比which更可靠。注意which本身不是 POSIX 标准命令虽然在 Linux 和 macOS 上都有但语义不一致还不如统一用command -v。8.7 关于 macOS 系统数据占用过大的提醒这里顺带回答热搜词里的一个高频问题。很多 macOS 用户发现“系统数据”占用过大其实根因往往是缓存、日志、Docker 镜像、Xcode 派生数据这些东西。如果你是开发者可以定期清理下面几个目录~/Library/Caches/ ~/Library/Developer/Xcode/DerivedData/但这些清理命令不建议直接塞进跨平台脚本里因为它们属于 macOS 特有的目录结构放到 Linux 上不仅没有意义还可能误删数据。跨平台脚本的边界感很重要能用 POSIX 接口解决的事才放进通用脚本平台特有的操作单独写平台脚本。9. 总结理解 POSIX 不是背诵标准而是建立直觉回到文章开头的问题为什么你的 Shell 脚本能在 Linux 和 macOS 通用答案可以浓缩成一句话因为它们都遵守了 POSIX 这套接口契约你的脚本只要写在这套契约之内就天然获得跨平台能力而一旦用到契约之外的扩展就需要自己去处理平台差异。这篇文章真正想帮你建立的不是对 POSIX 条文的知识储备而是一种判断直觉写每一行命令时先问自己这个语法是 POSIX 规定的吗如果是跨平台基本安全如果不是要确认在目标机器上是否存在。遇到报错时先检查 shebang、编码、换行符这三个最基础的属性再深入命令参数。在 Linux 和 macOS 之间迁移脚本时把 GNU 工具和 BSD 工具的差异当成第一排查重点而不是怀疑脚本逻辑。尽量用#!/bin/sh POSIX 语法把脚本的可移植边界撑到最大如果做不到就明用 bash 并做平台分支。如果你还想继续深入推荐按下面几个方向学习阅读 POSIX 标准中关于 Shell and Utilities 的部分不需要全看重点看Shell Command Language。研究 SHELL 的sh与bash、zsh的差异这能让你知道哪些“高级特性”其实是某个 Shell 的私有语法。练习把日常工作里常用的 Linux 命令改成同时支持 BSD 和 GNU 的形式。学习 CI 脚本的跨平台写法因为 CI 执行器的操作系统差异往往比本地环境更严格更能暴露脚本里的兼容问题。最后给两条实际的行动建议。第一条所有脚本入库前至少在一台 Linux 和一台 macOS 上各跑一遍。如果没有 macOS 真机用虚拟机或者 CI 的 macOS 构建节点也能验证。第二条如果你现在正在维护一个只写了 Linux 版本的脚本不妨先把它复制到 macOS 上执行一次通常用不了五分钟你就能发现一批隐藏的兼容性问题。这些问题早发现比上线后半夜被叫起来排查要好得多。建议收藏这篇文章下次遇到跨平台脚本报错时对照常见问题表排查一遍。如果觉得这篇对你有帮助也欢迎分享给身边正被 Linux 和 macOS 差异困扰的同事。