819

在 Bash 脚本中,我想将一行分成几部分并将它们存储在一个数组中。

例如,给定以下行:

Paris, France, Europe

我想让结果数组看起来像这样:

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

一个简单的实现是可取的;速度无所谓。我该怎么做?

4

21 回答 21

1344
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}"

较大的负偏移选择距离数组末尾较远的位置。请注意旧表格中减号前的空格。这是必需的。

于 2012-05-14T15:16:48.597 回答
522

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


错误答案#1

IFS=', ' read -r -a array <<< "$string"

1:这是对$IFS. $IFS变量的值被视为单个可变长度字符串分隔符,而是被视为一单字符字符串分隔符,其中从输入行分离的每个字段read都可以由集合中的任何字符终止(在此示例中为逗号空格)。

实际上,对于真正的坚持者来说,完整的含义$IFS稍微复杂一些。从bash 手册

shell 将IFS的每个字符视为分隔符,并将其他扩展的结果拆分为使用这些字符作为字段终止符的单词。如果IFS未设置,或者它的值恰好是<space><tab><newline>(默认值),那么<space><tab><newline>的序列位于先前扩展结果的开头和结尾处被忽略,并且任何不在开头或结尾的IFS字符序列都用于分隔单词。如果IFS的值不是默认值,则空格字符<space><tab><的序列只要空白字符在 IFS 的值中(一个 IFS 空白字符),就会在单词的开头和结尾忽略IFS中不是IFS空白的任何字符,以及任何相邻的IFS空白字符,都会分隔一个字段。IFS空白字符序列也被视为分隔符。如果IFS的值为空,则不发生分词。

基本上,对于 的非默认非空值$IFS,字段可以用(1)一个或多个字符的序列来分隔,这些字符都来自“IFS 空白字符”集(即<space>中的任何一个,<tab><newline>(“换行符”表示换行符(LF)$IFS,或(2)任何非“IFS 空白字符”$IFS与围绕它的任何“IFS 空白字符”一起出现在输入行中。

对于 OP,我在上一段中描述的第二种分离模式可能正是他想要的输入字符串,但我们可以确信我描述的第一种分离模式根本不正确。例如,如果他的输入字符串是'Los Angeles, United States, North America'

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]="")

也许OP不会关心这一点,但这仍然是一个值得了解的限制。它降低了解决方案的鲁棒性和通用性。

这个问题可以通过在输入字符串之前附加一个虚拟尾随分隔符来解决,read稍后我将演示。


错误答案#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'(或'Los Angeles:United States:North America')。

此外,分词之后通常是文件名扩展又名路径名扩展,又名通配符),如果这样做,可能会损坏包含字符*,?[后跟](并且,如果extglob设置,括号片段前面是?, *, +, @, )的单词或!)通过将它们与文件系统对象进行匹配并相应地扩展单词(“globs”)。set -f这三个回答者中的第一个通过预先运行禁用通配符巧妙地解决了这个问题。从技术上讲,这是可行的(尽管您可能应该添加set +f之后为可能依赖于它的后续代码重新启用 globbing),但是为了破解本地代码中的基本字符串到数组解析操作而不得不弄乱全局 shell 设置是不可取的。

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

