如何在Bash中将字符串分割成数组?

925

在Bash脚本中,我想要将一行内容分割为多个片段并存储到一个数组中。

例如,给定以下这一行:

Paris, France, Europe

我希望最终的数组看起来像这样:

array[0] = Paris
array[1] = France
array[2] = Europe

最好使用简单的实现方式,速度不重要。我该如何做?


69
这是谷歌搜索结果排名第一的答案,但由于问题不幸地要求使用“, ”(逗号空格)作为分隔符而不是单个字符(如逗号),因此答案存在争议。如果您只对后者感兴趣,可以在此处找到更容易理解的答案:https://dev59.com/_XNA5IYBdhLWcg3wjOhS - antak
2
如果你想要处理一个字符串,而不关心它是否为数组,那么 cut 是一个有用的 bash 命令,也可以定义分隔符。https://en.wikibooks.org/wiki/Cut 你还可以从固定宽度的记录结构中提取数据。https://en.wikipedia.org/wiki/Cut_(Unix) https://www.computerhope.com/unix/ucut.htm - JGFMK
25个回答

1495
IFS=', ' read -r -a array <<< "$string"
请注意,$IFS 中的字符被单独视为分隔符,因此在这种情况下,字段可以通过逗号或空格之一分隔而不是两个字符的序列。有趣的是,当逗号和空格同时出现在输入中时,不会创建空字段,因为空格被特殊处理。
要访问单个元素:
echo "${array[0]}"

遍历元素:

for element in "${array[@]}"
do
    echo "$element"
done

要同时获取索引和数值:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

最后一个示例很有用,因为Bash数组是稀疏的。换句话说,您可以删除一个元素或添加一个元素,然后索引不是连续的。

unset "array[1]"
array[42]=Earth
获取数组中元素的数量: ```

要获取数组中元素的数量:

```
echo "${#array[@]}"

如上所述,数组可以是稀疏的,因此您不应使用长度来获取最后一个元素。以下是在Bash 4.2及更高版本中的方法:

echo "${array[-1]}"

在任何 Bash 版本中(自2.05b之后):

echo "${array[@]: -1:1}"

负数的偏移量越大,选择的位置距离数组末尾越远。请注意老式写法中减号前面的空格,这是必须的。


17
只需使用IFS=', ',就不必单独删除空格。测试:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}" - l0b0
6
谢谢,我不知道当时我在想什么。顺便说一下,我喜欢用declare -p array来进行测试输出。 - Dennis Williamson
3
@YisraelDov:Bash本身无法处理CSV。它无法区分引号内外的逗号。您需要使用一个理解CSV的工具,例如高级语言中的库,比如Python中的csv模块。 - Dennis Williamson
1
@tetris11:这可以两种方式。数组索引是一个算术上下文,通常不需要使用美元符号。 - Dennis Williamson
11
请注意,str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str" 这个命令将字符串按照逗号和空格分隔成一个数组 array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")。需要注意的是,这只适用于没有空格的字段,因为 IFS=', ' 是一组单独的字符,而不是字符串定界符。 - dawg
显示剩余30条评论

674

这个问题的所有答案都在某种程度上存在错误。


错误答案#1

IFS=', ' read -r -a array <<< "$string"
1: 这是对 $IFS 的误用。$IFS 变量的值被视为一组单字符字符串分隔符,而不是一个单一的可变长度字符串分隔符,每个由 read 从输入行中拆分出的字段可以由集合中的任何字符终止(在此示例中为逗号或空格)。
实际上,对于真正的坚持者来说,$IFS 的完整含义略微复杂。从 bash 手册 中可以了解到:
Shell会将每个字符的IFS作为分隔符,并使用这些字符作为字段终止符将其他扩展的结果拆分成单词。如果IFS未设置或其值恰好为默认值“”,则忽略前一个扩展结果开头和结尾的、和序列,并且任何不在开头或结尾的IFS字符序列都用于分隔单词。如果IFS具有非默认值,则忽略以空格字符、制表符和换行符开头和结尾的空白字符,只要空白字符在IFS值(IFS空白字符)中即可。IFS中任何不是IFS空白的字符及其相邻的任何IFS空白字符都将分隔字段。IFS空白字符序列也被视为分隔符。如果IFS的值为空,则不进行单词拆分。
基本上,对于非默认非空值的 $IFS,字段可以通过以下方式之一分隔:(1) 由一个或多个字符序列组成,这些字符都来自“IFS空白字符”集合中的字符(也就是说,无论哪个字符是 <space><tab><newline>(“newline”表示 换行符(LF))在 $IFS 中的任何位置存在),或者 (2) 在输入行中与任何“IFS空白字符”相邻的任何非“IFS空白字符”一起出现的字符。

