sed
sed是一个流编辑器(Stream EDitor),对输入流(文件或管道输入)进行基本的文本转换。踩坑背景:一个 shell 脚本本地命令行跑了 N 次都没问题,放进 Xcode Run Script 就报
RE error: repetition-operator operand invalid——根因是 GNU sed 和 BSD sed 的语法差异,完整排查过程见文末实战案例。
# base
sed 命令的一般语法是: sed OPTIONS... [SCRIPT] [INPUTFILE...]. 其中 OPTIONS 是命令行选项, SCRIPT 是 sed 的编辑脚本, 可以是一个或多个命令, INPUTFILE 是要处理的输入文件
# OPTIONS
-i: 原地编辑文件,而非输出到控制台。平台差异:GNU 直接sed -i 's/a/b/',macOS/BSD 必须带空串参数sed -i '' 's/a/b/',跨平台写法见 其他常见不兼容语法-n: 抑制自动输出
# mode
s: 替换, 规则s/PATTERN/REPLACEMENT/FLAGS. FLAGS 是可选的标志, 例如g表示全局替换sed -E 's/\[//g; s/\]//g': 多规则替换。这里表示替换[和]为空字符串sed 's/foo/bar&/' text.txt: 在 foo 前面直接加 bar。 这里的&: 表示当前匹配到的字符串
d: 删除模式sed '/foo/d' text.txt: 删除文本中所有包含 "foo" 的行
i: 在匹配模式前插入一行文本sed '/foo/i\bar' text.txt
a: 用于在匹配模式后追加一行文本sed '/foo/a\bar' text.txt
p: 用于打印匹配模式的行sed -n '1p' url.txt: 只打印第一行sed -n '/foo/p' text.txt
# &
表示当前匹配到的字符串
# BSD sed vs GNU sed 兼容性
# 为什么会有两个 sed?
历史遗留问题(POSIX / GNU 的概念背景见 shell 入门指南):
| 版本 | 来源 | 默认在哪里 | 特点 |
|---|---|---|---|
| BSD sed | BSD Unix 原版 | macOS 系统自带 /usr/bin/sed | 严格 POSIX 标准,功能少 |
| GNU sed | GNU 项目 | Linux 默认;Mac 上需 brew install gnu-sed,命令叫 gsed(默认不覆盖系统 sed) | 扩展语法多,功能强大,但不兼容 BSD |
关键问题:很多 Mac 开发者装了 gnu-sed,再按 brew 提示把它的 gnubin 目录(/opt/homebrew/opt/gnu-sed/libexec/gnubin,里面的 sed 即 GNU 版)顶到 PATH 前面,或 alias 成 sed——本地命令行和脚本里用的就是 GNU sed,写脚本时会习惯性地用 GNU 扩展语法。一旦脚本被放到 Xcode Run Script 等干净环境里执行——PATH 里只有系统目录,用的是 BSD sed——就炸了。
# 核心差异一览
| 特性 | GNU sed | BSD sed | 兼容性 |
|---|---|---|---|
| 基本正则 (BRE) | ✅ | ✅ | ⚠️ POSIX 子集兼容;GNU BRE 还有 \+、\?、\| 等扩展,BSD 不认 |
扩展正则 (ERE, -E) | ✅ | ✅ | ⚠️ 部分扩展语法不兼容 |
非贪婪匹配 .+? | ⚠️ 不报错,但按贪婪处理 | ❌ 直接报错 | ❌ 两家都不支持非贪婪 |
\s 空白字符 | ✅ | ❌ | ❌ 不兼容 |
\d 数字 | ❌ 也不支持 | ❌ | ❌ PCRE 特性,sed 里别用 |
\w 单词字符 | ✅ | ❌ | ❌ 不兼容 |
-i 原地修改 | sed -i 's/old/new/' | sed -i '' 's/old/new/' | ❌ 语法不同 |
\n 换行匹配 | ✅ | ⚠️ 部分支持 | ⚠️ 有差异 |
| 分组捕获 | \(...\) | \(...\) | ✅ 兼容 |
后向引用 \1 | ✅ | ✅ | ✅ 兼容 |
# 最容易踩的坑:非贪婪匹配
正则默认是贪婪匹配(尽可能多匹配);非贪婪匹配(.+?,尽可能少匹配)是 Perl/PCRE 的特性,sed 两家都不支持(正则语法基础见 正则表达式完整指南)。.+? 里的 ? 在 sed 里只是"0 或 1 次"量词,两个量词连用属于 POSIX 未定义行为,两家各选了一种处理方式:
- GNU sed:不报错,实际仍按贪婪匹配处理——坑更隐蔽,你以为在用非贪婪,其实没有。实测:
echo "<a><b>" | gsed -E 's/<(.+?)>/[\1]/'
# 输出 [a><b] —— 一直吃到最后的 >,并没有非贪婪
2
- BSD sed:严格校验,直接报语法错误:
sed: RE error: repetition-operator operand invalid
解决:
# ❌ PCRE 习惯写法(GNU 不报错但按贪婪处理,BSD 直接报错)
echo "my-project" | sed -E 's/.+?-([a-z0-9]+)$/\1/'
# ✅ 方案一:用 [^-] 排除法,匹配"到分隔符为止"
echo "my-project" | sed -E 's/[^-]+-([a-z0-9]+)$/\1/'
# ✅ 方案二:本来就要匹配到最后一个分隔符,直接用 .*
echo "my-project" | sed -E 's/^.*-([a-z]+[0-9]*)$/\1/'
# ✅ 方案三:复杂场景干脆用 awk,跨平台一致性更好
echo "my-project" | awk -F'-' '{print $NF}'
2
3
4
5
6
7
8
9
10
11
# 其他常见不兼容语法
1. \s 匹配空白
# ❌ PCRE 习惯写法(BRE 下 + 是字面量,GNU sed 上也不生效)
echo "hello world" | sed 's/\s+/-/g'
# ❌ GNU 专属写法(\s 是 GNU 扩展,BSD 把它当字面量 s,静默得到错误结果)
echo "hello world" | sed -E 's/\s+/-/g'
# ✅ 兼容写法(POSIX 字符类,两端都能跑)
echo "hello world" | sed -E 's/[[:space:]]+/-/g'
2
3
4
5
6
7
8
2. \d 匹配数字(GNU sed 也不支持)
# ❌ PCRE 习惯写法(\d 是 PCRE 特性,GNU sed 也不支持,实测原样输出)
echo "abc123" | sed -E 's/\d+//g'
# ✅ 兼容写法(用 [0-9])
echo "abc123" | sed -E 's/[0-9]+//g'
2
3
4
5
3. \w 单词字符
# ❌ GNU 专属(\w 是 GNU 扩展,BSD 把它当字面量 w)
echo "hello world" | sed -E 's/\w+/(&)/g' # GNU/gsed 输出 (hello) (world),BSD 输出 hello (w)orld
# ✅ 兼容写法(POSIX 字符类)
echo "hello world" | sed -E 's/[[:alnum:]_]+/(&)/g'
2
3
4
5
4. -i 原地修改参数
# ❌ GNU 写法
sed -i 's/old/new/' file.txt
# ✅ BSD 写法(需要空字符串参数)
sed -i '' 's/old/new/' file.txt
# ✅ 跨平台兼容写法(带后缀备份再删掉)
sed -i.bak 's/old/new/' file.txt && rm file.txt.bak
2
3
4
5
6
7
8
5. \n 匹配换行:GNU sed 支持在模式里直接用 \n 匹配换行,BSD sed 不行(替换串里的 \n 两家都可以)。多行文本处理建议换 awk 或 perl。
6. \b 单词边界:无统一写法——GNU 认 \b,BSD 认 [[:<:]]/[[:>:]],互不相认。跨平台请改写成显式字符类或换 awk/perl。
7. BRE 里没有 \+:\+ 是 GNU BRE 扩展,BSD 不认。POSIX BRE 表达"一个或多个"用重复写法,或改用 -E:
# ✅ POSIX BRE:[x][x]* 等价于 x+
echo "hello world" | sed 's/[a-zA-Z0-9][a-zA-Z0-9]*/(&)/g'
# ✅ 或直接用 -E(ERE 的 + 两家都支持)
echo "hello world" | sed -E 's/[a-zA-Z0-9]+/(&)/g'
2
3
4
5
# Xcode Run Script 环境的特殊坑
Xcode Run Script 的环境和本地终端完全不一样,这是很多人踩坑的根源:
1. PATH 很精简:默认只有 /usr/bin:/bin:/usr/sbin:/sbin,没有 Homebrew 路径(/opt/homebrew/bin、/usr/local/bin)。本地命令行用的 yq、node、python3、GNU sed 统统找不到,用的是系统自带的 BSD 工具。解决方法:脚本开头手动加 PATH:
#!/bin/sh
if ! command -v yq >/dev/null 2>&1; then
for p in /opt/homebrew/bin /usr/local/bin; do
[ -d "$p" ] && export PATH="$p:$PATH"
done
fi
2
3
4
5
6
2. Shell 可能不是你想的那个:默认用 /bin/sh。如果写成 bash "${PROJECT_DIR}/scripts/xxx.sh",Xcode 会按 PATH 找 bash,ARM Mac 上可能命中 /usr/local/bin 的 Intel 版 bash,报 Bad CPU type in executable。解决:用绝对路径 /bin/bash,或脚本第一行写 #!/bin/bash 直接调用。
3. 环境变量不一样:有 PROJECT_DIR、SRCROOT、BUILT_PRODUCTS_DIR 等 Xcode 专用变量,但没有 ~/.zshrc、~/.bashrc 里的别名和 PATH。
# 最佳实践
1. 脚本开头统一设置 PATH:
#!/bin/sh
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
2
2. 正则只用 POSIX 标准语法:\s/\d/\w/.+?/\b 的替代写法见上文 其他常见不兼容语法。
3. 复杂正则用 awk/perl 代替:跨平台一致性比 sed 好很多。
4. 写完脚本在干净环境下测一遍:
# 模拟 Xcode Run Script 的干净环境
env -i HOME="$HOME" PATH="/usr/bin:/bin:/usr/sbin:/sbin" /bin/sh your_script.sh
2
# 实战案例:Xcode 编译失败全过程
问题现象:Xcode 里 Cmd+R 编译报 Command PhaseScriptExecution failed with a nonzero exit code,但命令行 xcodebuild 完全正常。
排查过程:
- 看错误日志:
sed: 1: "s/^.+?-([a-z]+[0-9]*)$/ ...": RE error: repetition-operator operand invalid - 定位到脚本
scripts/build/generate_build_info.sh第 44 行 - 本地手动跑:
bash scripts/build/generate_build_info.sh✅ 完全正常,没报错 - 模拟干净环境:
env -i HOME="$HOME" PATH="/usr/bin:/bin" /bin/sh scripts/build/generate_build_info.sh❌ 复现同样的错误 - 根因:本地用的是 Homebrew 装的 GNU sed,遇到
.+?不报错(静默按贪婪处理);Xcode 环境用的是系统自带的 BSD sed,+?量词连用直接报语法错误。
修复:
# ❌ 修复前(PCRE 习惯写法,BSD 报错)
BUILT_BY=$(echo "${WORKTREE_NAME}" | sed -E 's/^.+?-([a-z]+[0-9]*)$/\1/')
# ✅ 修复后(POSIX 兼容)
BUILT_BY=$(echo "${WORKTREE_NAME}" | sed -E 's/^.*-([a-z]+[0-9]*)$/\1/')
2
3
4
5
💡 记住:本地能跑不代表 CI 能跑,更不代表 Xcode 能跑。写 shell 脚本时,永远假设你跑在最干净的最小环境里,只用最基础的语法。
# link
- shell 入门指南 - POSIX / GNU 概念背景与 shell 基础
- 正则表达式(Regex)完整指南 - 正则语法基础
- sed 命令用法小结 (opens new window)
- GNU sed 用户手册 (opens new window)