注意:如果您要使用此解决方案,最好使用参数扩展${string//:/ }的“模式替换”形式,而不是麻烦地调用命令替换(它分叉 shell)、启动管道和运行外部可执行文件(或),因为参数扩展纯粹是 shell 内部操作。(此外,对于and解决方案,输入变量应在命令替换中用双引号引起来;否则分词将在命令中生效并可能与字段值混淆。此外,命令替换的形式比旧的形式更可取trsedtrsedecho$(...)`...`形式,因为它简化了命令替换的嵌套并允许文本编辑器更好地突出显示语法。)


错误答案#3

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

这个答案与#2几乎相同。不同之处在于,回答者假设字段由两个字符分隔,其中一个在 default 中表示,$IFS另一个不是。他通过使用模式替换扩展删除非 IFS 表示的字符,然后使用分词来拆分幸存的 IFS 表示的分隔符字符上的字段,解决了这个相当具体的情况。

这不是一个非常通用的解决方案。此外,可以说逗号实际上是这里的“主要”分隔符,并且剥离它然后根据空格字符进行字段拆分是完全错误的。再次考虑我的反例:'Los Angeles, United States, North America'.

同样,文件名扩展可能会损坏扩展的单词,但这可以通过使用set -f和 then临时禁用分配的通配符来防止set +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 -fand中再次解决set +f

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

因此,总而言之,假设您有一个单字符分隔符,并且它是非“IFS 空白字符”或者您不关心空字段,并且您将关键语句包装在set -fand中set +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赋值临时。你可能会想,为什么不直接在语句中添加一个 no-op 命令词来: builtin使$IFS赋值临时呢?这不起作用,因为它也会使$array分配成为临时的:

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

所以,我们实际上陷入了僵局,有点像第 22 条规则。但是,当eval运行它的代码时,它是在shell环境中运行的,就好像它是正常的静态源代码一样,因此我们可以$array在参数内部运行赋值,eval使其在shell环境中生效,而$IFS前缀赋值以命令为前缀eval不会超过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)

嗯什么?OP 有一个字符串变量,需要将其解析为数组。这个“答案”从粘贴到数组文字中的输入字符串的逐字内容开始。我想这是一种方法。

看起来回答者可能假设该$IFS变量会影响所有上下文中的所有 bash 解析,这是不正确的。从 bash 手册:

IFS内部字段分隔符,用于扩展后的分词,并使用read内置命令    将行拆分为单词。默认值为<space><tab><newline>

所以这个$IFS特殊变量实际上只在两种情况下使用:(1)在扩展之后执行的分词(意味着在解析 bash 源代码时执行)和(2)用于将输入行拆分为read内置的单词。

让我试着更清楚地说明这一点。我认为区分parsingexecution可能会很好。Bash必须先解析源代码,这显然是一个解析事件,然后它执行代码,这就是扩展进入画面的时候。扩展实际上是一个执行事件。此外,我对$IFS上面刚刚引用的变量的描述有异议;与其说分词是在扩展之后执行的,我会说分词是扩展期间执行的,或者更准确地说,分词扩展过程。短语“分词”仅指扩展这一步;它永远不应该用来指对 bash 源代码的解析,尽管不幸的是,文档似乎确实经常使用“split”和“words”这两个词。这是 bash 手册的linux.die.net 版本的相关摘录:

将其拆分为单词后在命令行上执行扩展。执行的扩展有七种:大括号扩展波浪号扩展参数和变量扩展命令替换算术扩展分词路径名扩展

展开顺序为:大括号展开;波浪号扩展、参数和变量扩展、算术扩展和命令替换(以从左到右的方式完成);分词;和路径名扩展。

您可能会争辩说GNU 版本的手册做得稍微好一些,因为它在扩展部分的第一句中选择了“令牌”而不是“单词”:

将其拆分为令牌后,在命令行上执行扩展。

重要的一点是,$IFS不会改变 bash 解析源代码的方式。解析 bash 源代码实际上是一个非常复杂的过程,涉及到对 shell 语法的各种元素的识别,例如命令序列、命令列表、管道、参数扩展、算术替换和命令替换。在大多数情况下,bash 解析过程不能通过变量赋值之类的用户级操作来改变(实际上,这个规则有一些小例外;例如,请参阅各种compatxxshell 设置,它可以即时改变解析行为的某些方面)。然后根据上述文档摘录中分解的“扩展”的一般过程,扩展从这个复杂的解析过程产生的上游“单词”/“令牌”,其中扩展(扩展?)文本的分词到下游文字只是该过程的一个步骤。分词只涉及从前一个扩展步骤中吐出的文本;它不会影响直接从源字节流解析的文字文本。


错误答案#7

string='first line
        second line
        third line'

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

这是最好的解决方案之一。请注意,我们又回到了使用read. 我之前不是说read不合适,因为它执行两级拆分,而我们只需要一个吗?这里的诀窍是,您可以read以这样一种方式调用,即它只有效地进行一级拆分,特别是每次调用只拆分一个字段,这需要在循环中重复调用它的成本。这有点花招,但它确实有效。

