兼容的答案
bash 中有很多不同的方法可以做到这一点。
但是,首先要注意bash 有许多特殊 功能(所谓的bashisms)在任何其他@987654323 中都不起作用,这一点很重要@。
特别是 arrays、associative arrays 和 pattern substitution,它们用于本文的解决方案以及其他线程是 bashisms,可能无法在许多人使用的其他 shell 下工作。
例如:在我的 Debian GNU/Linux 上,有一个名为 dash 的 标准 shell;我知道很多人喜欢使用另一个叫做ksh 的shell;还有一个特殊的工具叫做busybox,带有他自己的shell解释器(ash)。
请求的字符串
上题中要拆分的字符串是:
IN="bla@some.com;john@home.com"
我将使用此字符串的修改版本来确保我的解决方案对包含空格的字符串具有鲁棒性,这可能会破坏其他解决方案:
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
根据bash(版本>=4.2)中的分隔符拆分字符串
在 pure bash 中,我们可以创建一个 array,其中元素被 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
在较新版本的bash 中,使用 IFS 定义为命令添加前缀仅更改该命令的 IFS,然后立即将其重置为以前的值。这意味着我们可以在一行中完成上述操作:
IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'
我们可以看到字符串IN已经被存储到一个名为fields的数组中,用分号分割:
set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'
(我们也可以使用declare -p显示这些变量的内容:)
declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
请注意,read 是进行拆分的最快方式,因为没有分叉或调用外部资源。
一旦定义了数组,您就可以使用一个简单的循环来处理每个字段(或者,更确切地说,处理您现在定义的数组中的每个元素):
# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
echo "> [$x]"
done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]
或者您可以在使用 shifting 方法处理后从数组中删除每个字段,我喜欢这种方法:
while [ "$fields" ] ;do
echo "> [$fields]"
# slice the array
fields=("${fields[@]:1}")
done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]
如果你只是想要一个简单的数组打印输出,你甚至不需要循环它:
printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]
更新:最近的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"
# mail -s "This is not a spam..." "$2" </path/to/body
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"
# mail -s "This is not a spam..." "$dest" </path/to/body
printf "\e[3D, done.\n"
}
mapfile <<<"$IN" -td \; -c 1 -C myPubliMail
# Renders the same output:
# 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.
根据shell中的分隔符拆分字符串
如果你不能使用bash,或者如果你想写一些可以在许多不同的shell中使用的东西,你通常不能使用bashisms -- 这包括我们在上述解决方案中一直使用的数组。
但是,我们不需要使用数组来循环字符串的“元素”。在许多 shell 中都有一种语法用于从模式的 first 或 last 出现中删除字符串的子字符串。请注意,* 是一个通配符,代表零个或多个字符:
(到目前为止发布的任何解决方案都缺乏这种方法是我写这个答案的主要原因;)
${var#*SubStr} # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*} # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string
正如Score_Under所解释的:
#和%分别从字符串的start和end中删除可能的最短匹配子字符串,并且
## 和%% 删除可能最长的匹配子串。
使用上述语法,我们可以创建一种方法,通过删除分隔符之前或之后的子字符串,从字符串中提取子字符串“元素”。
下面的代码块在bash(包括Mac OS 的bash)、dash、ksh 和busybox 的ash 中运行良好:
(感谢Adam Katz 的comment,让这个循环变得简单多了!)
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" != "$iter" ] ;do
# extract the substring from start of string up to delimiter.
iter=${IN%%;*}
# delete this first "element" AND next separator, from $IN.
IN="${IN#$iter;}"
# Print (or doing anything with) the first "element".
echo "> [$iter]"
done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]
玩得开心!