兼容性答案
在bash中,有很多不同的方法可以做到这一点。
然而,首先需要注意的是,bash
有许多特殊功能(所谓的bashisms),这些功能在其他任何shell中都无法工作。
特别是,在本帖以及其他帖子中使用的解决方案中,使用了数组、关联数组和模式替换,这些都是bashisms,可能在许多人使用的其他shell下无法工作。
例如:在我的Debian GNU/Linux上,有一个名为dash的标准shell;我知道很多人喜欢使用另一个叫ksh的shell;还有一个特殊的工具叫busybox,它有自己的shell解释器ash。
对于posix shell兼容的答案,请参考本答案的最后部分!
请求的字符串
上述问题中要拆分的字符串是:
IN="bla@some.com;john@home.com"
我将使用这个字符串的修改版本来确保我的解决方案对包含空格的字符串具有鲁棒性,以免破坏其他解决方案。
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
在
bash(版本>=4.2)中,根据分隔符拆分字符串。
在纯粹的bash中,我们可以通过使用临时值来创建一个由元素组成的数组,该临时值用于
IFS(输入字段分隔符)。IFS除其他外,还告诉bash在定义数组时应将哪个字符视为元素之间的分隔符:
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS
在较新的版本中,使用IFS定义前缀来改变命令的IFS,只会对该命令产生影响,并且在之后立即将其重置为先前的值。这意味着我们可以用一行代码完成上述操作。
IFS=\; read -a fields <<<"$IN"
set | grep ^IFS=
我们可以看到字符串
IN
已经存储在一个名为
fields
的数组中,通过分号进行拆分:
set | grep ^fields=\\\|^IN=
我们还可以使用
declare -p
来显示这些变量的内容。
declare -p IN fields
请注意,
read
是最快的拆分方式,因为没有调用任何外部资源或进程。
一旦数组被定义,你可以使用一个简单的循环来处理每个字段(或者说是你现在定义的数组中的每个元素)。
for x in "${fields[@]}"
echo "> [$x]"
done
或者你可以在处理完数组后,使用一种“移位”方法将每个字段从数组中删除,我喜欢这种方法。
while [ "$fields" ]
echo "> [$fields]"
fields=("${fields[@]:1}")
done
如果你只想要一个简单的数组打印输出,甚至不需要遍历它:
printf "> [%s]\n" "${fields[@]}"
更新:最近的
bash版本>= 4.4
在较新的bash版本中,你还可以尝试使用命令mapfile:
mapfile -td \; fields < <(printf "%s\0" "$IN")
这个语法保留特殊字符、换行符和空字段!
如果你不想包含空字段,可以按照以下方式操作:
mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}") # drop '\n' added by '<<<'
使用
mapfile
,您还可以跳过声明数组并隐式地“循环”遍历分隔的元素,在每个元素上调用一个函数。
myPubliMail() {
printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
printf "\e[3D, done.\n"
}
mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail
(注意:如果您不在意字符串末尾的空字段或者它们不存在,那么格式字符串末尾的
\0
是无用的。)
mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail
# Seq: 0: Sending mail to 'bla@some.com', done.
# Seq: 1: Sending mail to 'john@home.com', done.
# Seq: 2: Sending mail to 'Full Name <fulnam@other.org>', done.
或者你可以使用<<<,然后在函数体中添加一些处理来去掉它添加的换行符。
myPubliMail() {
local seq=$1 dest="${2%$'\n'}"
printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
printf "\e[3D, done.\n"
}
mapfile <<<"$IN" -td \; -c 1 -C myPubliMail
根据分隔符在shell中拆分字符串
如果你不能使用bash
,或者你想要编写可以在许多不同的shell中使用的代码,通常你不能使用bashisms,这包括我们在上面解决方案中使用的数组。
然而,我们不需要使用数组来循环遍历字符串的“元素”。许多shell中都有一种语法,用于从字符串的第一个或最后一个匹配模式的位置删除子字符串。注意,*
是一个通配符,代表零个或多个字符:
(迄今为止,任何已发布的解决方案中缺乏这种方法是我撰写这个答案的主要原因;)
${var#*SubStr}
${var##*SubStr}
${var%SubStr*}
${var%%SubStr*}
根据
Score_Under的解释:
#和%从字符串的开头和结尾删除最短匹配的子字符串,
##和%%删除最长匹配的子字符串。
使用上述语法,我们可以创建一种方法,通过删除定界符之前或之后的子字符串来提取字符串中的“元素”子字符串。
下面的代码块在
bash(包括Mac OS的
bash
)、
dash、
ksh、
lksh、
yash、
zsh和
busybox的
ash中都能正常工作。
(多亏了
Adam Katz的
评论, 这个循环变得简单多了!)
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" != "$iter" ] ;do
iter=${IN%%;*}
IN="${IN#$iter;}"
printf '> [%s]\n' "$iter"
done
为什么不使用cut
?
cut
在处理大文件中提取列非常有用,但是反复执行forks(var=$(echo ... | cut ...)
)很快就会变得过于繁琐!
这里是一个正确的语法,在许多posix shell中经过测试,使用cut
,正如DougW的另一个答案所建议的:
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
i=1
while iter=$(echo "$IN"|cut -d\; -f$i) ; [ -n "$iter" ] ;do
printf '> [%s]\n' "$iter"
i=$((i+1))
done
为了比较执行时间,我写下了这个。
在我的树莓派上,它看起来是这样的:
$ export TIMEFORMAT=$'(%U + %S) / \e[1m%R\e[0m : %P '
$ time sh splitDemo.sh >/dev/null
(0.000 + 0.019) / 0.019 : 99.63
$ time sh splitDemo_cut.sh >/dev/null
(0.051 + 0.041) / 0.188 : 48.98
整体执行时间大约长了10倍,使用1个叉子切割,按领域进行!
local IFS=...
,给予肯定;(b)不赞成使用unset IFS
,虽然我相信使用 unset IFS 会使 IFS 的行为与默认值 $' \t\n' 相同,但这似乎是一种不好的做法,因为你盲目地假设你的代码永远不会被调用时 IFS 被设置为自定义值;(c)另一个想法是调用子 shell:(IFS=$custom; ...)
,当子 shell 退出时,IFS 将返回到最初的状态。 - dubiousjimruby -e "puts ENV.fetch('PATH').split(':')"
. 如果你想保持纯 bash,那么它无法帮助你,但使用任何具有内置分割功能的脚本语言都更容易。 - ichigolasfor x in $(IFS=';';echo $IN); do echo "> [$x]"; done
- user2037659\n
更改为一个空格。因此,最终行是mails=($(echo $IN | tr ";" " "))
。现在我可以使用数组表示法mails[index]
来检查mails
的元素,或者只需在循环中迭代。 - afranques