但也有问题。第一:当您向NAME提供至少一个参数时read,它会自动忽略从输入字符串中分离出来的每个字段中的前导和尾随空格。如本文前面所述,无论是否$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 循环过早中断,并且我们丢失了 final 字段。

从技术上讲,同样的错误也影响了前面的例子。不同之处在于字段分隔符被视为 LF,这是您未指定-d选项时的默认值,并且<<<("here-string") 机制在将其提供为之前自动将 LF 附加到字符串命令的输入。因此,在这些情况下,我们无意中在输入中附加了一个额外的虚拟终止符,从而意外地解决了最终字段丢失的问题。让我们将此解决方案称为“虚拟终结者”解决方案。我们可以在此处字符串中实例化它时,通过自己将其与输入字符串连接起来,手动为任何自定义分隔符应用虚拟终止符解决方案:

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

到了,问题解决了。read另一种解决方案是仅在 (1)返回失败和 (2)$REPLY为空时才中断 while 循环,这意味着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 。它当然可以通过如前所述的显式修剪操作单独剥离,但显然手动虚拟终结器方法直接解决了它,所以我们可以这样做。手动 dummy-terminator 解决方案实际上非常方便,因为它一次性解决了这两个问题(dropped-final-field 问题和 appended-LF 问题)。

所以,总的来说,这是一个非常强大的解决方案。唯一剩下的弱点是缺乏对多字符分隔符的支持,我将在稍后解决。


错误答案#8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

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

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

首先,只是为了解决这个问题,请注意,就像read进行字段解析时的行为一样,readarray如果尾随字段为空,则删除它。同样,这可能不是 OP 关心的问题,但它可能适用于某些用例。稍后我会回到这个问题。

其次,和以前一样,它不支持多字符分隔符。我稍后也会对此进行修复。

第三,所写的解决方案不解析OP的输入字符串,事实上,它不能按原样使用来解析它。我也会对此进行扩展。

由于上述原因,我仍然认为这是对 OP 问题的“错误答案”。下面我将给出我认为正确的答案。


正确答案

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

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

我们看到结果与我们从#7read中讨论的循环解决方案的双条件方法得到的结果相同。我们几乎可以通过手动虚拟终结器技巧来解决这个问题:

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,不需要循环,并且不满足于单字符分隔符。


修整解决方案

最后,我想展示我自己的相当复杂的修剪解决方案-C callback,使用readarray. 不幸的是,我已经用完了 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")
于 2017-07-19T21:20:22.683 回答
250

这是一种不设置 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)

注意:此答案使用split+glob 运算符。因此,为了防止某些字符(例如*)的扩展,最好暂停此脚本的通配符。

于 2013-03-14T02:20:13.680 回答
121
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

打印三个

于 2015-07-14T11:54:19.137 回答
32

接受的答案适用于一行中的值。
如果变量有几行:

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]
于 2015-07-24T21:24:27.003 回答
31

有时我碰巧接受的答案中描述的方法不起作用,特别是如果分隔符是回车符。
在这些情况下,我以这种方式解决了:

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
于 2012-11-02T13:44:37.083 回答
13

如果你使用 macOS 并且不能使用 readarray,你可以简单地这样做——

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

迭代元素:

for element in "${array[@]}"
do
    echo $element
done
于 2020-08-05T15:29:18.850 回答
12

这适用于我在 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" " "))

简单的 :-)

于 2019-05-15T09:03:02.900 回答
9

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

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

打印 1

于 2016-06-03T15:24:19.477 回答
8

将字符串拆分为数组的关键是", ". 使用多字符分隔符的任何解决方案IFS本质上都是错误的,因为 IFS 是一组这些字符,而不是字符串。

如果您指定IFS=", ",则字符串将在 EITHER ","OR" "或它们的任何组合上中断,这不是 . 的两个字符分隔符的准确表示", "

您可以使用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...

使用第二种形式,没有子外壳,它本质上会更快。


由 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
##
于 2017-11-26T19:59:33.320 回答
7

纯 bash 多字符分隔符解决方案。

正如其他人在这个线程中指出的那样,OP的问题给出了一个逗号分隔字符串被解析为数组的例子,但没有表明他/她是否只对逗号分隔符、单字符分隔符或多字符感兴趣分隔符。

