如何在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个回答

1661
您可以设置内部字段分隔符(IFS)变量,然后让它解析为数组。当这发生在命令中时,那么对IFS的赋值仅在该单个命令的环境(到read)中进行。然后根据IFS变量值将输入解析为数组,我们可以遍历它。
此示例将解析由;分隔的一行项目,并将其推入数组中:
IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
  # process "$i"
done

这个示例是用于处理整个$IN的内容,每次输入一行,以;分隔。
while IFS=';' read -ra ADDR; do
  for i in "${ADDR[@]}"; do
    # process "$i"
  done
done <<< "$IN"

34
这可能是最好的方法。IFS 将在当前值下持续多久?如果在不应该设置它的情况下被设置,是否会破坏我的代码?我在使用完后如何重置它? - Chris Lutz
14
现在在应用修复后,只有在读取命令期间才能生效。 - Johannes Schaub - litb
20
您可以使用以下命令一次性读取所有内容,无需使用 while 循环: read -r -d '' -a addr <<< "$in" # 这里的 -d '' 是关键,它告诉 read 命令不要在第一个换行符处停止(默认为 -d),而是继续读取直到文件结尾或 NULL 字节(只出现在二进制数据中)。 - lhunath
84
在使用read命令时,如果在同一行设置IFS且没有使用分号或其他分隔符与其分开,与将其放在独立的命令中不同,那么它的作用域将被限定为该命令——因此它总是会被“还原”的,你无需手动进行任何操作。 - Charles Duffy
5
这里有一个与herestrings和IFS本地更改相关的漏洞,需要对 $IN 进行引用。该漏洞已在 bash 4.3中修复。 - chepner
显示剩余16条评论

1570

以下内容源自Bash shell脚本分割数组

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })
echo ${arrIN[1]}                  # Output: john@home.com

解释:

这个语句将字符串IN中所有的';'替换为一个空格(初始的//表示全局替换),然后将用空格分隔的字符串解释为一个数组(括号的作用就是这个)。

花括号中使用的语法来将每个';'字符替换为一个' '字符称为参数展开

以下有一些常见的问题:

  1. 如果原始字符串包含空格,则需要使用IFS
  • IFS=':'; arrIN=($IN); unset IFS;
  1. 如果原始字符串包含空格且定界符是换行符,则可以使用以下命令设置IFS
  • IFS=$'\n'; arrIN=($IN); unset IFS;

119
我想补充一点:这是最简单的方法,你可以使用 ${arrIN[1]} 访问数组元素(当然从零开始计数)。 - oz123
35
技术术语:在 ${} 内修改变量的技巧被称为“参数扩展”。 - KomodoDave
31
不,我不认为在存在空格时这个方法可行……它会将“,”转换为“ ”,然后构建一个以空格分隔的数组。 - Ethan
17
非常简洁,但对于一般使用有警告: shell 对字符串应用 单词拆分扩展,这可能是不想要的;只需尝试使用 IN="bla@some.com;john@home.com;*;broken apart"。简而言之:如果您的令牌包含嵌入式空格和/或字符,此方法将会出错,如 * 可能会使令牌匹配当前文件夹中的文件名。 - mklement0
69
出于其他原因,这是一种不好的方法:例如,如果你的字符串包含 ;*;,那么 * 将会被扩展成当前目录下的文件名列表。-1 - Charles Duffy
显示剩余19条评论

517

我看到有几个回答提到了cut命令,但它们都被删除了。很奇怪没有人详细说明这一点,因为我认为这是做这种类型的事情中最有用的命令之一,尤其是用于解析分隔符日志文件。

对于将此特定示例拆分为bash脚本数组,tr可能更有效,但可以使用cut,如果您想从中提取特定字段,则更有效。

例如:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

你显然可以将它放入循环中,并迭代-f参数以独立提取每个字段。

当你拥有一个包含像这样行的分隔符日志文件时,这变得更加有用:

2015-04-27|12345|some action|an attribute|meta data

cut 很方便,可以将文件通过 cat 命令输出并选择特定字段供后续处理。


41
使用“cut”工具非常棒,它是这项工作的正确工具!比任何shell hack清晰得多。 - MisterMiyagi
11
如果你事先知道元素的数量,这种方法才能起作用;你需要编写一些额外的逻辑来实现。它还会为每个元素运行一个外部工具。 - uli42
1
正是我所需要的,试图避免在CSV中出现空字符串。现在我也可以指定确切的“列”值了。IFS已经在循环中使用过了。对于我的情况来说比预期的要好。 - Louis Loudog Trottier
7
这个答案值得往下滚动超过半页 :) - Gucu112
1
@uli42请看一下我在我的回答中的*为什么不使用cut*段落!这里有一个小的while*循环,用于处理整行中的任意数量的字段...(但它并不真正*快速!!) - F. Hauri - Give Up GitHub
它还限制在单个字符的分隔符上。我一直在寻找一个多字符分隔符的等效方法,这就是我来到这里的原因... - undefined

