如何在Bash中使用分隔符拆分字符串?

2869
我有一个存储在变量中的字符串:
IN="bla@some.com;john@home.com"

现在我想通过分号;来分割字符串,得到如下结果:

ADDR1="bla@some.com"
ADDR2="john@home.com"

我不一定需要ADDR1ADDR2变量。如果它们是数组的元素,那就更好了。


在下面答案的建议下,我最终得到了以下内容,这正是我想要的:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

输出:

> [bla@some.com]
> [john@home.com]

有一个解决方案是设置内部字段分隔符(IFS)为;。我不确定那个答案发生了什么事情,如何将IFS重置回默认值?

关于IFS的解决方案,我尝试了这个方法并且它是有效的,我保留了旧的IFS然后将其恢复:

IN="bla@some.com;john@home.com"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

顺便说一句,当我尝试时

mails2=($IN)

在循环中只得到了第一个字符串,没有在$IN周围加上括号就可以工作。


27
关于你的“Edit2”:你可以简单地使用“unset IFS”命令,即可将其恢复为默认状态。除非你有理由认为它已经被设置为非默认值,否则无需显式保存和恢复它。此外,如果你正在函数内部执行此操作(如果没有,为什么不呢?),你可以将IFS设置为局部变量,在退出函数后它将返回到先前的值。 - Brooks Moses
29
(a)对于可以使用 local IFS=...,给予肯定;(b)不赞成使用 unset IFS,虽然我相信使用 unset IFS 会使 IFS 的行为与默认值 $' \t\n' 相同,但这似乎是一种不好的做法,因为你盲目地假设你的代码永远不会被调用时 IFS 被设置为自定义值;(c)另一个想法是调用子 shell:(IFS=$custom; ...),当子 shell 退出时,IFS 将返回到最初的状态。 - dubiousjim
我只是想快速查看路径,以决定在哪里放置可执行文件,所以我使用了运行 ruby -e "puts ENV.fetch('PATH').split(':')". 如果你想保持纯 bash,那么它无法帮助你,但使用任何具有内置分割功能的脚本语言都更容易。 - ichigolas
12
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done - user2037659
3
为了将其保存为一个数组,我不得不在另一组括号中放置内容,并将\n更改为一个空格。因此,最终行是 mails=($(echo $IN | tr ";" " "))。现在我可以使用数组表示法 mails[index] 来检查 mails 的元素,或者只需在循环中迭代。 - afranques
显示剩余5条评论
38个回答

44

对于Darron的答案,我有一种不同的看法,这是我的做法:

IN="bla@some.com;john@home.com"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)

我认为它可以!运行上面的命令,然后执行“echo $ADDR1 ... $ADDR2”,我得到“bla@some.com ... john@home.com”的输出。 - nickjb
1
这对我来说非常有效... 我用它来迭代一个包含逗号分隔的DB、SERVER、PORT数据的字符串数组,以使用mysqldump。 - Nick
5
诊断:IFS=";" 的分配仅存在于 $(...; echo $IN) 子shell 中;这就是为什么一些读者(包括我)最初认为它不起作用的原因。我一开始以为所有的 $IN 都被 ADDR1 吸收了。但是nickjb是正确的,它确实可以工作。原因是 echo $IN 命令使用当前 $IFS 的值解析其参数,然后使用空格分隔符将它们回显到标准输出中,而不考虑 $IFS 的设置。因此,其净效应就像调用 read ADDR1 ADDR2 <<< "bla@some.com john@home.com" 一样(请注意,输入是基于空格分隔而不是 ; 分隔)。 - dubiousjim
1
这个在空格和换行符上会失败,并且还会扩展echo $IN中的通配符*,使用未引用的变量扩展。 - user8017719
我真的很喜欢这个解决方案。提供一些为什么它有效的描述会非常有用,使它成为更好的答案。 - Michael Gaskill
为什么这个变量($IN)可以工作,但是静态引用的文本却不能工作呢? read ADDR1 ADDR2 <<<$(IFS=";"; echo "bla@some.com;john@home.com") - nd34567s32e

40

如果你不使用数组,那么这个一行代码怎么样:

IFS=';' read ADDR1 ADDR2 <<<$IN

1
考虑使用 read -r ... 来确保输入中的两个字符 "\t" 以相同的两个字符形式出现在您的变量中(而不是单个制表符)。 - dubiousjim
-1 这在这里不起作用(Ubuntu 12.04)。在您的片段中添加“echo“ ADDR1 $ADDR1”\n echo“ ADDR2 $ADDR2”将输出“ADDR1 bla@some.com john@home.com\nADDR2”(\n为换行符)。 - Luca Borrione
1
这可能是由于IFS和here string之间存在的一个bug,在bash 4.3中得到了修复。引用$IN应该可以解决这个问题。(理论上,$IN在展开后不会被分隔或进行globbing,这意味着引号应该是不必要的。即使在4.3中,还有至少一个报告并计划修复的bug,因此引用仍然是个好主意。) - chepner
如果$in包含换行符,即使$IN已引用,此代码也会出现错误。并添加了一个尾随换行符。 - user8017719
这种解决方案以及许多其他解决方案的问题在于它假定$IN中恰好有两个元素,或者你愿意将第二个及后续项目合并到ADDR2中。我知道这符合要求,但这是一颗定时炸弹。 - Steven the Easily Amused