由于谷歌倾向于将这个答案排在搜索结果的顶部或附近,我想为读者提供一个关于多字符分隔符问题的强有力的答案,因为至少在一个回复中也提到了这一点。

如果您正在寻找多字符分隔符问题的解决方案,我建议您查看Mallikarjun M的帖子,特别是gniourf_gniourf的回复, 他使用参数扩展提供了这个优雅的纯 BASH 解决方案:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

链接到引用的评论/引用的帖子

链接到引用的问题:如何在 bash 中的多字符分隔符上拆分字符串?

于 2018-11-13T13:19:11.633 回答
2

试试这个

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

这很简单。如果需要,您还可以添加声明(并删除逗号):

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

添加 IFS 以撤消上述操作,但在新的 bash 实例中没有它也可以工作

于 2016-03-04T06:02:07.580 回答
1

在不修改 IFS 的情况下执行此操作的另一种方法:

read -r -a myarray <<< "${string//, /$IFS}"

与其更改 IFS 以匹配我们想要的分隔符,我们可以用via的内容替换所有出现的我们想要的分隔符。", "$IFS"${string//, /$IFS}"

也许这对于非常大的字符串来说会很慢?

这是基于丹尼斯威廉姆森的回答。

于 2018-05-31T05:56:22.787 回答
1

我在解析如下输入时遇到了这篇文章:word1,word2,...

以上都没有帮助我。通过使用 awk 解决了它。如果对某人有帮助:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done
于 2019-01-24T16:51:13.547 回答
0

更新:不要这样做,因为 eval 有问题。

稍微少一点仪式:

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

例如

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
于 2015-06-09T23:28:20.287 回答
0
#!/bin/bash

string="a | b c"
pattern=' | '

# replaces pattern with newlines
splitted="$(sed "s/$pattern/\n/g" <<< "$string")"

# Reads lines and put them in array
readarray -t array2 <<< "$splitted"

# Prints number of elements
echo ${#array2[@]}
# Prints all elements
for a in "${array2[@]}"; do
        echo "> '$a'"
done

此解决方案适用于较大的分隔符(超过一个字符)。
如果原始字符串中已有换行符,则不起作用

于 2022-01-10T15:39:43.030 回答
-1

这是我的黑客!

使用 bash 将字符串按字符串拆分是一件非常无聊的事情。发生的情况是,我们的方法有限,仅在少数情况下有效(由“;”、“/”、“.”等分割),或者我们在输出中有各种副作用。

下面的方法需要一些操作,但我相信它可以满足我们的大部分需求!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: https://dba.stackexchange.com/questions/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi
于 2018-01-31T20:45:59.900 回答
-1

由于有很多方法可以解决这个问题,让我们从定义我们希望在我们的解决方案中看到的内容开始。

  1. Bashreadarray为此提供了一个内置函数。让我们使用它。
  2. 避免丑陋和不必要的技巧,例如更改IFS、循环、使用eval或添加额外元素然后删除它。
  3. 找到一种简单易读的方法,可以很容易地适应类似的问题。

readarray命令最容易使用换行符作为分隔符。使用其他分隔符,它可能会向数组添加额外的元素。最简洁的方法是首先将我们的输入调整为一个可以很好地工作的表单,readarray然后再将其传入。

此示例中的输入没有多字符分隔符。如果我们应用一点常识,最好将其理解为逗号分隔的输入,每个元素可能需要对其进行修剪。我的解决方案是用逗号将输入分成多行,修剪每个元素,并将其全部传递给readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'
于 2020-03-22T03:17:30.503 回答
-1

对于多线元素,为什么不类似

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT
于 2020-05-09T16:42:31.100 回答
-2

另一种方法是:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

现在您的元素存储在“arr”数组中。遍历元素:

for i in ${arr[@]}; do echo $i; done
于 2017-08-09T03:21:05.540 回答
-3

另一种方法可以是:

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

在这个 'arr' 之后是一个包含四个字符串的数组。这不需要处理 IFS 或读取或任何其他特殊的东西,因此更简单和直接。

于 2016-09-13T16:21:10.617 回答