375

如果你不介意立即处理它们,我喜欢这样做:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

你可以使用这种循环来初始化一个数组,但可能有更简单的方法。


你应该保留IFS的答案,它教会了我一个之前不知道的知识点并且它肯定创建了一个数组,而这个现在只是个廉价替代品。 - Chris Lutz
4
你可以将它改为 echo "$IN" | tr ';' '\n' | while read -r ADDY; do # 处理"$ADDY"; done,这样他就会幸运的,我想 :) 注意,这将产生分支,并且无法从循环内部更改外部变量(这就是为什么我使用 <<< "$IN" 语法) - Johannes Schaub - litb
17
总结评论区的辩论:一般使用的注意事项:shell对字符串应用单词拆分展开,这可能是不期望的;只需尝试使用IN="bla@some.com;john@home.com;*;broken apart"。简而言之:如果您的标记包含嵌入空格和/或字符(例如 *),会导致此方法中断,这些字符恰好与当前文件夹中的文件名匹配。 - mklement0
我尝试在“-c”上进行分割,但是“tr”将其视为自身的参数。然后我尝试在“\ -c”上进行分割,但是“tr”会在文本中任何“c”处进行分割。 - Thomas Ahle
请仅返回翻译后的文本:请解释一下 tr ';' '\n' 的含义,分号是什么鬼啊兄弟。 - Alexander Mills
显示剩余2条评论

348

兼容性答案

中,有很多不同的方法可以做到这一点。

然而,首先需要注意的是,bash有许多特殊功能(所谓的bashisms),这些功能在其他任何中都无法工作。

特别是,在本帖以及其他帖子中使用的解决方案中,使用了数组关联数组模式替换,这些都是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>"

(版本>=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"
# 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>]

或者你可以在处理完数组后,使用一种“移位”方法将每个字段从数组中删除,我喜欢这种方法。
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>]

更新:最近的版本>= 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.

根据分隔符在中拆分字符串

如果你不能使用bash,或者你想要编写可以在许多不同的shell中使用的代码,通常你不能使用bashisms,这包括我们在上面解决方案中使用的数组。

然而,我们不需要使用数组来循环遍历字符串的“元素”。许多shell中都有一种语法,用于从字符串的第一个最后一个匹配模式的位置删除子字符串。注意,*是一个通配符,代表零个或多个字符:

(迄今为止,任何已发布的解决方案中缺乏这种方法是我撰写这个答案的主要原因;)

${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的解释:

#和%从字符串的开头和结尾删除最短匹配的子字符串,

##和%%删除最长匹配的子字符串。

使用上述语法,我们可以创建一种方法,通过删除定界符之前或之后的子字符串来提取字符串中的“元素”子字符串。
下面的代码块在(包括Mac OS的bash)、中都能正常工作。
(多亏了Adam Katz评论, 这个循环变得简单多了!)
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".
    printf '> [%s]\n' "$iter"
done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

为什么不使用cut

cut在处理大文件中提取列非常有用,但是反复执行forksvar=$(echo ... | cut ...))很快就会变得过于繁琐!

这里是一个正确的语法,在许多 中经过测试,使用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个叉子切割,按领域进行!

31
###%%%这四个替换符号的删除范围可用以下方式来记忆:#%会删除最短匹配的字符串,而##%%则会删除最长匹配的。 - Score_Under
1
IFS=\; read -a fields <<<"$var" 在换行符上失败并添加尾随换行符。另一种解决方案则会删除尾随的空字段。 - user8017719
最后一种选择是否可以与在其他地方设置的字段分隔符列表一起使用? 例如,我想将其用作shell脚本,并将字段分隔符列表作为位置参数传递。 - sancho.s ReinstateMonicaCellio
是的,在循环中:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ... - F. Hauri - Give Up GitHub
@F.Hauri-GiveUpGitHub 我现在无法复现它。奇怪。 - ekkis
显示剩余2条评论

176

这对我有用:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2