39
在Bash中,即使变量包含换行符,也有一种经过验证的方法可以确保其正常工作:
IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

请看:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

这个技巧的关键是使用read命令的-d选项(分隔符),并将其设为空分隔符,以便read强制读取所有输入内容。我们将变量in中的内容精确地传递给read,由于使用了printf,没有尾随换行符。请注意,在printf中也放置了分隔符,以确保传递给read的字符串具有尾随分隔符。如果没有它,read将修剪潜在的尾随空字段:
$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

保留末尾空字段。


Bash≥4.4的更新

自Bash 4.4以来,内置的mapfile(又名readarray)支持-d选项以指定分隔符。因此,另一种规范的方法是:

mapfile -d ';' -t array < <(printf '%s;' "$in")

5
在那个列表中,我发现它是少有的能够同时正确处理 \n、空格和 * 的解决方案。而且没有循环;执行后 shell 中可以访问数组变量(与得到最高赞的答案相反)。请注意,in=$'...' 这里需要用单引号而不是双引号。我认为这个答案值得更多的赞。 - John_West
如果我想使用“%”作为分隔符,则“mapfile”示例会失败。我建议使用“printf'%s'“$ in%””。 - Robin A. Meade
@RobinA.Meade 使用 printf '%s%%' 替代:在 printf 的格式说明符中使用 %% 以获得单个百分号。 - gniourf_gniourf

34

不设置IFS

如果只有一个冒号,您可以这样做:

a="foo:bar"
b=${a%:*}
c=${a##*:}

你将获得:

b = foo
c = bar

24

这是一个简洁的三行代码:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

其中IFS根据分隔符划分单词,()用于创建数组。然后使用[@]以将每个项作为单独的单词返回。

如果在此之后有任何代码,则还需要还原$IFS,例如unset IFS


5
$in未使用引号,可以扩展通配符。 - user8017719

15
以下Bash/zsh函数将第一个参数按照第二个参数指定的分隔符进行拆分:
split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

例如,命令

$ split 'a;b;c' ';'
产生。
a
b
c

例如,这个输出可以被管道传递给其他命令。示例:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

与其他给出的解决方案相比,这个方案具有以下优势:

  • IFS未被覆盖:由于即使是局部变量也存在动态作用域,循环中覆盖 IFS 会导致新值泄漏到从循环内执行的函数调用中。

  • 未使用数组:使用 read 将字符串读入数组在 Bash 中需要标志 -a,在 zsh 中需要标志 -A

如果需要,可以按照以下方式将该函数放入脚本中:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

似乎无法处理分隔符长度大于1个字符的情况:split=$(split "$content" "file://") - madprops
help read 中可以看到,True 表示:-d delim 会一直读取输入,直到读取到 DELIM 的第一个字符,而不是换行符。 - bisgardo

12

你可以将awk应用于许多情况

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

你也可以使用这个

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"

11

有很多答案和很多复杂性。尝试一个更简单的解决方案:

echo "string1, string2" | tr , "\n"

tr (read, translate) 将输入中的第一个参数替换为第二个参数。

因此,tr , "\n" 将逗号替换为新行字符,并使输入变为:

string1
string2

3
这将打印出两个标记,但不会将它们放入变量中。 (因此并没有完全回答问题。您可能需要调整答案使其正常工作。) - Jay Sullivan
这是 https://dev59.com/_XNA5IYBdhLWcg3wjOhS#918898 的完全副本。 - bfontaine

11

有一个像这样简单而聪明的方法:

echo "add:sfff" | xargs -d: -i  echo {}

但你必须使用GNU xargs,BSD xargs不支持-d delim参数。如果你像我一样使用苹果电脑,你可以安装GNU xargs:

brew install findutils

然后

echo "add:sfff" | gxargs -d: -i  echo {}

10
这里有一些很酷的答案(尤其是errator)。但是如果要类比其他语言中的split(分割)函数,我选择了这个:
IN="bla@some.com;john@home.com"
declare -a a="(${IN//;/ })";

现在,${a[0]}${a[1]}等都如您所预期的那样。使用${#a[*]}获取项数。当然,也可以进行迭代:

for i in ${a[*]}; do echo $i; done

重要提示:

这个方法适用于没有空格的情况,可以解决我的问题,但不一定能解决你的问题。如果有空格的情况,建议选择 $IFS 方法。


当“IN”包含两个以上的电子邮件地址时,无法正常工作。请参考palindrom's answer,这个答案提供了相同的思路(但已经修复)。 - oHo
最好使用${IN//;/ }(双斜杠)来让它也适用于多个值。请注意,任何通配符(*?[)都将被展开。并且尾随的空字段将被丢弃。 - user8017719

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接