Bash 如何像 Python `.split()` 一样按多字符分隔符拆分并存入数组?
Bash 没有直接对应 str.split() 的内建函数,可以用参数展开逐次查找多字符分隔符,再把每段字符串追加到数组中。对于不含 NUL 字节的文本,这种写法能保留换行、空格和空元素,实现类似 Python text.split('dab') 的效果。
为什么不能直接设置 IFS
IFS 会把其中的每个字符分别当作分隔符。例如:
IFS='dab'
这表示按 d、a、b 中的任意字符分隔,无法把完整的 dab 当作一个分隔符。配合 read -a 使用也是如此。
read -d 'dab' 同样不适用,因为 -d 只使用参数的第一个字符作为结束符,这里实际使用的是 d。
用参数展开拆分并存入数组
这段代码可以直接在 Bash 中运行,无需外部工具:
#!/usr/bin/env bash
text='
dab
a
b
c
dab
e
f
g
dab
h
i
j
'
sep='dab' # 分隔符必须非空
rest=$text
parts=()
while [[ $rest == *"$sep"* ]]; do
parts+=( "${rest%%"$sep"*}" )
rest=${rest#*"$sep"}
done
# 保存最后一段,即使它是空字符串
parts+=( "$rest" )
# 查看数组内容,%q 会把换行等字符显示为转义形式
for i in "${!parts[@]}"; do
printf 'parts[%s]=%q\n' "$i" "${parts[$i]}"
done
输出结果:
parts[0]=$'\n\n'
parts[1]=$'\na\nb\nc\n'
parts[2]=$'\ne\nf\ng\n'
parts[3]=$'\nh\ni\nj\n'
数组中保存的仍是真实换行,$'\n...' 只是 printf '%q' 将其显示出来的形式。
循环每次处理剩余文本中的第一个分隔符:
[[ $rest == *"$sep"* ]]判断剩余文本是否包含分隔符。${rest%%"$sep"*}删除匹配"$sep"*的最长后缀,得到第一个分隔符之前的内容。${rest#*"$sep"}删除匹配*"$sep"的最短前缀,留下第一个分隔符之后的内容。
保留 "$sep" 两侧的引号,才能按字面值匹配分隔符。这样,即使分隔符含有 *、?、[ 等模式字符,也不会把它们当作通配符。
是否会保留空元素
对于非空分隔符,这段代码会保留空元素,行为与 Python 显式指定分隔符时一致:
text | sep | 数组元素 |
|---|---|---|
abc | dab | "abc" |
| 空字符串 | dab | "" |
dabX | dab | ""、"X" |
Xdab | dab | "X"、"" |
XdabdabY | dab | "X"、""、"Y" |
最后的 parts+=( "$rest" ) 不能省略,否则会丢掉最后一段,也无法保留末尾分隔符产生的空元素。
文本中任意位置出现的 dab 都会参与拆分。例如,XdabY 会拆成 "X" 和 "Y",与 Python .split('dab') 一致。
输入来自命令的 stdout 时
如果不需要保留输出末尾的换行,可以先捕获输出,再执行上面的拆分代码:
text=$(your_command)
命令替换会删除输出末尾的所有换行符。需要保留这些换行时,可以改用:
IFS= read -r -d '' text < <(your_command) || :
将 your_command 替换为实际命令,然后执行从 sep='dab' 开始的拆分代码。
其中,IFS= 保留首尾空白,-r 保留反斜杠,-d '' 以 NUL 为结束符。普通文本没有 NUL,read 会一直读到 EOF,并返回非零状态,末尾的 || : 用来接受这种情况。这种写法只负责收集输出;如果还要判断生成输出的命令是否成功,需要单独处理它的退出状态。
使用时注意
分隔符必须非空,否则循环无法推进。Bash 变量不能保存 NUL 字节,因此这套方法适用于普通文本,不适合任意二进制数据。
读取或遍历结果时,也要保留双引号:
printf '%s' "${parts[1]}"
for part in "${parts[@]}"; do
printf '[%s]\n' "$part"
done
"${parts[@]}" 会把每个数组元素作为独立参数传递,保留其中的空格、换行和空元素。代码使用了 Bash 数组和 [[ ... ]],需要通过 Bash 执行,例如 bash script.sh。
备注:内容仅供参考。