对于 OP 来说,我在上一段中描述的第二种分隔模式可能正是他想要的输入字符串,但我们可以非常确定地说,我描述的第一种分隔模式根本不正确。例如,如果他的输入字符串是 '洛杉矶,美国,北美',会怎样呢?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")
2: 即使您使用单个字符分隔符(例如仅逗号,即没有后续空格或其他内容),如果$string变量的值恰好包含任何LF,则read遇到第一个LF时将停止处理。read内置每次调用仅处理一行。即使您仅将输入管道或重定向到read语句,就像我们在此示例中使用here-string机制一样,未处理的输入也保证会丢失。驱动read内置的代码不知道其包含命令结构中的数据流。
你可以认为这不太可能会导致问题,但仍然是一个微妙的危险,如果可能的话应该避免。这是由于read内置实际上进行了两个级别的输入分割:首先是按行分割,然后是按字段分割。由于OP只想要一个级别的分割,所以这种使用read内置不合适,我们应该避免使用它。
3: 这种解决方案存在一个不明显的潜在问题,即read始终会删除结尾为空的字段,尽管它在其他情况下保留空字段。以下是演示:
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

也许问题提出者并不在意这个,但这仍然是一个值得了解的限制。它减少了解决方案的鲁棒性和普适性。
可以通过在输入字符串末尾添加一个虚拟分隔符来解决这个问题,在稍后演示时我会说明。

错误答案 #2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

类似的想法:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(注:我添加了答案中遗漏的命令替换符号周围的括号。)

类似的想法:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

这些解决方案利用数组赋值中的单词拆分来将字符串拆分为字段。有趣的是,就像 read 一样,通用单词拆分也使用 $IFS 特殊变量,尽管在这种情况下,它被暗示为设置为其默认值 <space><tab><newline>,因此任何一个或多个 IFS 字符序列(现在都是空白字符)都被视为字段分隔符。
这解决了 read 所提交的两级拆分问题,因为单词拆分本身只构成一级拆分。但与之前一样,问题在于输入字符串中的各个字段可能已经包含 $IFS 字符,因此它们在单词拆分操作期间会被错误地拆分。这在这些回答者提供的任何示例输入字符串中都不是问题(多么方便...),但当然这并不改变使用此惯用法的任何代码库在某个时刻违反该假设的风险。再次考虑我的反例 '洛杉矶、美国、北美'(或 'Los Angeles:United States:North America')。
此外,单词拆分通常会跟随文件名扩展(又称路径名扩展或globbing),如果执行,可能会破坏包含字符*?[后跟]的单词(如果设置了extglob,则是由?*+@!引导的带括号片段),通过将它们与文件系统对象匹配并相应地展开单词(“globs”)。前三个回答者中的第一个巧妙地解决了这个问题,通过事先运行set -f来禁用globbing。从技术上讲,这样做是可行的(尽管您可能应该在之后添加set +f以重新启用对其有依赖的后续代码的globbing),但需要在本地代码中操作全局shell设置以黑客基本的字符串到数组解析操作是不可取的。

这个答案的另一个问题是所有空字段都将丢失。这可能是一个问题,也可能不是,具体取决于应用程序。