3
尽管它只能使用单个字符分隔符,但这正是原帖作者想要的(以分号作为记录分隔符)。 - GuyPaddock
1
大约四年前,@Ashok回答了这个问题,而且一年多以前,@DougW也回答了这个问题,比你的回答提供了更多的信息。请提供与其他人不同的解决方案。 - MAChitgarha
在我看来,这是最简洁易懂的 cut 示例。 - bkidd
正如http://shellcheck.net/所显示的那样,由于缺乏引号,这将在某些输入字符串上出现错误。另请参见[何时在shell变量周围添加引号](https://dev59.com/NWkw5IYBdhLWcg3wMHum)(秘密TLDR:基本上总是,在您理解何时可以甚至应该省略引号之前)。 - tripleee

157

我认为AWK是解决您问题的最佳和高效命令。几乎所有Linux发行版都默认包含了AWK。

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

将会给予

bla@some.com john@home.com

当然,您可以通过重新定义awk打印字段来存储每个电子邮件地址。


12
甚至更简单:echo "bla@some.com;john@home.com" | awk 'BEGIN{RS=";"} {print}' 将输入的字符串按分号";"分隔,使用awk打印每一部分。 - Jaro
@Jaro 当我有一个带逗号的字符串并需要将其重新格式化为行时,这对我非常有效。谢谢。 - Aquarelle
在这种情况下它是有效的 -> "echo "$SPLIT_0" | awk -F' inode=' '{print $1}'"!当我尝试使用字符串(“inode =”)而不是字符(“;”)时,我遇到了问题。$ 1、$ 2、$ 3、$ 4被设置为数组中的位置!如果有一种设置数组的方法...那就更好了!谢谢! - Eduardo Lucio
@EduardoLucio,我想的是,也许你可以先用sed -i 's/inode\=/\;/g' your_file_to_process将分隔符inode=替换为;,然后在应用awk时定义-F';',希望这能帮到你。 - Tong

101
这个方法怎么样?
IN="bla@some.com;john@home.com" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

源代码


8
+1... 但我不会把变量命名为"Array",这只是我的个人偏好。很好的解决方案。 - Yzmir Ramirez
14
+1 ...但是"set"和"declare -a"是不必要的。你同样可以只使用IFS";" && Array=($IN) - ata
6
首先,@ata是正确的,这些命令中的大部分都没有作用。其次,它使用单词分割来形成数组,并且在这样做时没有阻止通配符扩展(因此,如果数组元素中有通配符字符,则这些元素将被替换为匹配的文件名)。 - Charles Duffy
1
建议使用 $'...'IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'。然后 echo "${Array[2]}" 将打印一个带有换行符的字符串。在这种情况下,set -- "$IN" 也是必要的。是的,为了防止 glob 扩展,解决方案应包括 set -f - John_West
外部链接没有解释这段代码。请为 set$* 添加说明。 - mgutt
显示剩余2条评论

93
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g'
bla@some.com
john@home.com

4
如果字符串中包含空格怎么办?例如 IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) ) 在这种情况下将产生一个包含8个元素的数组(每个单词以空格分隔一个元素),而不是2个元素(每行以分号分隔一个元素)。 - Luca Borrione
5
不,sed脚本只会产生两行文本。当你将它放入bash数组中时(默认按空格分割),才会出现多个条目的情况。 - lothar
这正是关键所在:OP需要将条目存储到数组中以便循环,正如您在他的编辑中所看到的。我认为您(好的)的答案忘了提到使用arrIN =($(echo“$IN”| sed -e's /; / \ n / g'))来实现这一点,并建议将IFS更改为IFS = $'\ n',对于那些未来需要拆分包含空格的字符串的人。(然后再恢复它)。 :) - Luca Borrione
3
好的,我会尽力进行翻译。"@Luca Good point. However the array assignment was not in the initial question when I wrote up that answer." 可以翻译为:"@Luca 说得好。然而,在我撰写答案时,初始问题中并没有包括数组分配。" - lothar

73

这个也可以工作:

IN="bla@some.com;john@home.com"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

请注意,该解决方案并非总是正确的。如果您仅传递"bla@some.com",它将同时分配给ADD1和ADD2。


1
你可以使用 -s 来避免上述问题: http://superuser.com/questions/896800/cut-lies-if-delimeter-doesn-t-exist“-f,--fields=LIST 选择仅这些字段;也打印任何包含没有分隔符字符的行,除非指定了-s选项。” - fersarr

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