注意:如果您要使用此解决方案,最好使用参数扩展${string//:/ }“模式替换”形式,而不是费力地调用命令替换(它会分叉shell),启动管道并运行外部可执行文件(trsed),因为参数扩展纯粹是一个shell内部操作。(另外,对于trsed解决方案,输入变量应在命令替换中用双引号括起来;否则,单词分割将在echo命令中生效,可能会干扰字段值。此外,命令替换的$(...)形式优于旧的`...`形式,因为它简化了命令替换的嵌套,并允许文本编辑器进行更好的语法高亮显示。)

错误答案 #3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

这个答案与#2几乎相同。区别在于回答者假设字段由两个字符分隔,其中一个用默认的$IFS表示,另一个则没有。他通过模式替换扩展删除了非IFS表示字符,然后使用单词拆分将字段拆分为剩余的IFS表示分隔符字符。
这不是一个非常通用的解决方案。此外,可以认为逗号是真正的“主要”分隔符字符,在去除逗号后依赖空格字符进行字段拆分是错误的。再次考虑我的反例:'洛杉矶,美国,北美'
同样地,文件名扩展可能会破坏扩展单词,但可以通过暂时禁用赋值的全局扩展set -fset +f来防止这种情况。
同样地,所有空字段都将丢失,这可能是应用程序是否有问题的问题。

错误答案 #4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

这段代码与#2#3类似,使用单词分割来完成任务,只是现在的代码明确将$IFS设置为仅包含输入字符串中存在的单个字段分隔符。需要重申的是,对于多字符字段分隔符(如OP的逗号空格分隔符),这种方法无法工作。但对于像此示例中使用的LF这样的单字符分隔符,它实际上接近完美。与之前错误答案中看到的一样,字段不能意外地在中间被分割,并且只有一个级别的分割,正如所需的那样。

一个问题是文件名扩展将破坏受影响的单词,如前所述,尽管这可以通过将关键语句包装在set -fset +f中来解决。

另一个潜在问题是,由于LF符合先前定义的“IFS空白字符”,所有空字段都将丢失,就像#2#3中一样。当然,如果分隔符恰好是非“IFS空白字符”,那么这将不是问题,而且根据应用程序,这可能并不重要,但它确实削弱了解决方案的普适性。

因此,总的来说,假设您有一个单字符分隔符,并且它是非“IFS空格字符”,或者您不关心空字段,并且您将关键语句包装在set -fset +f中,则此解决方案有效,否则无效。
(另外,为了信息的完整性,在bash中将LF分配给变量可以更轻松地使用$'...'语法,例如IFS=$'\n';。)

错误答案 #5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

类似的想法:

IFS=', ' eval 'array=($string)'

这个解决方案实际上是 #1(将 $IFS 设置为逗号空格)和 #2-4(使用单词拆分将字符串拆分成字段)之间的交叉。因此,它遭受了所有错误答案的大部分问题,有点像最糟糕的情况。
此外,关于第二种变体,看起来似乎完全没有必要调用 eval,因为它的参数是一个单引号字符串字面量,因此是静态已知的。但实际上,以这种方式使用 eval 有一个非常不明显的好处。通常,当您运行仅包含变量赋值的简单命令时,即不跟随实际命令单词的命令,该赋值会在 shell 环境中生效。
IFS=', '; ## changes $IFS in the shell environment

即使简单命令涉及多个变量赋值,这仍然是正确的;同样,只要没有命令词,所有变量赋值都会影响shell环境。
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

但是,如果变量赋值附加在命令名称上(我喜欢称之为“前缀赋值”),则它不会影响shell环境,而只会影响执行的命令的环境,无论它是内置命令还是外部命令:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

来自bash手册的相关引用:

如果没有命令名结果,变量分配会影响当前shell环境。否则,变量将添加到执行命令的环境中,不会影响当前shell环境。

可以利用此变量分配功能仅临时更改$IFS,这使我们避免了在第一种情况下使用$OIFS变量进行整个保存和恢复操作。但是,我们面临的挑战是我们需要运行的命令本身只是一个简单的变量分配,因此它不涉及命令词以使$IFS分配临时。你可能会想,为什么不向语句中添加一个无操作命令词,如: builtin,以使$IFS分配临时?这不起作用,因为这将使$array分配也是临时的:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

因此,我们实际上陷入了僵局,有点进退两难。但是,当eval运行其代码时,它会在shell环境中运行,就像它是普通的静态源代码一样,因此我们可以在eval参数内运行$array赋值,使其在shell环境中生效,而前缀分配给eval命令的$IFS前缀分配将不会超出eval命令的范围。这正是第二种解决方案中使用的技巧:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

因此,正如您所看到的,这实际上是一个非常聪明的技巧,并以一种不太明显的方式完成了所需的任务(至少在分配效果方面)。尽管涉及“eval”,但我实际上并不反对这个技巧;只需小心单引号参数字符串以防止安全威胁。
但由于问题的“最坏情况”问题群集,这仍然是对OP要求的错误答案。

错误答案 #6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Um...什么?OP有一个字符串变量需要解析为数组。这个“答案”以输入字符串的逐字内容开始,然后粘贴到数组文字中。我猜那是一种方法。
看起来回答者可能假定$IFS变量影响所有上下文中的bash解析,这是不正确的。从bash手册中可以看到:
IFS 用于扩展后的单词拆分和使用read内置命令将行拆分为单词的内部字段分隔符。默认值为。
因此,特殊变量$IFS实际上只在两个上下文中使用:(1)在扩展后执行的单词拆分(意味着不是解析bash源代码时)和(2)通过read内置命令将输入行拆分成单词。
让我尝试让这更清晰。我认为区分解析和执行可能是有益的。Bash必须先解析源代码,显然这是一个解析事件,然后再执行代码,这时扩展就会出现。扩展实际上是一个执行事件。此外,我对刚才引用的$IFS变量的描述表示异议。与其说单词拆分是在扩展之后执行的,不如说单词拆分是在扩展期间执行的,或者更准确地说,单词拆分是扩展过程的一部分。短语“单词拆分”仅指扩展的这一步骤;它不应该用于指bash源代码的解析,尽管不幸的是文档似乎经常抛出“split”和“words”的词汇。以下是bash手册linux.die.net version的相关摘录:
在将命令行分成单词后,执行扩展。有七种扩展:花括号扩展、波浪线扩展、参数和变量扩展、命令替换、算术扩展、单词分割和路径名扩展。
扩展的顺序为:花括号扩展;波浪线扩展、参数和变量扩展、算术扩展和命令替换(从左到右执行);单词分割;以及路径名扩展。
您可以认为GNU版本的手册做得稍微好一些,因为在“扩展”部分的第一句话中选择了“标记”而不是“单词”这个词:
在将命令行分成标记后,执行扩展。
重要的一点是,$IFS 不会改变 bash 解析源代码的方式。解析 bash 源代码实际上是一个非常复杂的过程,它涉及识别 shell 语法的各种元素,例如命令序列、命令列表、管道、参数扩展、算术替换和命令替换。就大部分而言,bash 解析过程不能被用户级别的操作如变量赋值所改变(实际上,有一些小的例外情况;例如,参见各种compatxx shell settings,可以在运行时改变某些解析行为方面的设置)。这个复杂解析过程产生的上游 "单词"/"标记" 然后根据上述文档摘录中所分解的一般 "扩展" 过程进行扩展,其中将扩展(正在扩展?)文本拆分成下游单词只是该过程的一步。单词拆分仅触及已从前面的扩展步骤中输出的文本;它不影响直接从源字节流解析出来的文字。

错误答案 #7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

这是最好的解决方案之一。请注意,我们又回到使用read了。我之前不是说过read不合适吗,因为它执行了两个层次的分割,而我们只需要一个吗?这里的诀窍在于,你可以以这样的方式调用read,使其有效地只进行一层分割,具体方法是每次仅拆分一个字段,这就需要在循环中反复调用它的代价。这有点像戏法,但它确实有效。
但是存在问题。首先:当您为read提供至少一个NAME参数时,它会自动忽略从输入字符串中分离出的每个字段中的前导和尾随空格。无论$IFS是否设置为其默认值,如本文早些时候所述,都会发生这种情况。现在,对于他特定的用例,OP可能不关心此问题,实际上,这可能是解析行为的一个理想特性。但不是每个想要将字符串解析为字段的人都希望如此。然而,有一个解决方案:read的一种有点不明显的用法是传递零个NAME参数。在这种情况下,read将整个从输入流获取的输入行存储在名为$REPLY的变量中,并且作为奖励,它不会从值中剥离前导和尾随空格。这是read的一个非常强大的用法,我在我的shell编程生涯中经常利用。以下是行为差异的演示:
string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

这个解决方案的第二个问题是它实际上没有解决自定义字段分隔符的情况,比如 OP 的逗号空格。与之前一样,不支持多字符分隔符,这是这个解决方案的一个不幸限制。我们可以尝试通过指定 -d 选项来至少在逗号上拆分,但看看会发生什么:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

可预见的是,未计算的周围空格被拉入字段值中,因此必须通过修剪操作进行后续纠正(这也可以直接在 while 循环中完成)。但还有另一个明显的错误:欧洲不见了!发生了什么?答案是,如果 read 在最后一个字段上没有遇到最终字段终止符而命中文件结尾(在这种情况下,我们可以称之为字符串结尾),则返回失败的返回代码。这会导致 while 循环过早地中断,我们失去了最后一个字段。
从技术上讲,前面的示例也受到了同样的错误影响;区别在于,字段分隔符被认为是 LF,这是当您没有指定 -d 选项时的默认值,并且 <<<(“here-string”)机制在将其作为输入提供给命令之前自动将 LF 附加到字符串。因此,在那些情况下,我们有点无意中通过不知情地附加额外的虚拟终止符来解决了丢失最后一个字段的问题。让我们称这个解决方案为“虚拟终止符”解决方案。我们可以手动将其应用于任何自定义分隔符,方法是在实例化 here-string 时将其与输入字符串连接起来:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

问题解决了。另一种解决方案是只有在两个条件都满足时才中断 while 循环:(1) read 返回失败,且 (2) $REPLY 为空,表示 read 在遇到文件结尾之前无法读取任何字符。演示:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

这种方法还揭示了由<<<重定向运算符自动附加到此处字符串的秘密LF。当然,可以通过显式修剪操作单独剥离它,如前所述,但显然手动虚拟终止符方法直接解决了它,因此我们可以采用该方法。手动虚拟终止符解决方案实际上非常方便,因为它一次性解决了这两个问题(丢弃最后一个字段问题和附加LF问题)。
因此,总体而言,这是一个相当强大的解决方案。它唯一的剩余弱点是缺乏对多字符分隔符的支持,我稍后会解决。

错误答案 #8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

这实际上来自同一篇帖子#7的答案;回答者在同一篇帖子中提供了两个解决方案。

readarray内置命令,它是mapfile的同义词,非常理想。它是一个内置命令,可以将字节流一次性解析为数组变量;不需要使用循环、条件语句、替换或其他任何东西。并且它不会从输入字符串中偷偷地去除任何空格。如果没有给出-O,它还会方便地在分配之前清除目标数组。但它仍然不完美,因此我批评它是一个“错误的答案”。

首先,为了让这个问题远离我们,注意,就像read进行字段解析时的行为一样,如果尾随字段为空,readarray会删除它。再次强调,这可能不是OP关心的问题,但对于某些用例可能很重要。我一会儿会回到这个问题。

第二,与以前一样,它不支持多字符定界符。我一会儿也会解决这个问题。

第三,原解决方案未解析OP的输入字符串,实际上,它不能直接用于解析。稍后我将进一步扩展此内容。
出于以上原因,我仍然认为这是OP问题的“错误答案”。下面我将给出我认为正确的答案。

正确答案

这是一个天真的尝试,只通过指定 -d 选项来使 #8 工作:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

我们可以看到,这个结果与我们在第7部分讨论的循环“read”解决方案中使用的双重条件方法得到的结果完全相同。我们几乎可以使用手动虚拟终止符技巧来解决这个问题:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

问题在于readarray保留了尾随字段,因为<<<重定向运算符将LF附加到输入字符串中,因此尾随字段不为空(否则它将被删除)。我们可以通过事后显式取消设置最终数组元素来解决这个问题。
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

唯一剩下的两个问题是(1)需要修剪的多余空格,以及(2)不支持多字符分隔符,实际上这两个问题是相关的。
当然,可以在之后修剪空格(例如,参见如何从Bash变量中修剪空格?)。但是,如果我们可以破解多字符分隔符,那么这将一举解决两个问题。
不幸的是,没有直接的方法使多字符分隔符起作用。我想到的最好的解决方案是预处理输入字符串,将多字符分隔符替换为一个单字符分隔符,保证不会与输入字符串的内容发生冲突。唯一具有此保证的字符是NUL字节。这是因为,在bash中(顺便说一句,在zsh中不是这样),变量不能包含NUL字节。这个预处理步骤可以在进程替换中内联完成。以下是使用awk执行此操作的方法:
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

终于完成了!这个解决方案不会错误地在字段中间分割,不会过早地切断,不会删除空字段,不会在文件名扩展时损坏自身,不会自动剥离前导和尾随空格,不会在末尾留下任何多余的LF,不需要循环,并且不仅限于单个字符作为分隔符。


修剪解决方案

最后,我想展示一下自己使用readarray的模糊-C回调选项实现的相当复杂的修剪解决方案。不幸的是,我已经超出了Stack Overflow严格的30,000字符帖子限制,所以我无法解释它。我将把这留给读者作为练习。

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

18
值得注意的是(尽管可以理解你没有足够的空间来说明),readarray 命令中的 -d 选项首次出现在 Bash 4.4 版本中。 - fbicknel
5
很好的回答(+1)。如果你将awk更改为 awk '{ gsub(/,[ ]+|$/,"\0"); print }' 并消除最终 ", " 的连接,那么就不必再进行消除最终记录的操作了。因此,在支持readarray的Bash上执行以下命令:readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")。请注意,你的方法是Bash 4.4+ ,我认为是因为在 readarray 中使用了 -d - dawg
18
哇,多么出色的回答啊!嘿嘿,我的回应是:放弃bash脚本,启动Python! - artfulrobot
14
我会把你正确的答案移到顶部,这样别人就不用浏览大量无用信息才能找到正确方法了 :-) - paxdiablo
68
这正是让你决定永远不再用Bash编程的事情。一个惊人简单的任务却有8个错误的解决方案。顺便说一句,这并没有设计限制:“让它尽可能晦涩难懂”。 - Connor
显示剩余20条评论

258

以下是一种不需要设置 IFS 的方法:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

这个想法是使用字符串替换:

${string//substring/replacement}

将所有的$substring匹配替换为空格,然后使用替换后的字符串来初始化一个数组:

(element1 element2 ... elementN)

注意:这个答案使用了 分割+扩展运算符。因此,为了防止某些字符(比如*)被扩展,最好在此脚本中暂停globbing。


1
使用这种方法... 直到我遇到一个需要分割的长字符串。CPU 占用率超过 100%,持续了一分钟以上(然后我强制停止了它)。很遗憾,因为这种方法允许按字符串而不是 IFS 中的某个字符进行分割。 - Werner Lehmann
13
警告:使用这种方法时遇到了一个问题。如果您有一个名为“”的元素,您将获得当前工作目录中所有元素。因此,string="1:2:3:4:" 将根据您的实现给出一些意外和可能危险的结果。但是,使用 (IFS=', ' read -a array <<< "$string") 没有遇到同样的错误,这种方法似乎是安全的。 - Dieter Gribnitz
4
引用 ${string//:/ } 可以防止 shell 扩展。 - Andrew White
1
我在OSX上不得不使用以下代码:array=(${string//:/ }) - Mark Thomson
2
@Jim 如果 string 中的元素包含空格怎么办? - Putnik
显示剩余10条评论

153
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

打印三个


18
我实际上更喜欢这种方法。简单明了。 - shrimpwagon
4
我复制并粘贴了这段代码,使用echo时它没有起作用,但当我在for循环中使用它时却有效。 - Ben
2
这个并不像所说的那样工作。如果@Jmoney38或shrimpwagon能够将其粘贴到终端并获得所需的输出,请在此处粘贴结果。 - abalter
2
@abalter 对我来说,使用a=($(echo $t | tr ',' "\n"))可以正常工作。使用a=($(echo $t | tr ',' ' '))也能得到相同的结果。 - user1636522
1
这是一个简单的解决方案,但不幸的是不支持带有空格的字符串。 - Diogo Cardoso
显示剩余8条评论

33

接受的答案适用于单行值。
如果变量有多行:

string='first line
        second line
        third line'

我们需要一个非常不同的命令来获取所有文本行:

while read -r line; do lines+=("$line"); done <<<"$string"

或者更简单的bash命令readarray:

readarray -t lines <<<"$string"

利用printf的功能打印所有行非常容易:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
虽然并不是每个解决方案都适用于每种情况,但你提到的readarray...用5分钟代替了我最后的两个小时... 你得到了我的支持。 - Angry 84
1
那个 while read 循环不会产生所示的输出,因为它会剥离前导/尾随空格。需要使用 while IFS= read -r line; do lines+=("$line"); done <<<"$string" 才能产生所示的输出。 - Ed Morton

32

有时候我会遇到接受答案中描述的方法不起作用的情况,特别是当分隔符是回车符时。
在这些情况下,我是这样解决的:

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

3
这对我非常有效。我需要将多个由换行符分隔的字符串放入数组中,但是read -a arr <<< "$strings"在使用IFS=$'\n'时无法工作。 - Stefan van den Akker
5
以下是使得当分隔符为换行符时,接受的答案能够正常工作的解决方案:这里是让被接受的答案在分隔符为换行符时正常工作的解决方案: - Stefan van den Akker
这并没有完全回答原来的问题。 - Mike

21

如果您使用的是 macOS 并且无法使用 readarray,您可以简单地执行以下操作:

MY_STRING="string1 string2 string3"
array=($MY_STRING)

遍历元素:

for element in "${array[@]}"
do
    echo $element
done

1
虽然这很有效,但如果MY_STRING包含通配符,它也可能导致其值扩展... - Treviño
1
这个答案并没有去掉逗号。 - itsafire
1
这在 macOS 12.12 上无法工作。该数组是未经分割而生成的,因此所有元素都被塞入第一个元素中。 - seagull

17

在我的OSX上这个方法对我有效:

string="1 2 3 4 5"
declare -a array=($string)
如果您的字符串具有不同的分隔符,请首先将其替换为空格:
string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

简单 :-)


2
适用于Bash和Zsh,这是一个优点! - Elijah W. Gagne
终于有一个在 macOS 上可用的 zsh 答案了! - Reorx
由于$string未被引用,一般情况下这是不可能正常工作的。 - Ed Morton

12
将字符串分割为数组的关键是多字符限定符", "。任何使用IFS用于多字符限定符的解决方案都是错误的,因为IFS是这些字符的集合,而不是一个字符串。
如果你指定IFS=", ",那么字符串将在","" "或它们的任意组合处被分割,这不准确地表示了两个字符限定符", "
您可以使用awksed来拆分字符串,使用处理替换:
#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

在Bash中直接使用正则表达式更加高效:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

使用第二种方式,不会有子 shell,因此速度本质上更快。


bgoldst编辑: 这里是一些基准测试,比较了我的 readarray 解决方案与 dawg 的正则表达式解决方案,我还包括了 read 解决方案(注意:我稍微修改了正则表达式解决方案,以更好地与我的解决方案协调)(另请参见我在文章下面的评论):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

1
非常棒的解决方案!我从未想过在正则表达式匹配中使用循环,$BASH_REMATCH 的巧妙用法。它有效地避免了生成子shell。我给你点赞。然而,作为批评,正则表达式本身有点不理想,因为它似乎被迫复制分隔符标记的一部分(具体来说是逗号),以解决 ERE(bash内置的“扩展”正则表达式风格)不支持非贪婪乘数(也包括lookaround)的问题。这使得它稍微不够通用和健壮。 - bgoldst
1
其次,我进行了一些基准测试,虽然对于较小的字符串性能优于其他解决方案,但由于重复的字符串重建,性能会呈指数级恶化,对于非常大的字符串来说,这将变得灾难性。请参见我对您答案的编辑。 - bgoldst
1
@bgoldst:多酷的基准测试啊!为正则表达式辩护,对于数万或数十万个字段(正则表达式正在分割的内容),可能会有某种记录形式(例如\n分隔的文本行)组成这些字段,因此灾难性的减速可能不会发生。如果您有一个包含10万个字段的字符串——也许Bash并不理想;-) 感谢基准测试。我学到了一两件事情。 - dawg
1
回应@bgoldst答案的一些评论,c_readarray在v4.4之前的Bash中无法使用。 c_readc_regex可以正常工作。 你可能会问,在哪里可以找到这样“旧”的Bash版本??像RHEL7.9这样的发行版中就有。 - goldfishalpha
我看不出如何推广你的答案,因为bash没有非贪婪正则表达式,但我想出了一种使用模式替换的方法,它似乎以与@bgoldst gawk相当的速度运行,直到大约256k个字符,只要分隔符很短(<10左右)。https://dev59.com/d2kv5IYBdhLWcg3wdQhT#73225463 - jhnc

12

这类似于 Jmoney38 的方法,但使用了 sed:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

打印1


2
在我的情况下,它会打印出1 2 3 4 - minigeek
3
这基本上只是抄袭了 tr 的答案,并且让它变得更糟。现在涉及到一个更复杂的工具,具有更复杂的语法和正则表达式。此外,原始代码中的现代 $() 语法已被过时的反引号替换。 - Kaz

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